From 24c45e296ab03288e9bc5768789d823c0f61416f Mon Sep 17 00:00:00 2001 From: Samuel Flynn Date: Thu, 30 Jan 2020 22:41:19 -0500 Subject: [PATCH 01/14] Initial attempt at LADX ram controller. Still needs testing --- .../ramcontroller/Links Awakening.lua | 614 ++++++++++++++++++ 1 file changed, 614 insertions(+) create mode 100644 bizhawk-co-op/ramcontroller/Links Awakening.lua diff --git a/bizhawk-co-op/ramcontroller/Links Awakening.lua b/bizhawk-co-op/ramcontroller/Links Awakening.lua new file mode 100644 index 0000000..a09fd90 --- /dev/null +++ b/bizhawk-co-op/ramcontroller/Links Awakening.lua @@ -0,0 +1,614 @@ +local NO_ITEM_VALUE = 0x00 + +local MAX_NUM_HEART_CONTAINERS = 0x0E -- 14 +local MAX_SWORD_LEVEL = 0x02 +local MAX_SHIELD_LEVEL = 0x02 +local MAX_BRACELET_LEVEL = 0x02 +local MAX_TRADING_ITEM = 0x0E +local MAX_GOLDEN_LEAVES = 0x06 + +local LOG_LEVEL_VERBOSE = 'Verbose' + +-- Source: https://datacrystal.romhacking.net/wiki/The_Legend_of_Zelda:_Link%27s_Awakening:RAM_map +local inventoryItemVals = { + [NO_ITEM_VALUE] = 'Nothing', + [0x01] = 'Sword', + [0x02] = 'Bombs', + [0x03] = 'Power bracelet', + [0x04] = 'Shield', + [0x05] = 'Bow', + [0x06] = 'Hookshot', + [0x07] = 'Fire rod', + [0x08] = 'Pegasus boots', + [0x09] = 'Ocarina', + [0x0A] = 'Feather', + [0x0B] = 'Shovel', + [0x0C] = 'Magic powder', + [0x0D] = 'Boomrang', +} + +-- All inventory item transmissions are done over the B-item address, since items could appear/disappear from other +-- inventory slots as players equip. +-- Assumption: Only one inventory item may be acquired in a single transmission interval +local B_SLOT_ADDR = 0xDB00 + +local inventorySlotInfos = { --Order is important, since we want to add items to the first available slot + {address = B_SLOT_ADDR, name = 'B Slot'}, + {address = 0xDB01, name = 'A Slot'}, + {address = 0xDB02, name = 'Inv 01'}, + {address = 0xDB03, name = 'Inv 02'}, + {address = 0xDB04, name = 'Inv 03'}, + {address = 0xDB05, name = 'Inv 04'}, + {address = 0xDB06, name = 'Inv 05'}, + {address = 0xDB07, name = 'Inv 06'}, + {address = 0xDB08, name = 'Inv 07'}, + {address = 0xDB09, name = 'Inv 08'}, + {address = 0xDB0A, name = 'Inv 09'}, + {address = 0xDB0B, name = 'Inv 10'}, +} + +local gameStateAddr = 0xDB50 +local gameStateVals = { + [0x00] = 'Title screen', + [0xFF] = 'System bootup' +} -- Otherwise, assume game is running + +local menuStateAddr = 0xDB9A +local menuStateVals = { + [0x00] = {desc = 'Pause Menu', transmitEvents = false}, -- TODO change this for inventory insanity + [0x80] = {desc = 'Game running/Title Screen Running', transmitEvents = true}, + [0xFF] = {desc = 'Death/Save+Quit Menu', transmitEvents = false}, +} + +function isGameLoaded(gameStateVal) + return gameStateVals[gameMode] == nil +end + +function isGameLoaded() + return isGameLoaded(readRAM(gameStateAddr)) +end + +function tableCount(table) + local count = 0 + for _, _ in pairs(table) do + count = count + 1 + end + return count +end + +local prevRAM = nil +local gameMode +local prevGameMode = nil + +local gameLoaded +local prevGameLoaded = true +local dying = false +local prevmode = 0 +local ramController = {} +local playercount = 1 +local possessedInventoryItems = {} + +-- Writes value to RAM using little endian +function writeRAM(address, size, value) + + -- default size byte + if (size == nil) then + size = 1 + end + + if (value == nil) then + return + end + + if size == 1 then + memory.writebyte(address, value) + elseif size == 2 then + memory.write_u16_le(address, value) + elseif size == 4 then + memory.write_u32_le(address, value) + else + console.log(string.format('ERROR: Attempt to write illegal length memory block [%s] from address [%s]. Legal lengths are 1, 2, 4.', size, address)) + end +end + +-- Reads a value from RAM using little endian +function readRAM(address, size) + + -- default size byte + if (size == nil) then + size = 1 + end + + if size == 1 then + return memory.readbyte(address) + elseif size == 2 then + return memory.read_u16_le(address) + elseif size == 4 then + return memory.read_u32_le(address) + else + console.log(string.format('ERROR: Attempt to read illegal length memory block [%s] from address [%s]. Legal lengths are 1, 2, 4.', size, address)) + end +end + +function giveInventoryItem(itemVal) + + local firstEmptySLotAddr = nil + + for _, slotInfo in ipairs(inventorySlotInfos) do + local slotAddr = slotInfo['address'] + local thisSlotsItem = readRAM(slotAddr, 1) + if thisSlotsItem == itemVal then + return -- We already have this item + end + if thisSlotsItem == NO_ITEM_VALUE then + firstEmptySLotAddr = slotAddr + return + end + end + + if not firstEmptySLotAddr then + console.log(string.format('ERROR: Attempt to award item %s, but all inventory slots are full!', inventoryItemVals[itemVal])) + return + end + + writeRAM(firstEmptySLotAddr, 1, itemVal) +end + +local ramItemAddrs = { + [0xDB0C] = {name = 'Flippers', type = 'bool'}, + [0xDB0D] = {name = 'Potion', type = 'bool'}, + [0xDB0E] = {name = 'Trading Item', type = 'num', maxVal = MAX_TRADING_ITEM}, + [0xDB0F] = {name = 'Number of secret shells', type = 'num'}, + [0xDB10] = {name = 'Slime Key', type = 'bool'}, + [0xDB11] = {name = 'Tail Key', type = 'bool'}, + [0xDB12] = {name = 'Angler Key', type = 'bool'}, + [0xDB13] = {name = 'Face Key', type = 'bool'}, + [0xDB14] = {name = 'Birdie Key', type = 'bool'}, + [0xDB15] = {name = 'Number of golden leaves', type = 'num', maxVal = MAX_GOLDEN_LEAVES}, + [0xDB43] = {name = 'Power bracelet level', type = 'num', maxVal = MAX_BRACELET_LEVEL}, + [0xDB44] = {name = 'Shield level', type = 'num', maxVal = MAX_SHIELD_LEVEL}, + [0xDB45] = {name = 'Number of arrows', type = 'num', flag = 'ammo'}, + [0xDB49] = {name = { + [0] = 'unknown song', + [1] = 'unknown song', + [2] = 'unknown song', + [3] = 'unknown song', + [4] = 'unknown song', + [5] = 'Ballad of the Wind Fish', + [6] = 'Manbo Mambo', + [7] = 'Frog\'s Song of Soul', + }, type = 'bitmask'}, + [0xDB4A] = {name = 'Ocarina selected song', type = 'num'}, + [0xDB4C] = {name = 'Magic powder quantity', type = 'num', flag = 'ammo'}, + [0xDB4D] = {name = 'Number of bombs', type = 'num', flag = 'ammo'}, + [0xDB4E] = {name = 'Sword level', type = 'num', maxVal = MAX_SWORD_LEVEL}, +-- DB56-DB58 Number of times the character died for each save slot (one byte per save slot) + [0xDB5A] = {name = 'Current health', type = 'num', flag = 'life'}, --Each increment of 08 is one full heart, each increment of 04 is one-half heart + [0xDB5B] = {name = 'Maximum health', type = 'num', maxVal = MAX_NUM_HEART_CONTAINERS}, --Max recommended value is 0E (14 hearts) + [0xDB5D] = {name = 'Rupees', type = 'num', flag = 'money', size = 2}, --2 bytes, decimal value + [0xDB76] = {name = 'Max magic powder', type = 'num'}, + [0xDB77] = {name = 'Max bombs', type = 'num'}, + [0xDB78] = {name = 'Max arrows', type = 'num'}, +-- [0xDBAE] = {name = 'Dungeon map grid position', type = 'num'}, + [0xDBD0] = {name = 'Keys possessed', type = 'num'}, + [0xDB16] = {name = 'Tail Cave', type = 'dungeonFlags'}, -- 5 byte sections (Map bool, compass bool, beak bool, nightmare key bool, key num) + [0xDB1B] = {name = 'Bottle Grotto', type = 'dungeonFlags'}, + [0xDB20] = {name = 'Key Cavern', type = 'dungeonFlags'}, + [0xDB25] = {name = 'Angler\'s Tunnel', type = 'dungeonFlags'}, + [0xDB2A] = {name = 'Catfish\'s Maw', type = 'dungeonFlags'}, + [0xDB2F] = {name = 'Face Shrine', type = 'dungeonFlags'}, + [0xDB34] = {name = 'Eagle\'s Tower', type = 'dungeonFlags'}, + [0xDB39] = {name = 'Turtle Rock', type = 'dungeonFlags'}, + [0xDB65] = {name = 'Tail Cave', type = 'dungeonState', instrumentName = 'Full Moon Cello'}, -- 00=starting state, 01=defeated miniboss, 02=defeated boss, 03=have instrument + [0xDB66] = {name = 'Bottle Grotto', type = 'num', instrumentName = 'Conch Horn'}, + [0xDB67] = {name = 'Key Cavern', type = 'num', instrumentName = 'Sea Lily\'s Bell'}, + [0xDB68] = {name = 'Angler\'s Tunnel', type = 'num', instrumentName = 'Surf Harp'}, + [0xDB69] = {name = 'Catfish\'s Maw', type = 'num', instrumentName = 'Wind Marimba'}, + [0xDB6A] = {name = 'Face Shrine', type = 'num', instrumentName = 'Coral Triangle'}, + [0xDB6B] = {name = 'Eagle\'s Tower', type = 'num', instrumentName = 'Organ of Evening Calm'}, + [0xDB6C] = {name = 'Turtle Rock', type = 'num', instrumentName = 'Thunder Drum'}, +} + +for _, slotInfo in pairs(inventorySlotInfos) do + ramItemAddrs[slotInfo['address']] = {name = slotInfo['name'], type = 'Inventory Slot'} +end + + +function promoteItem(list, newItem) -- TODO + local index + if (list[newItem] == nil) then + index = math.huge + else + index = list[newItem] + end + + local count = 0 + for item,val in pairs(list) do + count = count + 1 + if (val < index) then + list[item] = val + 1 + end + end + + list[newItem] = 0 + + if index == math.huge then + return count + else + return index + end +end + + +-- Display a message of the ram event +function getGUImessage(address, prevVal, newVal, user) + -- Only display the message if there is a name for the address + local name = ramItemAddrs[address].name + if name and prevVal ~= newVal then + -- If boolean, show 'Removed' for false + if ramItemAddrs[address].type == 'bool' then + gui.addmessage(user .. ': ' .. name .. (newVal == 0 and 'Removed' or '')) + -- If numeric, show the indexed name or name with value + elseif ramItemAddrs[address].type == 'num' then + if (type(name) == 'string') then + gui.addmessage(user .. ': ' .. name .. ' = ' .. newVal) + elseif (name[newVal]) then + gui.addmessage(user .. ': ' .. name[newVal]) + end + -- If bitflag, show each bit: the indexed name or bit index as a boolean + elseif ramItemAddrs[address].type == 'bit' then + for b=0,7 do + local newBit = bit.check(newVal, b) + local prevBit = bit.check(prevVal, b) + + if (newBit ~= prevBit) then + if (type(name) == 'string') then + gui.addmessage(user .. ': ' .. name .. ' flag ' .. b .. (newBit and '' or ' Removed')) + elseif (name[b]) then + gui.addmessage(user .. ': ' .. name[b] .. (newBit and '' or ' Removed')) + end + end + end + -- if delta, show the indexed name, or the differential + elseif ramItemAddrs[address].type == 'delta' then + local delta = newVal - prevVal + if (delta > 0) then + if (type(name) == 'string') then + gui.addmessage(user .. ': ' .. name .. (delta > 0 and ' +' or ' ') .. delta) + elseif (name[newVal]) then + gui.addmessage(user .. ': ' .. name[newVal]) + end + end + else + gui.addmessage('Unknown item ram type') + end + end +end + +-- Reset this script's record of your possessed items to what's currently in memory, ignoring any previous state +-- Used when entering into a playable state, such as when loading a save +function getPossessedItemsTable(itemsState) + + -- Create a blank possessed items table + local itemsTable = {} + for memVal, itemName in pairs(inventoryItemVals) do + if memVal ~= NO_ITEM_VALUE then + itemsTable[memVal] = false + end + end + + -- Search the passed-in itemsState for items and mark all found items as possessed + for _, slotInfo in pairs(inventorySlotInfos) do + local slotAddr = slotInfo['address'] + local itemInSlot = itemsState[slotAddr] + if itemInSlot ~= NO_ITEM_VALUE then + itemsTable[itemInSlot] = true + end + end + + return itemsTable +end + + +-- Get the list of ram values +function getTransmittableItemsState() + + local transmittableTable = {} + for address, item in pairs(ramItemAddrs) do + local skip = false + if not config.ramconfig.ammo and item.flag == 'ammo' then + skip = true + end + + if not config.ramconfig.health and item.flag == 'health' then + skip = true + end + + if not skip then + -- Default byte length to 1 + if (not item.size) then + item.size = 1 + end + + local ramval = readRAM(address, item.size) + + newRAM[address] = ramval + end + end + + return newRAM +end + + +-- Get a list of changed ram events +function getItemStateChanges(prevState, newState) + local ramevents = {} + local changes = false + + for address, val in pairs(newState) do + -- If change found + if (prevState[address] ~= val) then + getGUImessage(address, prevState[address], val, config.user) + + local itemType = ramItemAddrs[address].type + + -- If boolean, get T/F + if itemType == 'bool' then + ramevents[address] = (val ~= 0) + changes = true + -- If numeric, get value + elseif itemType == 'num' then + ramevents[address] = val + changes = true + -- If bitmask, get the changed bits + elseif itemType == 'bitmask' then + local changedBits = {} + for b=0,7 do + local newBit = bit.check(val, b) + local prevBit = bit.check(prevState[address], b) + + if (newBit ~= prevBit) then + changedBits[b] = newBit + end + end + ramevents[address] = changedBits + changes = true + elseif itemType == 'Inventory Item' then + -- Do nothing. We do a separate check for new inventory items below + else + console.log(string.format('Unknown item type [%s] for item %s (Address: %s)', itemType, ramItemAddrs[address].name, address)) + end + end + end + + local prevPossessedItems = getPossessedItemsTable(prevState) + local newPossessedItems = getPossessedItemsTable(newState) + + for itemVal, isPrevPossessed in pairs(prevPossessedItems) do + local isNewPossessed = newPossessedItems[itemVal] + if not isPrevPossessed and isNewPossessed then + changes = true + if ramevents[B_SLOT_ADDR] then -- Log an error if the assumption that only one item can be acquired at a time is violated + local existingItemName = inventoryItemVals[ramevents[B_SLOT_ADDR]] + local refusedItemName = inventoryItemVals[itemVal] + error(string.format("Error: Multiple items were acquired simultaneously. Already transmitting [%s]. Unable to transmit [%s]. ", existingItemName, refusedItemName)) + else + ramevents[B_SLOT_ADDR] = itemVal + end + end + end + + if (changes) then + return ramevents + else + return false + end +end + + +-- set a list of ram events +function applyItemStateChanges(prevRAM, their_user, newEvents) + for address, val in pairs(newEvents) do + local newval + + if config.ramconfig.verbose then + gui.addmessage(string.format('Applying state change [%s=%s]', address, val)) + end + -- If boolean type value + if ramItemAddrs[address].type == 'bool' then + newval = (val and 1 or 0) -- Coercing booleans back to 1 or 0 numeric + -- If numeric type value + elseif ramItemAddrs[address].type == 'num' then + newval = val + -- If bitflag update each bit + elseif ramItemAddrs[address].type == 'bit' then + newval = prevRAM[address] + for b, bitval in pairs(val) do + if bitval then + newval = bit.set(newval, b) + else + newval = bit.clear(newval, b) + end + end + elseif address == B_SLOT_ADDR then + giveInventoryItem(val) + else + printOutput(string.format('Unknown item type [%s] for item %s (Address: %s)', itemType, ramItemAddrs[address].name, address)) + newval = prevRAM[address] + end + + -- Write the new value + getGUImessage(address, prevRAM[address], newval, their_user) + prevRAM[address] = newval + local gameLoaded = isGameLoaded() + if gameLoaded then + writeRAM(address, ramItemAddrs[address].size, newval) + end + end + return prevRAM +end + + +client.reboot_core() +ramController.itemcount = tableCount(ramItemAddrs) + +local messageQueue = {first = 0, last = -1} +function messageQueue.isEmpty() + return messageQueue.first > messageQueue.last +end +function messageQueue.pushLeft (value) + local first = messageQueue.first - 1 + messageQueue.first = first + messageQueue[first] = value +end +function messageQueue.pushRight (value) + local last = messageQueue.last + 1 + messageQueue.last = last + messageQueue[last] = value +end +function messageQueue.popLeft () + local first = messageQueue.first + if messageQueue.isEmpty() then error('list is empty') end + local value = messageQueue[first] + messageQueue[first] = nil -- to allow garbage collection + messageQueue.first = first + 1 + return value +end +function messageQueue.popRight () + local last = messageQueue.last + if messageQueue.isEmpty() then error('list is empty') end + local value = messageQueue[last] + messageQueue[last] = nil -- to allow garbage collection + messageQueue.last = last - 1 + return value +end + + +-- Gets a message to send to the other player of new changes +-- Returns the message as a dictionary object +-- Returns false if no message is to be send +function ramController.getMessage() + -- Check if game is playing + local gameLoaded = isGameLoaded() + + -- Don't check for updated when game is not running + if not gameLoaded then + prevGameMode = gameMode + return false + end + + -- Don't bother transmitting events if we're in a menu state that would preclude that (e.g. Game Over screen) + menuState = readRAM(menuStateAddr) + local currentMenuState = menuStateVals[menuState] + if not currentMenuState then + error(string.format('Menu state contains unknown value [%s]', menuState)) + return false + end + + local transmitEventsMenuState = currentMenuState.transmitEvents + if not transmitEventsMenuState then + return false + end + + -- Initilize previous RAM frame if missing + if prevItemState == nil then + if config.ramconfig.verbose then + gui.addmessage('Doing first-time item state init') + end + prevItemState = getTransmittableItemsState() + end + + -- Game was just loaded, restore to previous known RAM state + if (gameLoaded and not isGameLoaded(prevGameMode)) then + -- get changes to prevRAM and apply them to game RAM + if config.ramconfig.verbose then + gui.addmessage('Performing save restore') + end + local newItemState = getTransmittableItemsState() + local message = getItemStateChanges(newItemState, prevItemState) + prevItemState = newItemState + if (message) then + ramController.processMessage('Save Restore', message) + end + end + + -- Load all queued changes + if config.ramconfig.verbose then + gui.addmessage('Processing all queued messages...') + end + while not messageQueue.isEmpty() do + local nextmessage = messageQueue.popLeft() + ramController.processMessage(nextmessage.their_user, nextmessage.message) + end + + -- Get current RAM events + local newRAM = getTransmittableItemsState() + local message = getItemStateChanges(prevItemState, newRAM) + + -- Update the RAM frame pointer + prevRAM = newRAM + prevGameMode = gameMode + + return message +end + + +-- Process a message from another player and update RAM +function ramController.processMessage(their_user, message) + + if config.ramconfig.verbose then + gui.addmessage(string.format('Processing message [%s] from [%s].', message, their_user)) + end + if isGameLoaded() then + prevRAM = applyItemStateChanges(prevRAM, their_user, message) + else + messageQueue.pushRight({['their_user']=their_user, ['message']=message}) -- Put the message back in the queue so we reprocess it once the game is loaded + end +end + +local configformState + +function configOK() + configformState = 'OK' +end +function configCancel() + configformState = 'Cancel' +end + + +function ramController.getConfig() + + configformState = 'Idle' + + forms.setproperty(mainform, 'Enabled', false) + + local configform = forms.newform(145, 165, '') + local chkAmmo = forms.checkbox(configform, 'Ammo', 10, 10) + local chkHealth = forms.checkbox(configform, 'Health', 10, 40) + local logLevelDropdown forms.dropdown(configform, {'Default', LOG_LEVEL_VERBOSE}, 10, 70, 100, 35) + local btnOK = forms.button(configform, 'OK', configOK, 10, 110, 50, 23) + local btnCancel = forms.button(configform, 'Cancel', configCancel, 70, 110, 50, 23) + + while configformState == 'Idle' do + coroutine.yield() + end + + local config = { + ammo = forms.ischecked(chkAmmo), + health = forms.ischecked(chkHealth), + verbose = forms.GetText(logLevelDropdown) == LOG_LEVEL_VERBOSE + } + + forms.destroy(configform) + forms.setproperty(mainform, 'Enabled', true) + + if configformState == 'OK' then + return config + else + return false + end +end + +return ramController + + From 944a456a384f222d063f4f045e24b463ef8da047 Mon Sep 17 00:00:00 2001 From: Samuel Flynn Date: Sat, 1 Feb 2020 19:38:59 -0500 Subject: [PATCH 02/14] Fix to config error printing. LADX ram controller bugfixes --- bizhawk-co-op.lua | 2 +- .../ramcontroller/Links Awakening.lua | 131 +++++++++++------- 2 files changed, 85 insertions(+), 48 deletions(-) diff --git a/bizhawk-co-op.lua b/bizhawk-co-op.lua index 55d69a4..1ba89fc 100644 --- a/bizhawk-co-op.lua +++ b/bizhawk-co-op.lua @@ -314,7 +314,7 @@ while 1 do local status, err = coroutine.resume(k) if (status == false) then if (err ~= nil) then - printOutput("Error during " .. v .. ": " .. err) + printOutput("Error during " .. v .. ": " .. tostring(err)) else printOutput("Error during " .. v .. ": No error message") end diff --git a/bizhawk-co-op/ramcontroller/Links Awakening.lua b/bizhawk-co-op/ramcontroller/Links Awakening.lua index a09fd90..f6d45b4 100644 --- a/bizhawk-co-op/ramcontroller/Links Awakening.lua +++ b/bizhawk-co-op/ramcontroller/Links Awakening.lua @@ -47,11 +47,13 @@ local inventorySlotInfos = { --Order is important, since we want to add items to {address = 0xDB0B, name = 'Inv 10'}, } -local gameStateAddr = 0xDB50 -local gameStateVals = { - [0x00] = 'Title screen', - [0xFF] = 'System bootup' -} -- Otherwise, assume game is running +local gameStateAddr = 0xDB95 +-- Source https://github.com/zladx/LADX-Disassembly/blob/4ae748bd354f94ed2887f04d4014350d5a103763/src/constants/gameplay.asm#L22-L48 +local gameStateVals = { -- Only states where we can do events are listed + [0x07] = 'Map Screen', + [0x0B] = 'Main Gameplay', + [0x0C] = 'Inventory Screen', +} local menuStateAddr = 0xDB9A local menuStateVals = { @@ -61,10 +63,10 @@ local menuStateVals = { } function isGameLoaded(gameStateVal) - return gameStateVals[gameMode] == nil + return gameStateVals[gameStateVal] ~= nil end -function isGameLoaded() +function isGameLoadedWithFetch() -- Grr. Why doesn't lua support function overloading?? return isGameLoaded(readRAM(gameStateAddr)) end @@ -76,12 +78,32 @@ function tableCount(table) return count end +function tableString(table) + + local returnStr = '{' + for key,value in pairs(table) do + returnStr = returnStr..string.format('%s=%s,', asString(key), asString(value)) + end + returnStr = returnStr..'}' + return returnStr +end + + +function asString(object) + + if type(object) == 'table' then + return tableString(object) + elseif type(object) == 'number' then + return string.format('%x', object) + else + return tostring(object) + end +end + local prevRAM = nil -local gameMode -local prevGameMode = nil -local gameLoaded -local prevGameLoaded = true +local gameLoaded = false +local prevGameLoaded = false local dying = false local prevmode = 0 local ramController = {} @@ -245,18 +267,23 @@ function getGUImessage(address, prevVal, newVal, user) -- Only display the message if there is a name for the address local name = ramItemAddrs[address].name if name and prevVal ~= newVal then + + local itemType = ramItemAddrs[address].type + -- If boolean, show 'Removed' for false - if ramItemAddrs[address].type == 'bool' then + if itemType == 'bool' then gui.addmessage(user .. ': ' .. name .. (newVal == 0 and 'Removed' or '')) + -- If numeric, show the indexed name or name with value - elseif ramItemAddrs[address].type == 'num' then + elseif itemType == 'num' then if (type(name) == 'string') then gui.addmessage(user .. ': ' .. name .. ' = ' .. newVal) elseif (name[newVal]) then gui.addmessage(user .. ': ' .. name[newVal]) end + -- If bitflag, show each bit: the indexed name or bit index as a boolean - elseif ramItemAddrs[address].type == 'bit' then + elseif itemType == 'bitmask' then for b=0,7 do local newBit = bit.check(newVal, b) local prevBit = bit.check(prevVal, b) @@ -269,16 +296,10 @@ function getGUImessage(address, prevVal, newVal, user) end end end - -- if delta, show the indexed name, or the differential - elseif ramItemAddrs[address].type == 'delta' then - local delta = newVal - prevVal - if (delta > 0) then - if (type(name) == 'string') then - gui.addmessage(user .. ': ' .. name .. (delta > 0 and ' +' or ' ') .. delta) - elseif (name[newVal]) then - gui.addmessage(user .. ': ' .. name[newVal]) - end - end + + -- If an inventory item, just show the inventory item name + elseif ram == 'Inventory Slot' then + gui.addmessage(user .. ': ' .. inventoryItemVals[newVal]) else gui.addmessage('Unknown item ram type') end @@ -332,11 +353,11 @@ function getTransmittableItemsState() local ramval = readRAM(address, item.size) - newRAM[address] = ramval + transmittableTable[address] = ramval end end - return newRAM + return transmittableTable end @@ -348,6 +369,10 @@ function getItemStateChanges(prevState, newState) for address, val in pairs(newState) do -- If change found if (prevState[address] ~= val) then + + if config.ramconfig.verbose then + printOutput(string.format('Updating address [%s] to value [%s].', asString(address), asString(val))) + end getGUImessage(address, prevState[address], val, config.user) local itemType = ramItemAddrs[address].type @@ -373,7 +398,7 @@ function getItemStateChanges(prevState, newState) end ramevents[address] = changedBits changes = true - elseif itemType == 'Inventory Item' then + elseif itemType == 'Inventory Slot' then -- Do nothing. We do a separate check for new inventory items below else console.log(string.format('Unknown item type [%s] for item %s (Address: %s)', itemType, ramItemAddrs[address].name, address)) @@ -387,6 +412,9 @@ function getItemStateChanges(prevState, newState) for itemVal, isPrevPossessed in pairs(prevPossessedItems) do local isNewPossessed = newPossessedItems[itemVal] if not isPrevPossessed and isNewPossessed then + if config.ramconfig.verbose then + printOutput(string.format('Discovered that item [%s] is newly possessed.', itemVal)) + end changes = true if ramevents[B_SLOT_ADDR] then -- Log an error if the assumption that only one item can be acquired at a time is violated local existingItemName = inventoryItemVals[ramevents[B_SLOT_ADDR]] @@ -399,6 +427,9 @@ function getItemStateChanges(prevState, newState) end if (changes) then + if config.ramconfig.verbose then + printOutput(string.format('Found events to send: %s', asString(ramevents))) + end return ramevents else return false @@ -412,7 +443,7 @@ function applyItemStateChanges(prevRAM, their_user, newEvents) local newval if config.ramconfig.verbose then - gui.addmessage(string.format('Applying state change [%s=%s]', address, val)) + printOutput(string.format('Applying state change [%s=%s]', asString(address), asString(val))) end -- If boolean type value if ramItemAddrs[address].type == 'bool' then @@ -440,7 +471,7 @@ function applyItemStateChanges(prevRAM, their_user, newEvents) -- Write the new value getGUImessage(address, prevRAM[address], newval, their_user) prevRAM[address] = newval - local gameLoaded = isGameLoaded() + local gameLoaded = isGameLoadedWithFetch() if gameLoaded then writeRAM(address, ramItemAddrs[address].size, newval) end @@ -489,11 +520,10 @@ end -- Returns false if no message is to be send function ramController.getMessage() -- Check if game is playing - local gameLoaded = isGameLoaded() + local gameLoaded = isGameLoadedWithFetch() -- Don't check for updated when game is not running if not gameLoaded then - prevGameMode = gameMode return false end @@ -513,16 +543,16 @@ function ramController.getMessage() -- Initilize previous RAM frame if missing if prevItemState == nil then if config.ramconfig.verbose then - gui.addmessage('Doing first-time item state init') + printOutput('Doing first-time item state init') end prevItemState = getTransmittableItemsState() end -- Game was just loaded, restore to previous known RAM state - if (gameLoaded and not isGameLoaded(prevGameMode)) then + if (gameLoaded and not prevGameLoaded) then -- get changes to prevRAM and apply them to game RAM if config.ramconfig.verbose then - gui.addmessage('Performing save restore') + printOutput('Performing save restore') end local newItemState = getTransmittableItemsState() local message = getItemStateChanges(newItemState, prevItemState) @@ -533,21 +563,21 @@ function ramController.getMessage() end -- Load all queued changes - if config.ramconfig.verbose then - gui.addmessage('Processing all queued messages...') - end while not messageQueue.isEmpty() do + if config.ramconfig.verbose then + printOutput('Processing incoming message') + end local nextmessage = messageQueue.popLeft() ramController.processMessage(nextmessage.their_user, nextmessage.message) end -- Get current RAM events - local newRAM = getTransmittableItemsState() - local message = getItemStateChanges(prevItemState, newRAM) + local newItemState = getTransmittableItemsState() + local message = getItemStateChanges(prevItemState, newItemState) -- Update the RAM frame pointer - prevRAM = newRAM - prevGameMode = gameMode + prevItemState = newItemState + prevGameLoaded = gameLoaded return message end @@ -556,12 +586,18 @@ end -- Process a message from another player and update RAM function ramController.processMessage(their_user, message) + if message['i'] then + message['i'] = nil -- Item splitting is not supported yet + end + if config.ramconfig.verbose then - gui.addmessage(string.format('Processing message [%s] from [%s].', message, their_user)) + printOutput(string.format('Processing message [%s] from [%s].', asString(message), asString(their_user))) end - if isGameLoaded() then - prevRAM = applyItemStateChanges(prevRAM, their_user, message) + if isGameLoadedWithFetch() then + printOutput("Game loaded. About to do the message") + prevItemState = applyItemStateChanges(prevItemState, their_user, message) else + printOutput("Game not loaded. Putting the message back on the queue") messageQueue.pushRight({['their_user']=their_user, ['message']=message}) -- Put the message back in the queue so we reprocess it once the game is loaded end end @@ -582,10 +618,11 @@ function ramController.getConfig() forms.setproperty(mainform, 'Enabled', false) - local configform = forms.newform(145, 165, '') + local configform = forms.newform(200, 190, '') local chkAmmo = forms.checkbox(configform, 'Ammo', 10, 10) local chkHealth = forms.checkbox(configform, 'Health', 10, 40) - local logLevelDropdown forms.dropdown(configform, {'Default', LOG_LEVEL_VERBOSE}, 10, 70, 100, 35) + local logLevelLabel = forms.label(configform, 'Messages', 10, 73, 60, 40) + local logLevelDropdown = forms.dropdown(configform, {'Default', LOG_LEVEL_VERBOSE}, 75, 70, 100, 35) local btnOK = forms.button(configform, 'OK', configOK, 10, 110, 50, 23) local btnCancel = forms.button(configform, 'Cancel', configCancel, 70, 110, 50, 23) @@ -596,7 +633,7 @@ function ramController.getConfig() local config = { ammo = forms.ischecked(chkAmmo), health = forms.ischecked(chkHealth), - verbose = forms.GetText(logLevelDropdown) == LOG_LEVEL_VERBOSE + verbose = forms.gettext(logLevelDropdown) == LOG_LEVEL_VERBOSE } forms.destroy(configform) From 68a15d48e0127b72e58de317b9feb155652154cb Mon Sep 17 00:00:00 2001 From: Samuel Flynn Date: Sun, 2 Feb 2020 17:35:00 -0500 Subject: [PATCH 03/14] LADX is working somewhat. Now for bugfixes --- .../ramcontroller/Links Awakening.lua | 56 ++++++++++++------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/bizhawk-co-op/ramcontroller/Links Awakening.lua b/bizhawk-co-op/ramcontroller/Links Awakening.lua index f6d45b4..77de2d4 100644 --- a/bizhawk-co-op/ramcontroller/Links Awakening.lua +++ b/bizhawk-co-op/ramcontroller/Links Awakening.lua @@ -27,11 +27,10 @@ local inventoryItemVals = { [0x0D] = 'Boomrang', } --- All inventory item transmissions are done over the B-item address, since items could appear/disappear from other --- inventory slots as players equip. --- Assumption: Only one inventory item may be acquired in a single transmission interval local B_SLOT_ADDR = 0xDB00 +local NEW_INV_ITEMS_KEY = 'New inventory Items List' + local inventorySlotInfos = { --Order is important, since we want to add items to the first available slot {address = B_SLOT_ADDR, name = 'B Slot'}, {address = 0xDB01, name = 'A Slot'}, @@ -154,7 +153,7 @@ end function giveInventoryItem(itemVal) - local firstEmptySLotAddr = nil + local firstEmptySlotAddr = nil for _, slotInfo in ipairs(inventorySlotInfos) do local slotAddr = slotInfo['address'] @@ -162,18 +161,22 @@ function giveInventoryItem(itemVal) if thisSlotsItem == itemVal then return -- We already have this item end - if thisSlotsItem == NO_ITEM_VALUE then - firstEmptySLotAddr = slotAddr - return + if thisSlotsItem == NO_ITEM_VALUE and not firstEmptySlotAddr then + firstEmptySlotAddr = slotAddr end end - if not firstEmptySLotAddr then + if not firstEmptySlotAddr then console.log(string.format('ERROR: Attempt to award item %s, but all inventory slots are full!', inventoryItemVals[itemVal])) return end - writeRAM(firstEmptySLotAddr, 1, itemVal) + + if config.ramconfig.verbose then + printOutput(string.format('About to write item val %s (%s) to addr %s', asString(itemVal), asString(inventoryItemVals[itemVal]), asString(firstEmptySlotAddr))) + end + + writeRAM(firstEmptySlotAddr, 1, itemVal) end local ramItemAddrs = { @@ -301,7 +304,7 @@ function getGUImessage(address, prevVal, newVal, user) elseif ram == 'Inventory Slot' then gui.addmessage(user .. ': ' .. inventoryItemVals[newVal]) else - gui.addmessage('Unknown item ram type') + gui.addmessage(string.format('Unknown item ram type %s', itemType)) end end end @@ -322,6 +325,9 @@ function getPossessedItemsTable(itemsState) for _, slotInfo in pairs(inventorySlotInfos) do local slotAddr = slotInfo['address'] local itemInSlot = itemsState[slotAddr] + if not itemInSlot then + error(string.format('Unable to find item in slot %s. Items state: %s', asString(slotAddr), asString(itemsState))) + end if itemInSlot ~= NO_ITEM_VALUE then itemsTable[itemInSlot] = true end @@ -409,23 +415,24 @@ function getItemStateChanges(prevState, newState) local prevPossessedItems = getPossessedItemsTable(prevState) local newPossessedItems = getPossessedItemsTable(newState) + local listOfNewlyAcquiredItemVals = {} + for itemVal, isPrevPossessed in pairs(prevPossessedItems) do local isNewPossessed = newPossessedItems[itemVal] if not isPrevPossessed and isNewPossessed then + if config.ramconfig.verbose then printOutput(string.format('Discovered that item [%s] is newly possessed.', itemVal)) end changes = true - if ramevents[B_SLOT_ADDR] then -- Log an error if the assumption that only one item can be acquired at a time is violated - local existingItemName = inventoryItemVals[ramevents[B_SLOT_ADDR]] - local refusedItemName = inventoryItemVals[itemVal] - error(string.format("Error: Multiple items were acquired simultaneously. Already transmitting [%s]. Unable to transmit [%s]. ", existingItemName, refusedItemName)) - else - ramevents[B_SLOT_ADDR] = itemVal - end + table.insert(listOfNewlyAcquiredItemVals, itemVal) end end + if table.getn(listOfNewlyAcquiredItemVals) > 0 then + ramevents[NEW_INV_ITEMS_KEY] = listOfNewlyAcquiredItemVals + end + if (changes) then if config.ramconfig.verbose then printOutput(string.format('Found events to send: %s', asString(ramevents))) @@ -439,6 +446,19 @@ end -- set a list of ram events function applyItemStateChanges(prevRAM, their_user, newEvents) + + -- First, handle the newly acquired inventory items + local listOfNewlyAcquiredItemVals = newEvents[NEW_INV_ITEMS_KEY] + if listOfNewlyAcquiredItemVals then + for _,itemVal in ipairs(listOfNewlyAcquiredItemVals) do + if config.ramconfig.verbose then + printOutput(string.format('About to award item: %s', asString(inventoryItemVals[itemVal]))) + end + giveInventoryItem(itemVal) + end + end + newEvents[NEW_INV_ITEMS_KEY] = nil + for address, val in pairs(newEvents) do local newval @@ -461,8 +481,6 @@ function applyItemStateChanges(prevRAM, their_user, newEvents) newval = bit.clear(newval, b) end end - elseif address == B_SLOT_ADDR then - giveInventoryItem(val) else printOutput(string.format('Unknown item type [%s] for item %s (Address: %s)', itemType, ramItemAddrs[address].name, address)) newval = prevRAM[address] From d7a60326e7342f5853331db383fda5198bb0a55e Mon Sep 17 00:00:00 2001 From: Samuel Flynn Date: Sun, 2 Feb 2020 22:21:29 -0500 Subject: [PATCH 04/14] Added readme notes for LADX and rancontroller bugfixes (health, rupees, dungeon flags, display messages) --- README.md | 16 +- .../ramcontroller/Links Awakening.lua | 176 +++++++++++++----- 2 files changed, 143 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 92d4416..0664413 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,11 @@ bizhawk-co-op is a Lua script for BizHawk that allows two or more people to play ## Metroid: Zero Mission Co-op -Health and ammo is shared. Items obtained by a player are given to everyone. Items locations are split such that each item can be optained by only one player. The other players will find a screw attack block in its place. The items at the unknown item locations, power grip location, and imago location can be obtained by either player. Events such as boss deaths are also shared. +Health and ammo is shared. Items obtained by a player are given to everyone. Items locations are split such that each item can be obtained by only one player. The other players will find a screw attack block in its place. The items at the unknown item locations, power grip location, and imago location can be obtained by either player. Events such as boss deaths are also shared. ## Link to the Past Co-op -Items obtained by a player are given to everyone. Items locations are split such that each item can be optained by only one player. It's usually a good idea to spread out on the overworld and communicate which items still need to be checked by who. With Split Keys or Raid Bosses enabled, it's highly recommended to enter dungeons together. The following can are configurable: +Items obtained by a player are given to everyone. Items locations are split such that each item can be obtained by only one player. It's usually a good idea to spread out on the overworld and communicate which items still need to be checked by who. With Split Keys or Raid Bosses enabled, it's highly recommended to enter dungeons together. The following can are configurable: * **Health and Ammo** Health and ammo is shared. This includes bottles, bombs, arrows, magic, etc. Death is synced, so if you notice a pause, it's because the script is waiting for everyone to die together * **Split Big Keys** If enabled then dungeon Big Keys will be split so only one person can obtain it. Otherwise either player can get them. @@ -33,7 +33,17 @@ Nothing is shared, however there are now player specific items which are mixed b This works with the latest OoT Randomizer found on the website [https://www.ootrandomizer.com](https://www.ootrandomizer.com/) and the latest major release of the source code [GitHub](https://github.com/TestRunnerSRL/OoT-Randomizer/tree/master). Set the Player Count to the number and use the same settings and seed. Each player should then set a unique Player ID (from 1 to the Player Count). The output filename should be the **same** for every player except the last number which indicates the player ID (excluding `-comp`). The logic will guarantee that every player can beat the game. * **2-Player File name example:** > - `OoT_R4AR3PKKPKF8UK7DSA_TestSeed_W2P1-comp.z64` -> - `OoT_R4AR3PKKPKF8UK7DSA_TestSeed_W2P2-comp.z64` +> - `OoT_R4AR3PKKPKF8UK7DSA_TestSeed_W2P2-comp.z64` + +## Link's Awakening DX Co-op + +Items obtained by a player are given to everyone. Unlike the Link to the Past Co-op, however, items are not split such that each item +may only be obtained by one player. Any player may obtain any item. + +Additional Options: +* **Ammo** Players' power, bomb, and arrow counts will all be synchronized +* **Health** Any health pickups or damage taken will be applied to all players +* **Rupees** Any rupees found or spend will be applied to all players ## Setup There are two different methods to install. diff --git a/bizhawk-co-op/ramcontroller/Links Awakening.lua b/bizhawk-co-op/ramcontroller/Links Awakening.lua index 77de2d4..db5b2f7 100644 --- a/bizhawk-co-op/ramcontroller/Links Awakening.lua +++ b/bizhawk-co-op/ramcontroller/Links Awakening.lua @@ -1,6 +1,7 @@ local NO_ITEM_VALUE = 0x00 local MAX_NUM_HEART_CONTAINERS = 0x0E -- 14 +local MAX_NUM_HEART_PIECES = 0x04 local MAX_SWORD_LEVEL = 0x02 local MAX_SHIELD_LEVEL = 0x02 local MAX_BRACELET_LEVEL = 0x02 @@ -56,7 +57,7 @@ local gameStateVals = { -- Only states where we can do events are listed local menuStateAddr = 0xDB9A local menuStateVals = { - [0x00] = {desc = 'Pause Menu', transmitEvents = false}, -- TODO change this for inventory insanity + [0x00] = {desc = 'Pause Menu', transmitEvents = true}, [0x80] = {desc = 'Game running/Title Screen Running', transmitEvents = true}, [0xFF] = {desc = 'Death/Save+Quit Menu', transmitEvents = false}, } @@ -184,12 +185,51 @@ local ramItemAddrs = { [0xDB0D] = {name = 'Potion', type = 'bool'}, [0xDB0E] = {name = 'Trading Item', type = 'num', maxVal = MAX_TRADING_ITEM}, [0xDB0F] = {name = 'Number of secret shells', type = 'num'}, - [0xDB10] = {name = 'Slime Key', type = 'bool'}, [0xDB11] = {name = 'Tail Key', type = 'bool'}, [0xDB12] = {name = 'Angler Key', type = 'bool'}, [0xDB13] = {name = 'Face Key', type = 'bool'}, [0xDB14] = {name = 'Birdie Key', type = 'bool'}, [0xDB15] = {name = 'Number of golden leaves', type = 'num', maxVal = MAX_GOLDEN_LEAVES}, + [0xDB16] = {name = 'Tail Cave Map', type = 'bool'}, + [0xDB17] = {name = 'Tail Cave Compass', type = 'bool'}, + [0xDB18] = {name = 'Tail Cave Owl\'s Beak', type = 'bool'}, + [0xDB19] = {name = 'Tail Cave Nightmare Key', type = 'bool'}, + [0xDB1A] = {name = 'Tail Cave Small Keys', type = 'num'}, + [0xDB1B] = {name = 'Bottle Grotto Map', type = 'bool'}, + [0xDB1C] = {name = 'Bottle Grotto Compass', type = 'bool'}, + [0xDB1D] = {name = 'Bottle Grotto Owl\'s Beak', type = 'bool'}, + [0xDB1E] = {name = 'Bottle Grotto Nightmare Key', type = 'bool'}, + [0xDB1F] = {name = 'Bottle Grotto Small Keys', type = 'num'}, + [0xDB20] = {name = 'Key Cavern Map', type = 'bool'}, + [0xDB21] = {name = 'Key Cavern Compass', type = 'bool'}, + [0xDB22] = {name = 'Key Cavern Owl\'s Beak', type = 'bool'}, + [0xDB23] = {name = 'Key Cavern Nightmare Key', type = 'bool'}, + [0xDB24] = {name = 'Key Cavern Small Keys', type = 'num'}, + [0xDB25] = {name = 'Angler\'s Tunnel Map', type = 'bool'}, + [0xDB26] = {name = 'Angler\'s Tunnel Compass', type = 'bool'}, + [0xDB27] = {name = 'Angler\'s Tunnel Owl\'s Beak', type = 'bool'}, + [0xDB28] = {name = 'Angler\'s Tunnel Nightmare Key', type = 'bool'}, + [0xDB29] = {name = 'Angler\'s Tunnel Small Keys', type = 'num'}, + [0xDB2A] = {name = 'Catfish\'s Maw Map', type = 'bool'}, + [0xDB2B] = {name = 'Catfish\'s Maw Compass', type = 'bool'}, + [0xDB2C] = {name = 'Catfish\'s Maw Owl\'s Beak', type = 'bool'}, + [0xDB2D] = {name = 'Catfish\'s Maw Nightmare Key', type = 'bool'}, + [0xDB2E] = {name = 'Catfish\'s Maw Small Keys', type = 'num'}, + [0xDB2F] = {name = 'Face Shrine Map', type = 'bool'}, + [0xDB30] = {name = 'Face Shrine Compass', type = 'bool'}, + [0xDB31] = {name = 'Face Shrine Owl\'s Beak', type = 'bool'}, + [0xDB32] = {name = 'Face Shrine Nightmare Key', type = 'bool'}, + [0xDB33] = {name = 'Face Shrine Small Keys', type = 'num'}, + [0xDB34] = {name = 'Eagle\'s Tower Map', type = 'bool'}, + [0xDB35] = {name = 'Eagle\'s Tower Compass', type = 'bool'}, + [0xDB36] = {name = 'Eagle\'s Tower Owl\'s Beak', type = 'bool'}, + [0xDB37] = {name = 'Eagle\'s Tower Nightmare Key', type = 'bool'}, + [0xDB38] = {name = 'Eagle\'s Tower Small Keys', type = 'num'}, + [0xDB39] = {name = 'Turtle Rock Map', type = 'bool'}, + [0xDB3A] = {name = 'Turtle Rock Compass', type = 'bool'}, + [0xDB3B] = {name = 'Turtle Rock Owl\'s Beak', type = 'bool'}, + [0xDB3C] = {name = 'Turtle Rock Nightmare Key', type = 'bool'}, + [0xDB3D] = {name = 'Turtle Rock Small Keys', type = 'num'}, [0xDB43] = {name = 'Power bracelet level', type = 'num', maxVal = MAX_BRACELET_LEVEL}, [0xDB44] = {name = 'Shield level', type = 'num', maxVal = MAX_SHIELD_LEVEL}, [0xDB45] = {name = 'Number of arrows', type = 'num', flag = 'ammo'}, @@ -204,27 +244,17 @@ local ramItemAddrs = { [7] = 'Frog\'s Song of Soul', }, type = 'bitmask'}, [0xDB4A] = {name = 'Ocarina selected song', type = 'num'}, + [0xDB4B] = {name = 'Toadstool', type = 'bool'}, [0xDB4C] = {name = 'Magic powder quantity', type = 'num', flag = 'ammo'}, [0xDB4D] = {name = 'Number of bombs', type = 'num', flag = 'ammo'}, [0xDB4E] = {name = 'Sword level', type = 'num', maxVal = MAX_SWORD_LEVEL}, -- DB56-DB58 Number of times the character died for each save slot (one byte per save slot) - [0xDB5A] = {name = 'Current health', type = 'num', flag = 'life'}, --Each increment of 08 is one full heart, each increment of 04 is one-half heart + --[0xDB5A] = {name = 'Current health', type = 'num', flag = 'life'}, --Each increment of 08 is one full heart, each increment of 04 is one-half heart (Don't set this directly. Use the health buffers) [0xDB5B] = {name = 'Maximum health', type = 'num', maxVal = MAX_NUM_HEART_CONTAINERS}, --Max recommended value is 0E (14 hearts) - [0xDB5D] = {name = 'Rupees', type = 'num', flag = 'money', size = 2}, --2 bytes, decimal value - [0xDB76] = {name = 'Max magic powder', type = 'num'}, - [0xDB77] = {name = 'Max bombs', type = 'num'}, - [0xDB78] = {name = 'Max arrows', type = 'num'}, + [0xDB5C] = {name = 'Number of heart pieces', type = 'num', maxVal = MAX_NUM_HEART_PIECES}, + --[0xDB5D] = {name = 'Rupees', type = 'num', flag = 'money', size = 2}, --2 bytes, decimal value (Don't set this directly. Use the buffers) -- [0xDBAE] = {name = 'Dungeon map grid position', type = 'num'}, - [0xDBD0] = {name = 'Keys possessed', type = 'num'}, - [0xDB16] = {name = 'Tail Cave', type = 'dungeonFlags'}, -- 5 byte sections (Map bool, compass bool, beak bool, nightmare key bool, key num) - [0xDB1B] = {name = 'Bottle Grotto', type = 'dungeonFlags'}, - [0xDB20] = {name = 'Key Cavern', type = 'dungeonFlags'}, - [0xDB25] = {name = 'Angler\'s Tunnel', type = 'dungeonFlags'}, - [0xDB2A] = {name = 'Catfish\'s Maw', type = 'dungeonFlags'}, - [0xDB2F] = {name = 'Face Shrine', type = 'dungeonFlags'}, - [0xDB34] = {name = 'Eagle\'s Tower', type = 'dungeonFlags'}, - [0xDB39] = {name = 'Turtle Rock', type = 'dungeonFlags'}, - [0xDB65] = {name = 'Tail Cave', type = 'dungeonState', instrumentName = 'Full Moon Cello'}, -- 00=starting state, 01=defeated miniboss, 02=defeated boss, 03=have instrument + [0xDB65] = {name = 'Tail Cave', type = 'num', instrumentName = 'Full Moon Cello'}, -- 00=starting state, 01=defeated miniboss, 02=???, 03=have instrument [0xDB66] = {name = 'Bottle Grotto', type = 'num', instrumentName = 'Conch Horn'}, [0xDB67] = {name = 'Key Cavern', type = 'num', instrumentName = 'Sea Lily\'s Bell'}, [0xDB68] = {name = 'Angler\'s Tunnel', type = 'num', instrumentName = 'Surf Harp'}, @@ -232,6 +262,18 @@ local ramItemAddrs = { [0xDB6A] = {name = 'Face Shrine', type = 'num', instrumentName = 'Coral Triangle'}, [0xDB6B] = {name = 'Eagle\'s Tower', type = 'num', instrumentName = 'Organ of Evening Calm'}, [0xDB6C] = {name = 'Turtle Rock', type = 'num', instrumentName = 'Thunder Drum'}, + [0xDB76] = {name = 'Max magic powder', type = 'num'}, + [0xDB77] = {name = 'Max bombs', type = 'num'}, + [0xDB78] = {name = 'Max arrows', type = 'num'}, + -- Buffers are rupee/health amounts that are to be added to your total over time. + -- Picking up rupees/health adds to the "add" buffers. Paying money/taking damage adds to the "subtract" buffers. + -- The game subtracts from these buffers over time, applying their effect to your money/health totals + -- Only additions to buffer values should be transmitted + [0xDB8F] = {name = 'Rupees Added', type = 'buffer', flag = 'rupees', size = 2}, + [0xDB91] = {name = 'Rupees Spent', type = 'buffer', flag = 'rupees', size = 2}, + [0xDB93] = {name = 'Health Added', type = 'buffer', flag = 'health'}, + [0xDB94] = {name = 'Health Lost', type = 'buffer', flag = 'health'}, + [0xDC04] = {name = 'Tunic Color', type = 'num'}, } for _, slotInfo in pairs(inventorySlotInfos) do @@ -275,14 +317,22 @@ function getGUImessage(address, prevVal, newVal, user) -- If boolean, show 'Removed' for false if itemType == 'bool' then - gui.addmessage(user .. ': ' .. name .. (newVal == 0 and 'Removed' or '')) + gui.addmessage(string.format('%s: %s %s', user, (newVal == 0 and 'Removed' or 'Added'), name)) - -- If numeric, show the indexed name or name with value + -- If numeric, show the name with value elseif itemType == 'num' then - if (type(name) == 'string') then - gui.addmessage(user .. ': ' .. name .. ' = ' .. newVal) - elseif (name[newVal]) then - gui.addmessage(user .. ': ' .. name[newVal]) + + local instrumentName = ramItemAddrs[address].instrumentName + if instrumentName then + if newVal == 0 then + gui.addmessage(string.format('%s: Reset %s', user, name)) + elseif newVal == 1 then + gui.addmessage(string.format('%s: Defeated mini-boss in %s', user, name)) + elseif newVal == 3 then + gui.addmessage(string.format('%s: Got instrument %s', user, instrumentName)) + end + else + gui.addmessage(string.format('%s: %s = %s', user, name, newVal)) end -- If bitflag, show each bit: the indexed name or bit index as a boolean @@ -292,17 +342,15 @@ function getGUImessage(address, prevVal, newVal, user) local prevBit = bit.check(prevVal, b) if (newBit ~= prevBit) then - if (type(name) == 'string') then - gui.addmessage(user .. ': ' .. name .. ' flag ' .. b .. (newBit and '' or ' Removed')) - elseif (name[b]) then - gui.addmessage(user .. ': ' .. name[b] .. (newBit and '' or ' Removed')) - end + gui.addmessage(string.format('%s: %s %s', user, (newBit and 'Added' or ' Removed'), name)) end end -- If an inventory item, just show the inventory item name - elseif ram == 'Inventory Slot' then - gui.addmessage(user .. ': ' .. inventoryItemVals[newVal]) + elseif itemType == 'Inventory Slot' then + gui.addmessage(string.format('%s: Found %s', user, inventoryItemVals[newVal])) + elseif itemType == 'buffer' and newVal > prevVal then + gui.addmessage(string.format('%s: %s %s', user, newVal, name)) else gui.addmessage(string.format('Unknown item ram type %s', itemType)) end @@ -351,6 +399,10 @@ function getTransmittableItemsState() skip = true end + if not config.ramconfig.rupees and item.flag == 'rupees' then + skip = true + end + if not skip then -- Default byte length to 1 if (not item.size) then @@ -373,30 +425,34 @@ function getItemStateChanges(prevState, newState) local changes = false for address, val in pairs(newState) do + + local prevVal = prevState[address] + local itemType = ramItemAddrs[address].type + -- If change found - if (prevState[address] ~= val) then + if (prevVal ~= val) then if config.ramconfig.verbose then printOutput(string.format('Updating address [%s] to value [%s].', asString(address), asString(val))) end - getGUImessage(address, prevState[address], val, config.user) - - local itemType = ramItemAddrs[address].type + getGUImessage(address, prevVal, val, config.user) -- If boolean, get T/F if itemType == 'bool' then ramevents[address] = (val ~= 0) changes = true + -- If numeric, get value elseif itemType == 'num' then ramevents[address] = val changes = true + -- If bitmask, get the changed bits elseif itemType == 'bitmask' then local changedBits = {} for b=0,7 do local newBit = bit.check(val, b) - local prevBit = bit.check(prevState[address], b) + local prevBit = bit.check(prevVal, b) if (newBit ~= prevBit) then changedBits[b] = newBit @@ -404,6 +460,14 @@ function getItemStateChanges(prevState, newState) end ramevents[address] = changedBits changes = true + + -- Only transmit buffer increases + elseif itemType == 'buffer' then + if val > prevVal then + ramevents[address] = val - prevVal + changes = true + end + elseif itemType == 'Inventory Slot' then -- Do nothing. We do a separate check for new inventory items below else @@ -460,19 +524,28 @@ function applyItemStateChanges(prevRAM, their_user, newEvents) newEvents[NEW_INV_ITEMS_KEY] = nil for address, val in pairs(newEvents) do + + local itemType = ramItemAddrs[address].type local newval if config.ramconfig.verbose then printOutput(string.format('Applying state change [%s=%s]', asString(address), asString(val))) end -- If boolean type value - if ramItemAddrs[address].type == 'bool' then + if itemType == 'bool' then newval = (val and 1 or 0) -- Coercing booleans back to 1 or 0 numeric + -- If numeric type value - elseif ramItemAddrs[address].type == 'num' then - newval = val + elseif itemType == 'num' then + local maxVal = ramItemAddrs[address].maxVal + if maxVal and val > maxVal then + newval = maxVal + else + newval = val + end + -- If bitflag update each bit - elseif ramItemAddrs[address].type == 'bit' then + elseif itemType == 'bit' then newval = prevRAM[address] for b, bitval in pairs(val) do if bitval then @@ -481,6 +554,10 @@ function applyItemStateChanges(prevRAM, their_user, newEvents) newval = bit.clear(newval, b) end end + + elseif itemType == 'buffer' then + newval = prevRAM[address] + val + else printOutput(string.format('Unknown item type [%s] for item %s (Address: %s)', itemType, ramItemAddrs[address].name, address)) newval = prevRAM[address] @@ -612,10 +689,15 @@ function ramController.processMessage(their_user, message) printOutput(string.format('Processing message [%s] from [%s].', asString(message), asString(their_user))) end if isGameLoadedWithFetch() then - printOutput("Game loaded. About to do the message") + + if config.ramconfig.verbose then + printOutput("Game loaded. About to do the message") + end prevItemState = applyItemStateChanges(prevItemState, their_user, message) else - printOutput("Game not loaded. Putting the message back on the queue") + if config.ramconfig.verbose then + printOutput("Game not loaded. Putting the message back on the queue") + end messageQueue.pushRight({['their_user']=their_user, ['message']=message}) -- Put the message back in the queue so we reprocess it once the game is loaded end end @@ -636,13 +718,14 @@ function ramController.getConfig() forms.setproperty(mainform, 'Enabled', false) - local configform = forms.newform(200, 190, '') + local configform = forms.newform(200, 220, '') local chkAmmo = forms.checkbox(configform, 'Ammo', 10, 10) local chkHealth = forms.checkbox(configform, 'Health', 10, 40) - local logLevelLabel = forms.label(configform, 'Messages', 10, 73, 60, 40) - local logLevelDropdown = forms.dropdown(configform, {'Default', LOG_LEVEL_VERBOSE}, 75, 70, 100, 35) - local btnOK = forms.button(configform, 'OK', configOK, 10, 110, 50, 23) - local btnCancel = forms.button(configform, 'Cancel', configCancel, 70, 110, 50, 23) + local chkRupees = forms.checkbox(configform, 'Rupees', 10, 70) + local logLevelLabel = forms.label(configform, 'Messages', 10, 103, 60, 40) + local logLevelDropdown = forms.dropdown(configform, {'Default', LOG_LEVEL_VERBOSE}, 75, 100, 100, 35) + local btnOK = forms.button(configform, 'OK', configOK, 10, 140, 50, 23) + local btnCancel = forms.button(configform, 'Cancel', configCancel, 70, 140, 50, 23) while configformState == 'Idle' do coroutine.yield() @@ -651,6 +734,7 @@ function ramController.getConfig() local config = { ammo = forms.ischecked(chkAmmo), health = forms.ischecked(chkHealth), + rupees = forms.ischecked(chkRupees), verbose = forms.gettext(logLevelDropdown) == LOG_LEVEL_VERBOSE } From 1057f745b79d2bf4cd02c28d744681759530d169 Mon Sep 17 00:00:00 2001 From: Samuel Flynn Date: Thu, 6 Feb 2020 21:53:39 -0500 Subject: [PATCH 05/14] Bugfixes to messages for buffers and bitmasks --- bizhawk-co-op/ramcontroller/Links Awakening.lua | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/bizhawk-co-op/ramcontroller/Links Awakening.lua b/bizhawk-co-op/ramcontroller/Links Awakening.lua index db5b2f7..5e60e48 100644 --- a/bizhawk-co-op/ramcontroller/Links Awakening.lua +++ b/bizhawk-co-op/ramcontroller/Links Awakening.lua @@ -342,15 +342,17 @@ function getGUImessage(address, prevVal, newVal, user) local prevBit = bit.check(prevVal, b) if (newBit ~= prevBit) then - gui.addmessage(string.format('%s: %s %s', user, (newBit and 'Added' or ' Removed'), name)) + gui.addmessage(string.format('%s: %s %s', user, (newBit and 'Added' or ' Removed'), name[b])) end end -- If an inventory item, just show the inventory item name elseif itemType == 'Inventory Slot' then gui.addmessage(string.format('%s: Found %s', user, inventoryItemVals[newVal])) - elseif itemType == 'buffer' and newVal > prevVal then - gui.addmessage(string.format('%s: %s %s', user, newVal, name)) + elseif itemType == 'buffer' then + if newVal > prevVal then + gui.addmessage(string.format('%s: %s %s', user, newVal, name)) + end else gui.addmessage(string.format('Unknown item ram type %s', itemType)) end From acf90e5e40b1b97050c455d6c706071031cc393d Mon Sep 17 00:00:00 2001 From: Samuel Flynn Date: Sat, 4 Apr 2020 15:49:44 -0400 Subject: [PATCH 06/14] Several bug fixes and added sync (color dungeon stuff, map exploration) --- .../ramcontroller/Links Awakening.lua | 180 +++++++++++------- 1 file changed, 108 insertions(+), 72 deletions(-) diff --git a/bizhawk-co-op/ramcontroller/Links Awakening.lua b/bizhawk-co-op/ramcontroller/Links Awakening.lua index 5e60e48..ea5730b 100644 --- a/bizhawk-co-op/ramcontroller/Links Awakening.lua +++ b/bizhawk-co-op/ramcontroller/Links Awakening.lua @@ -47,6 +47,8 @@ local inventorySlotInfos = { --Order is important, since we want to add items to {address = 0xDB0B, name = 'Inv 10'}, } +local MAP_FLAGS_BASE_ADDR = 0xD800 + local gameStateAddr = 0xDB95 -- Source https://github.com/zladx/LADX-Disassembly/blob/4ae748bd354f94ed2887f04d4014350d5a103763/src/constants/gameplay.asm#L22-L48 local gameStateVals = { -- Only states where we can do events are listed @@ -152,35 +154,37 @@ function readRAM(address, size) end end -function giveInventoryItem(itemVal) +function healthToString(val) - local firstEmptySlotAddr = nil + local whole = math.floor(val / 8) + val = val - (whole * 8) - for _, slotInfo in ipairs(inventorySlotInfos) do - local slotAddr = slotInfo['address'] - local thisSlotsItem = readRAM(slotAddr, 1) - if thisSlotsItem == itemVal then - return -- We already have this item - end - if thisSlotsItem == NO_ITEM_VALUE and not firstEmptySlotAddr then - firstEmptySlotAddr = slotAddr + local part = nil + if val > 0 then + if val % 4 == 0 then + part = (val / 4) .. '/2' + elseif val % 2 == 0 then + part = (val / 2) .. '/4' + else + part = val .. '/8' end end - if not firstEmptySlotAddr then - console.log(string.format('ERROR: Attempt to award item %s, but all inventory slots are full!', inventoryItemVals[itemVal])) - return - end - - - if config.ramconfig.verbose then - printOutput(string.format('About to write item val %s (%s) to addr %s', asString(itemVal), asString(inventoryItemVals[itemVal]), asString(firstEmptySlotAddr))) + if whole > 0 and part then + return whole .. ' ' .. part + elseif whole > 0 then + return whole + else + return part end +end - writeRAM(firstEmptySlotAddr, 1, itemVal) +function reverseU16Int(value) + return ((value % 256) * 256) + math.floor(value / 256) end local ramItemAddrs = { + [0xCFF2] = {name = 'Overworld Sword', type = 'num'}, [0xDB0C] = {name = 'Flippers', type = 'bool'}, [0xDB0D] = {name = 'Potion', type = 'bool'}, [0xDB0E] = {name = 'Trading Item', type = 'num', maxVal = MAX_TRADING_ITEM}, @@ -234,16 +238,11 @@ local ramItemAddrs = { [0xDB44] = {name = 'Shield level', type = 'num', maxVal = MAX_SHIELD_LEVEL}, [0xDB45] = {name = 'Number of arrows', type = 'num', flag = 'ammo'}, [0xDB49] = {name = { - [0] = 'unknown song', - [1] = 'unknown song', - [2] = 'unknown song', - [3] = 'unknown song', - [4] = 'unknown song', - [5] = 'Ballad of the Wind Fish', - [6] = 'Manbo Mambo', - [7] = 'Frog\'s Song of Soul', + [0] = 'Frog\'s Song of Soul', + [1] = 'Manbo Mambo', + [2] = 'Ballad of the Wind Fish' }, type = 'bitmask'}, - [0xDB4A] = {name = 'Ocarina selected song', type = 'num'}, + -- [0xDB4A] = {name = 'Ocarina selected song', type = 'num'}, Add only for inventory insanity [0xDB4B] = {name = 'Toadstool', type = 'bool'}, [0xDB4C] = {name = 'Magic powder quantity', type = 'num', flag = 'ammo'}, [0xDB4D] = {name = 'Number of bombs', type = 'num', flag = 'ammo'}, @@ -269,41 +268,27 @@ local ramItemAddrs = { -- Picking up rupees/health adds to the "add" buffers. Paying money/taking damage adds to the "subtract" buffers. -- The game subtracts from these buffers over time, applying their effect to your money/health totals -- Only additions to buffer values should be transmitted - [0xDB8F] = {name = 'Rupees Added', type = 'buffer', flag = 'rupees', size = 2}, - [0xDB91] = {name = 'Rupees Spent', type = 'buffer', flag = 'rupees', size = 2}, - [0xDB93] = {name = 'Health Added', type = 'buffer', flag = 'health'}, - [0xDB94] = {name = 'Health Lost', type = 'buffer', flag = 'health'}, - [0xDC04] = {name = 'Tunic Color', type = 'num'}, + [0xDB8F] = {name = 'Rupees Added', type = 'buffer', flag = 'rupees', size = 2, displayFunc = function(user, val) return string.format("%s found %s rupees", user, val) end }, + [0xDB91] = {name = 'Rupees Spent', type = 'buffer', flag = 'rupees', size = 2, displayFunc = function(user, val) return string.format("%s spent %s rupees", user, val) end }, + [0xDB93] = {name = 'Health Added', type = 'buffer', flag = 'health', displayFunc = function(user, val) return string.format("%s got %s hearts of health", user, healthToString(val)) end }, + [0xDB94] = {name = 'Health Lost', type = 'buffer', flag = 'health', displayFunc = function(user, val) return string.format("%s lost %s hearts of health", user, healthToString(val)) end }, + [0xDBCC] = {name = 'Color Dungeon Map', type = 'bool'}, + [0xDBCD] = {name = 'Color Dungeon Compass', type = 'bool'}, + [0xDBCE] = {name = 'Color Dungeon Owl\'s Beak', type = 'bool'}, + [0xDBCF] = {name = 'Color Dungeon Nightmare Key', type = 'bool'}, + [0xDBD0] = {name = 'Color Dungeon Small Keys', type = 'num'}, + [0xDC0F] = {name = 'Tunic Color', type = 'num'}, } for _, slotInfo in pairs(inventorySlotInfos) do ramItemAddrs[slotInfo['address']] = {name = slotInfo['name'], type = 'Inventory Slot'} end - -function promoteItem(list, newItem) -- TODO - local index - if (list[newItem] == nil) then - index = math.huge - else - index = list[newItem] - end - - local count = 0 - for item,val in pairs(list) do - count = count + 1 - if (val < index) then - list[item] = val + 1 - end - end - - list[newItem] = 0 - - if index == math.huge then - return count - else - return index - end +-- Add each map tile's explored flag to the sync locations +for mapTileIndex=0,256 do + ramItemAddrs[MAP_FLAGS_BASE_ADDR + mapTileIndex] = {name = { + [7] = 'Map tile explored' + }, type = 'bitmask', silent = true} end @@ -311,7 +296,8 @@ end function getGUImessage(address, prevVal, newVal, user) -- Only display the message if there is a name for the address local name = ramItemAddrs[address].name - if name and prevVal ~= newVal then + local silent = ramItemAddrs[address].silent + if name and not silent and prevVal ~= newVal then local itemType = ramItemAddrs[address].type @@ -338,11 +324,13 @@ function getGUImessage(address, prevVal, newVal, user) -- If bitflag, show each bit: the indexed name or bit index as a boolean elseif itemType == 'bitmask' then for b=0,7 do - local newBit = bit.check(newVal, b) - local prevBit = bit.check(prevVal, b) + if ramItemAddrs[address].name[b] then + local newBit = bit.check(newVal, b) + local prevBit = bit.check(prevVal, b) - if (newBit ~= prevBit) then - gui.addmessage(string.format('%s: %s %s', user, (newBit and 'Added' or ' Removed'), name[b])) + if (newBit ~= prevBit) then + gui.addmessage(string.format('%s: %s %s', user, (newBit and 'Added' or ' Removed'), name[b])) + end end end @@ -351,14 +339,47 @@ function getGUImessage(address, prevVal, newVal, user) gui.addmessage(string.format('%s: Found %s', user, inventoryItemVals[newVal])) elseif itemType == 'buffer' then if newVal > prevVal then - gui.addmessage(string.format('%s: %s %s', user, newVal, name)) + gui.addmessage(ramItemAddrs[address].displayFunc(user, newVal - prevVal)) end - else + else gui.addmessage(string.format('Unknown item ram type %s', itemType)) end end end +function giveInventoryItem(itemVal, prevRAM, their_user) + + local firstEmptySlotAddr = nil + + for _, slotInfo in ipairs(inventorySlotInfos) do + local slotAddr = slotInfo['address'] + local thisSlotsItem = readRAM(slotAddr, 1) + if thisSlotsItem == itemVal then + return -- We already have this item. Just ignore this request. + end + if thisSlotsItem == NO_ITEM_VALUE and not firstEmptySlotAddr then + firstEmptySlotAddr = slotAddr + end + end + + if not firstEmptySlotAddr then + console.log(string.format('ERROR: Attempt to award item %s, but all inventory slots are full!', inventoryItemVals[itemVal])) + return + end + + + if config.ramconfig.verbose then + printOutput(string.format('About to write item val %s (%s) to addr %s', asString(itemVal), asString(inventoryItemVals[itemVal]), asString(firstEmptySlotAddr))) + end + + writeRAM(firstEmptySlotAddr, 1, itemVal) + + getGUImessage(firstEmptySlotAddr, prevRAM[address], itemVal, their_user) + + -- Write the itemVal to the previous memory state so that the update check doesn't think we found this item + prevRAM[firstEmptySlotAddr] = itemVal +end + -- Reset this script's record of your possessed items to what's currently in memory, ignoring any previous state -- Used when entering into a playable state, such as when loading a save function getPossessedItemsTable(itemsState) @@ -412,6 +433,9 @@ function getTransmittableItemsState() end local ramval = readRAM(address, item.size) + if item.flag == 'rupees' then + ramval = reverseU16Int(ramval) + end transmittableTable[address] = ramval end @@ -437,27 +461,31 @@ function getItemStateChanges(prevState, newState) if config.ramconfig.verbose then printOutput(string.format('Updating address [%s] to value [%s].', asString(address), asString(val))) end - getGUImessage(address, prevVal, val, config.user) -- If boolean, get T/F if itemType == 'bool' then + getGUImessage(address, prevVal, val, config.user) ramevents[address] = (val ~= 0) changes = true -- If numeric, get value elseif itemType == 'num' then + getGUImessage(address, prevVal, val, config.user) ramevents[address] = val changes = true -- If bitmask, get the changed bits elseif itemType == 'bitmask' then + getGUImessage(address, prevVal, val, config.user) local changedBits = {} for b=0,7 do - local newBit = bit.check(val, b) - local prevBit = bit.check(prevVal, b) + if ramItemAddrs[address].name[b] then + local newBit = bit.check(val, b) + local prevBit = bit.check(prevVal, b) - if (newBit ~= prevBit) then - changedBits[b] = newBit + if (newBit ~= prevBit) then + changedBits[b] = newBit + end end end ramevents[address] = changedBits @@ -465,6 +493,7 @@ function getItemStateChanges(prevState, newState) -- Only transmit buffer increases elseif itemType == 'buffer' then + getGUImessage(address, prevVal, val, config.user) if val > prevVal then ramevents[address] = val - prevVal changes = true @@ -489,9 +518,12 @@ function getItemStateChanges(prevState, newState) if config.ramconfig.verbose then printOutput(string.format('Discovered that item [%s] is newly possessed.', itemVal)) + printOutput(string.format('Previous possessed table: %s', asString(prevPossessedItems))) + printOutput(string.format('New possessed table: %s', asString(newPossessedItems))) end changes = true table.insert(listOfNewlyAcquiredItemVals, itemVal) + getGUImessage(B_SLOT_ADDR, NO_ITEM_VALUE, itemVal, config.user) end end @@ -520,7 +552,7 @@ function applyItemStateChanges(prevRAM, their_user, newEvents) if config.ramconfig.verbose then printOutput(string.format('About to award item: %s', asString(inventoryItemVals[itemVal]))) end - giveInventoryItem(itemVal) + giveInventoryItem(itemVal, prevRAM, their_user) end end newEvents[NEW_INV_ITEMS_KEY] = nil @@ -547,7 +579,7 @@ function applyItemStateChanges(prevRAM, their_user, newEvents) end -- If bitflag update each bit - elseif itemType == 'bit' then + elseif itemType == 'bitmask' then newval = prevRAM[address] for b, bitval in pairs(val) do if bitval then @@ -570,7 +602,11 @@ function applyItemStateChanges(prevRAM, their_user, newEvents) prevRAM[address] = newval local gameLoaded = isGameLoadedWithFetch() if gameLoaded then - writeRAM(address, ramItemAddrs[address].size, newval) + local valToWrite = newval + if ramItemAddrs[address].flags == 'rupees' then + valToWrite = reverseU16Int(valToWrite) + end + writeRAM(address, ramItemAddrs[address].size, valToWrite) end end return prevRAM From 5c416aef847acab40c7a2848cebc1265a0ed788e Mon Sep 17 00:00:00 2001 From: Samuel Flynn Date: Sat, 4 Apr 2020 23:17:56 -0400 Subject: [PATCH 07/14] Rupee sync fix and big fairy healing --- .../ramcontroller/Links Awakening.lua | 104 ++++++++++++++++-- 1 file changed, 92 insertions(+), 12 deletions(-) diff --git a/bizhawk-co-op/ramcontroller/Links Awakening.lua b/bizhawk-co-op/ramcontroller/Links Awakening.lua index ea5730b..8796d54 100644 --- a/bizhawk-co-op/ramcontroller/Links Awakening.lua +++ b/bizhawk-co-op/ramcontroller/Links Awakening.lua @@ -64,13 +64,26 @@ local menuStateVals = { [0xFF] = {desc = 'Death/Save+Quit Menu', transmitEvents = false}, } -function isGameLoaded(gameStateVal) - return gameStateVals[gameStateVal] ~= nil -end +-- There are 16 non-player entities that can be tracked on screen at once +local ENTITY_TABLE_LENGTH = 0x10 -function isGameLoadedWithFetch() -- Grr. Why doesn't lua support function overloading?? - return isGameLoaded(readRAM(gameStateAddr)) -end +local ENTITY_STATUS_TABLE_START_ADDR = 0xC280 +local ENTITY_STATUS_ACTIVE_VAL = 0x05 + +-- Entity state. Each entity has its own meaning for each value +local ENTITY_STATE_1_TABLE_START_ADDR = 0xC290 +local ENTITY_STATE_1_BIG_FAIRY_INTERACTING_VAL = 0x01 + +local ENTITY_STATE_2_TABLE_STRT_ADDR = 0xC2B0 +local ENTITY_STATE_2_BIG_FAIRY_HEAL_START_COUNTER_VAL = 0x04 + +local ENTITY_TYPE_TABLE_START_ADDR = 0xC3A0 +local ENTITY_TYPE_BIG_FAIRY_VAL = 0x84 + +local BIG_FAIRY_HEALING_KEY = 'Healing from big fairy' + +local ADD_HEALTH_BUFFER_ADDR = 0xDB93 +local BIG_FAIRY_HEALING_BUFFER_VAL = 0x04 function tableCount(table) local count = 0 @@ -106,11 +119,11 @@ local prevRAM = nil local gameLoaded = false local prevGameLoaded = false -local dying = false -local prevmode = 0 +local prevBigFairyHealing = false + +-- keys: string of player name, value: true/false if they were previously fairy healing +local prevRemotePlayerBigFairyHealing = {} local ramController = {} -local playercount = 1 -local possessedInventoryItems = {} -- Writes value to RAM using little endian function writeRAM(address, size, value) @@ -154,6 +167,14 @@ function readRAM(address, size) end end +function isGameLoaded(gameStateVal) + return gameStateVals[gameStateVal] ~= nil +end + +function isGameLoadedWithFetch() -- Grr. Why doesn't lua support function overloading?? + return isGameLoaded(readRAM(gameStateAddr)) +end + function healthToString(val) local whole = math.floor(val / 8) @@ -270,7 +291,7 @@ local ramItemAddrs = { -- Only additions to buffer values should be transmitted [0xDB8F] = {name = 'Rupees Added', type = 'buffer', flag = 'rupees', size = 2, displayFunc = function(user, val) return string.format("%s found %s rupees", user, val) end }, [0xDB91] = {name = 'Rupees Spent', type = 'buffer', flag = 'rupees', size = 2, displayFunc = function(user, val) return string.format("%s spent %s rupees", user, val) end }, - [0xDB93] = {name = 'Health Added', type = 'buffer', flag = 'health', displayFunc = function(user, val) return string.format("%s got %s hearts of health", user, healthToString(val)) end }, + [ADD_HEALTH_BUFFER_ADDR] = {name = 'Health Added', type = 'buffer', flag = 'health', displayFunc = function(user, val) return string.format("%s got %s hearts of health", user, healthToString(val)) end }, [0xDB94] = {name = 'Health Lost', type = 'buffer', flag = 'health', displayFunc = function(user, val) return string.format("%s lost %s hearts of health", user, healthToString(val)) end }, [0xDBCC] = {name = 'Color Dungeon Map', type = 'bool'}, [0xDBCD] = {name = 'Color Dungeon Compass', type = 'bool'}, @@ -444,12 +465,52 @@ function getTransmittableItemsState() return transmittableTable end +function isBigFairyHealing() + + for index = 0,(ENTITY_TABLE_LENGTH - 1) do + + local entityType = readRAM(ENTITY_TYPE_TABLE_START_ADDR + index, 1) + if entityType == ENTITY_TYPE_BIG_FAIRY_VAL then + + local bigFairyStatus = readRAM(ENTITY_STATUS_TABLE_START_ADDR + index, 1) + local bigFairyInteractingState = readRAM(ENTITY_STATE_1_TABLE_START_ADDR + index, 1) + local bigFairyHealCounter = readRAM(ENTITY_STATE_2_TABLE_STRT_ADDR + index, 1) + + if bigFairyStatus == ENTITY_STATUS_ACTIVE_VAL and + bigFairyInteractingState == ENTITY_STATE_1_BIG_FAIRY_INTERACTING_VAL and + bigFairyHealCounter >= ENTITY_STATE_2_BIG_FAIRY_HEAL_START_COUNTER_VAL then + + return true + end + end + end + + return false +end + -- Get a list of changed ram events function getItemStateChanges(prevState, newState) local ramevents = {} local changes = false + -- Big fairy healing needs a special event type. + -- For health buffers, we rely on increases to the buffer. The big fairy sets it to 4 and holds it there over time, + -- meaning only 4 (1/2 heart) gets transmitted, but the buffer drains/resets before we can check for events, + -- so it only gets set once. + if isBigFairyHealing() then + ramevents[BIG_FAIRY_HEALING_KEY] = true + if not prevBigFairyHealing then + gui.addmessage(string.format('%s: Started healing at a Great Fairy', config.user)) + end + prevBigFairyHealing = true + changes = true + -- Suppress the normal add buffer event for big fairy healing + prevState[ADD_HEALTH_BUFFER_ADDR] = BIG_FAIRY_HEALING_BUFFER_VAL + else + prevBigFairyHealing = false + end + for address, val in pairs(newState) do local prevVal = prevState[address] @@ -557,6 +618,18 @@ function applyItemStateChanges(prevRAM, their_user, newEvents) end newEvents[NEW_INV_ITEMS_KEY] = nil + if newEvents[BIG_FAIRY_HEALING_KEY] then + prevRAM[ADD_HEALTH_BUFFER_ADDR] = BIG_FAIRY_HEALING_BUFFER_VAL + writeRAM(ADD_HEALTH_BUFFER_ADDR, ramItemAddrs[ADD_HEALTH_BUFFER_ADDR].size, BIG_FAIRY_HEALING_BUFFER_VAL) + if not prevRemotePlayerBigFairyHealing[their_user] then + gui.addmessage(string.format('%s: Started healing at a Great Fairy', their_user)) + end + prevRemotePlayerBigFairyHealing[their_user] = true + else + prevRemotePlayerBigFairyHealing[their_user] = false + end + newEvents[BIG_FAIRY_HEALING_KEY] = nil + for address, val in pairs(newEvents) do local itemType = ramItemAddrs[address].type @@ -603,8 +676,15 @@ function applyItemStateChanges(prevRAM, their_user, newEvents) local gameLoaded = isGameLoadedWithFetch() if gameLoaded then local valToWrite = newval - if ramItemAddrs[address].flags == 'rupees' then + if ramItemAddrs[address].flag == 'rupees' then + + if config.ramconfig.verbose then + printOutput(string.format('About to reverse rupees: %s', asString(valToWrite))) + end valToWrite = reverseU16Int(valToWrite) + if config.ramconfig.verbose then + printOutput(string.format('About to write rupees: %s', asString(valToWrite))) + end end writeRAM(address, ramItemAddrs[address].size, valToWrite) end From f292afbe1735603c7f4ccf3e3dedac14fc0c92e2 Mon Sep 17 00:00:00 2001 From: Samuel Flynn Date: Sun, 5 Apr 2020 16:28:15 -0400 Subject: [PATCH 08/14] Fix to color dungeon flag memory addresses and one more readme note --- README.md | 2 ++ bizhawk-co-op/ramcontroller/Links Awakening.lua | 10 +++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0664413..7afabb3 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,8 @@ Additional Options: * **Ammo** Players' power, bomb, and arrow counts will all be synchronized * **Health** Any health pickups or damage taken will be applied to all players * **Rupees** Any rupees found or spend will be applied to all players +* **Messages** Default/Verbose. Default is fine for most players. Verbose adds extra logging to the status window for debugging purposes. + ## Setup There are two different methods to install. diff --git a/bizhawk-co-op/ramcontroller/Links Awakening.lua b/bizhawk-co-op/ramcontroller/Links Awakening.lua index 8796d54..746b063 100644 --- a/bizhawk-co-op/ramcontroller/Links Awakening.lua +++ b/bizhawk-co-op/ramcontroller/Links Awakening.lua @@ -293,12 +293,12 @@ local ramItemAddrs = { [0xDB91] = {name = 'Rupees Spent', type = 'buffer', flag = 'rupees', size = 2, displayFunc = function(user, val) return string.format("%s spent %s rupees", user, val) end }, [ADD_HEALTH_BUFFER_ADDR] = {name = 'Health Added', type = 'buffer', flag = 'health', displayFunc = function(user, val) return string.format("%s got %s hearts of health", user, healthToString(val)) end }, [0xDB94] = {name = 'Health Lost', type = 'buffer', flag = 'health', displayFunc = function(user, val) return string.format("%s lost %s hearts of health", user, healthToString(val)) end }, - [0xDBCC] = {name = 'Color Dungeon Map', type = 'bool'}, - [0xDBCD] = {name = 'Color Dungeon Compass', type = 'bool'}, - [0xDBCE] = {name = 'Color Dungeon Owl\'s Beak', type = 'bool'}, - [0xDBCF] = {name = 'Color Dungeon Nightmare Key', type = 'bool'}, - [0xDBD0] = {name = 'Color Dungeon Small Keys', type = 'num'}, [0xDC0F] = {name = 'Tunic Color', type = 'num'}, + [0xDDDA] = {name = 'Color Dungeon Map', type = 'bool'}, + [0xDDDB] = {name = 'Color Dungeon Compass', type = 'bool'}, + [0xDDDC] = {name = 'Color Dungeon Owl\'s Beak', type = 'bool'}, + [0xDDDD] = {name = 'Color Dungeon Nightmare Key', type = 'bool'}, + [0xDDDE] = {name = 'Color Dungeon Small Keys', type = 'num'}, } for _, slotInfo in pairs(inventorySlotInfos) do From 8d3b64c83e624340977391c50a27371aa18fa6a3 Mon Sep 17 00:00:00 2001 From: Samuel Flynn Date: Wed, 8 Apr 2020 21:56:14 -0400 Subject: [PATCH 09/14] Beginnings of boomerang sync --- .../ramcontroller/Links Awakening.lua | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/bizhawk-co-op/ramcontroller/Links Awakening.lua b/bizhawk-co-op/ramcontroller/Links Awakening.lua index 746b063..5829952 100644 --- a/bizhawk-co-op/ramcontroller/Links Awakening.lua +++ b/bizhawk-co-op/ramcontroller/Links Awakening.lua @@ -571,7 +571,7 @@ function getItemStateChanges(prevState, newState) local prevPossessedItems = getPossessedItemsTable(prevState) local newPossessedItems = getPossessedItemsTable(newState) - local listOfNewlyAcquiredItemVals = {} + local invItemChanges = {} for itemVal, isPrevPossessed in pairs(prevPossessedItems) do local isNewPossessed = newPossessedItems[itemVal] @@ -583,13 +583,23 @@ function getItemStateChanges(prevState, newState) printOutput(string.format('New possessed table: %s', asString(newPossessedItems))) end changes = true - table.insert(listOfNewlyAcquiredItemVals, itemVal) + table.insert(invItemChanges, { [itemVal] = 'Added' }) getGUImessage(B_SLOT_ADDR, NO_ITEM_VALUE, itemVal, config.user) end + if isPrevPossessed and not isNewPossessed then + if config.ramconfig.verbose then + printOutput(string.format('Discovered that item [%s] was possessed, but has been lost (boomerang trade).', itemVal)) + printOutput(string.format('Previous possessed table: %s', asString(prevPossessedItems))) + printOutput(string.format('New possessed table: %s', asString(newPossessedItems))) + end + changes = true + table.insert(invItemChanges, { [itemVal] = 'Removed' }) + getGUImessage(B_SLOT_ADDR, itemVal, NO_ITEM_VALUE, config.user) + end end - if table.getn(listOfNewlyAcquiredItemVals) > 0 then - ramevents[NEW_INV_ITEMS_KEY] = listOfNewlyAcquiredItemVals + if table.getn(invItemChanges) > 0 then + ramevents[NEW_INV_ITEMS_KEY] = invItemChanges end if (changes) then @@ -607,11 +617,12 @@ end function applyItemStateChanges(prevRAM, their_user, newEvents) -- First, handle the newly acquired inventory items - local listOfNewlyAcquiredItemVals = newEvents[NEW_INV_ITEMS_KEY] - if listOfNewlyAcquiredItemVals then - for _,itemVal in ipairs(listOfNewlyAcquiredItemVals) do + local invItemChanges = newEvents[NEW_INV_ITEMS_KEY] + if invItemChanges then + for _,invItemEvent in ipairs(invItemChanges) do + for --TODO start here if config.ramconfig.verbose then - printOutput(string.format('About to award item: %s', asString(inventoryItemVals[itemVal]))) + printOutput(string.format('From %s: Item: %s was %s', asString(inventoryItemVals[itemVal]), their_user, invItemEvent)) end giveInventoryItem(itemVal, prevRAM, their_user) end From 3f9adc3876b24c23bc9e9ce747920960b5a0b39d Mon Sep 17 00:00:00 2001 From: Samuel Flynn Date: Thu, 9 Apr 2020 23:27:12 -0400 Subject: [PATCH 10/14] Finished naive method for boomerang. Need to add sanity checks --- .../ramcontroller/Links Awakening.lua | 40 +++++++++++++++---- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/bizhawk-co-op/ramcontroller/Links Awakening.lua b/bizhawk-co-op/ramcontroller/Links Awakening.lua index 5829952..7179aac 100644 --- a/bizhawk-co-op/ramcontroller/Links Awakening.lua +++ b/bizhawk-co-op/ramcontroller/Links Awakening.lua @@ -401,6 +401,18 @@ function giveInventoryItem(itemVal, prevRAM, their_user) prevRAM[firstEmptySlotAddr] = itemVal end +function removeInventoryItem(itemVal, prevRAM, their_user) + + for _, slotInfo in ipairs(inventorySlotInfos) do + local slotAddr = slotInfo['address'] + local thisSlotsItem = readRAM(slotAddr, 1) + if thisSlotsItem == itemVal then + writeRAM(slotAddr, 1, NO_ITEM_VALUE) + return + end + end +end + -- Reset this script's record of your possessed items to what's currently in memory, ignoring any previous state -- Used when entering into a playable state, such as when loading a save function getPossessedItemsTable(itemsState) @@ -583,7 +595,7 @@ function getItemStateChanges(prevState, newState) printOutput(string.format('New possessed table: %s', asString(newPossessedItems))) end changes = true - table.insert(invItemChanges, { [itemVal] = 'Added' }) + invItemChanges[itemVal] = 'Added' getGUImessage(B_SLOT_ADDR, NO_ITEM_VALUE, itemVal, config.user) end if isPrevPossessed and not isNewPossessed then @@ -593,12 +605,12 @@ function getItemStateChanges(prevState, newState) printOutput(string.format('New possessed table: %s', asString(newPossessedItems))) end changes = true - table.insert(invItemChanges, { [itemVal] = 'Removed' }) + invItemChanges[itemVal] = 'Removed' getGUImessage(B_SLOT_ADDR, itemVal, NO_ITEM_VALUE, config.user) end end - if table.getn(invItemChanges) > 0 then + if tableCount(invItemChanges) > 0 then ramevents[NEW_INV_ITEMS_KEY] = invItemChanges end @@ -618,17 +630,31 @@ function applyItemStateChanges(prevRAM, their_user, newEvents) -- First, handle the newly acquired inventory items local invItemChanges = newEvents[NEW_INV_ITEMS_KEY] + local itemRemoves = {} + local itemAdds = {} if invItemChanges then - for _,invItemEvent in ipairs(invItemChanges) do - for --TODO start here + for itemVal, eventType in pairs(invItemChanges) do if config.ramconfig.verbose then - printOutput(string.format('From %s: Item: %s was %s', asString(inventoryItemVals[itemVal]), their_user, invItemEvent)) + printOutput(string.format('From %s: Item: %s was %s', their_user, asString(inventoryItemVals[itemVal]), eventType)) + end + if eventType == 'Added' then + table.insert(itemAdds, itemVal) + + elseif eventType == 'Removed' then + table.insert(itemRemoves, itemVal) end - giveInventoryItem(itemVal, prevRAM, their_user) end end newEvents[NEW_INV_ITEMS_KEY] = nil + for _,itemVal in pairs(itemRemoves) do + removeInventoryItem(itemVal, prevRAM, their_user) + end + + for _,itemVal in pairs(itemAdds) do + giveInventoryItem(itemVal, prevRAM, their_user) + end + if newEvents[BIG_FAIRY_HEALING_KEY] then prevRAM[ADD_HEALTH_BUFFER_ADDR] = BIG_FAIRY_HEALING_BUFFER_VAL writeRAM(ADD_HEALTH_BUFFER_ADDR, ramItemAddrs[ADD_HEALTH_BUFFER_ADDR].size, BIG_FAIRY_HEALING_BUFFER_VAL) From f3f2da30dd9fb45a8dd147ec490b03ea0a082829 Mon Sep 17 00:00:00 2001 From: Samuel Flynn Date: Fri, 10 Apr 2020 14:24:03 -0400 Subject: [PATCH 11/14] Added a slightly smarter boomerang swap --- .../ramcontroller/Links Awakening.lua | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/bizhawk-co-op/ramcontroller/Links Awakening.lua b/bizhawk-co-op/ramcontroller/Links Awakening.lua index 7179aac..28bf5ee 100644 --- a/bizhawk-co-op/ramcontroller/Links Awakening.lua +++ b/bizhawk-co-op/ramcontroller/Links Awakening.lua @@ -1,4 +1,5 @@ local NO_ITEM_VALUE = 0x00 +local BOOMERANG_ITEM_VALUE = 0x0D local MAX_NUM_HEART_CONTAINERS = 0x0E -- 14 local MAX_NUM_HEART_PIECES = 0x04 @@ -25,7 +26,7 @@ local inventoryItemVals = { [0x0A] = 'Feather', [0x0B] = 'Shovel', [0x0C] = 'Magic powder', - [0x0D] = 'Boomrang', + [BOOMERANG_ITEM_VALUE] = 'Boomrang', } local B_SLOT_ADDR = 0xDB00 @@ -630,8 +631,9 @@ function applyItemStateChanges(prevRAM, their_user, newEvents) -- First, handle the newly acquired inventory items local invItemChanges = newEvents[NEW_INV_ITEMS_KEY] - local itemRemoves = {} + local itemRemoved = nil local itemAdds = {} + local boomerangAdded = false if invItemChanges then for itemVal, eventType in pairs(invItemChanges) do if config.ramconfig.verbose then @@ -639,16 +641,23 @@ function applyItemStateChanges(prevRAM, their_user, newEvents) end if eventType == 'Added' then table.insert(itemAdds, itemVal) + if itemVal == BOOMERANG_ITEM_VALUE then + boomerangAdded = true + end + - elseif eventType == 'Removed' then - table.insert(itemRemoves, itemVal) + -- Only remove up to 1 item at a time, since it should only be from a boomerang swap + elseif eventType == 'Removed' and not itemRemoved then + itemRemoved = itemVal end end end newEvents[NEW_INV_ITEMS_KEY] = nil - for _,itemVal in pairs(itemRemoves) do - removeInventoryItem(itemVal, prevRAM, their_user) + -- Item removes should only be done to the boomerang or if the boomerang is received + if itemRemoved == BOOMERANG_ITEM_VALUE or boomerangAdded then + + removeInventoryItem(itemRemoved, prevRAM, their_user) end for _,itemVal in pairs(itemAdds) do From b28c8caaa7a89065d31b9b1b9223e75305b1dea1 Mon Sep 17 00:00:00 2001 From: Samuel Flynn Date: Sat, 11 Apr 2020 13:04:42 -0400 Subject: [PATCH 12/14] Added reconciliation process for missed messages --- .../ramcontroller/Links Awakening.lua | 400 ++++++++++++------ 1 file changed, 279 insertions(+), 121 deletions(-) diff --git a/bizhawk-co-op/ramcontroller/Links Awakening.lua b/bizhawk-co-op/ramcontroller/Links Awakening.lua index 28bf5ee..373c900 100644 --- a/bizhawk-co-op/ramcontroller/Links Awakening.lua +++ b/bizhawk-co-op/ramcontroller/Links Awakening.lua @@ -86,6 +86,15 @@ local BIG_FAIRY_HEALING_KEY = 'Healing from big fairy' local ADD_HEALTH_BUFFER_ADDR = 0xDB93 local BIG_FAIRY_HEALING_BUFFER_VAL = 0x04 +local RECONCILE_INCREASE_ONLY = 'increaseOnly' +local RECONCILE_INCREASE_ON_CONFLICT = 'increaseOnConflict' +local RECONCILE_DECREASE_ON_CONFLICT = 'decreaseOnConflict' +local RECONCILE_AVERAGE = 'average' +local RECONCILE_SPECIAL = 'specialReconcile' + +local RECONCILE_PERIOD_SECONDS = 5 +local RECONCILILIATION_MESSAGE_KEY = 'Total inventory reconciliation' + function tableCount(table) local count = 0 for _, _ in pairs(table) do @@ -125,6 +134,7 @@ local prevBigFairyHealing = false -- keys: string of player name, value: true/false if they were previously fairy healing local prevRemotePlayerBigFairyHealing = {} local ramController = {} +local lastReconcileTime = os.time() -- Writes value to RAM using little endian function writeRAM(address, size, value) @@ -205,112 +215,87 @@ function reverseU16Int(value) return ((value % 256) * 256) + math.floor(value / 256) end +local dungeonFlags = { + {name = 'Tail Cave', startingAddr = 0xDB16}, + {name = 'Bottle Grotto', startingAddr = 0xDB1B}, + {name = 'Key Cavern', startingAddr = 0xDB20}, + {name = 'Angler\'s Tunnel', startingAddr = 0xDB25}, + {name = 'Catfish\'s Maw', startingAddr = 0xDB2A}, + {name = 'Face Shrine', startingAddr = 0xDB2F}, + {name = 'Eagle\'s Tower', startingAddr = 0xDB34}, + {name = 'Turtle Rock', startingAddr = 0xDB39}, + {name = 'Color Dungeon', startingAddr = 0xDDDA}, +} + local ramItemAddrs = { - [0xCFF2] = {name = 'Overworld Sword', type = 'num'}, - [0xDB0C] = {name = 'Flippers', type = 'bool'}, - [0xDB0D] = {name = 'Potion', type = 'bool'}, - [0xDB0E] = {name = 'Trading Item', type = 'num', maxVal = MAX_TRADING_ITEM}, - [0xDB0F] = {name = 'Number of secret shells', type = 'num'}, - [0xDB11] = {name = 'Tail Key', type = 'bool'}, - [0xDB12] = {name = 'Angler Key', type = 'bool'}, - [0xDB13] = {name = 'Face Key', type = 'bool'}, - [0xDB14] = {name = 'Birdie Key', type = 'bool'}, - [0xDB15] = {name = 'Number of golden leaves', type = 'num', maxVal = MAX_GOLDEN_LEAVES}, - [0xDB16] = {name = 'Tail Cave Map', type = 'bool'}, - [0xDB17] = {name = 'Tail Cave Compass', type = 'bool'}, - [0xDB18] = {name = 'Tail Cave Owl\'s Beak', type = 'bool'}, - [0xDB19] = {name = 'Tail Cave Nightmare Key', type = 'bool'}, - [0xDB1A] = {name = 'Tail Cave Small Keys', type = 'num'}, - [0xDB1B] = {name = 'Bottle Grotto Map', type = 'bool'}, - [0xDB1C] = {name = 'Bottle Grotto Compass', type = 'bool'}, - [0xDB1D] = {name = 'Bottle Grotto Owl\'s Beak', type = 'bool'}, - [0xDB1E] = {name = 'Bottle Grotto Nightmare Key', type = 'bool'}, - [0xDB1F] = {name = 'Bottle Grotto Small Keys', type = 'num'}, - [0xDB20] = {name = 'Key Cavern Map', type = 'bool'}, - [0xDB21] = {name = 'Key Cavern Compass', type = 'bool'}, - [0xDB22] = {name = 'Key Cavern Owl\'s Beak', type = 'bool'}, - [0xDB23] = {name = 'Key Cavern Nightmare Key', type = 'bool'}, - [0xDB24] = {name = 'Key Cavern Small Keys', type = 'num'}, - [0xDB25] = {name = 'Angler\'s Tunnel Map', type = 'bool'}, - [0xDB26] = {name = 'Angler\'s Tunnel Compass', type = 'bool'}, - [0xDB27] = {name = 'Angler\'s Tunnel Owl\'s Beak', type = 'bool'}, - [0xDB28] = {name = 'Angler\'s Tunnel Nightmare Key', type = 'bool'}, - [0xDB29] = {name = 'Angler\'s Tunnel Small Keys', type = 'num'}, - [0xDB2A] = {name = 'Catfish\'s Maw Map', type = 'bool'}, - [0xDB2B] = {name = 'Catfish\'s Maw Compass', type = 'bool'}, - [0xDB2C] = {name = 'Catfish\'s Maw Owl\'s Beak', type = 'bool'}, - [0xDB2D] = {name = 'Catfish\'s Maw Nightmare Key', type = 'bool'}, - [0xDB2E] = {name = 'Catfish\'s Maw Small Keys', type = 'num'}, - [0xDB2F] = {name = 'Face Shrine Map', type = 'bool'}, - [0xDB30] = {name = 'Face Shrine Compass', type = 'bool'}, - [0xDB31] = {name = 'Face Shrine Owl\'s Beak', type = 'bool'}, - [0xDB32] = {name = 'Face Shrine Nightmare Key', type = 'bool'}, - [0xDB33] = {name = 'Face Shrine Small Keys', type = 'num'}, - [0xDB34] = {name = 'Eagle\'s Tower Map', type = 'bool'}, - [0xDB35] = {name = 'Eagle\'s Tower Compass', type = 'bool'}, - [0xDB36] = {name = 'Eagle\'s Tower Owl\'s Beak', type = 'bool'}, - [0xDB37] = {name = 'Eagle\'s Tower Nightmare Key', type = 'bool'}, - [0xDB38] = {name = 'Eagle\'s Tower Small Keys', type = 'num'}, - [0xDB39] = {name = 'Turtle Rock Map', type = 'bool'}, - [0xDB3A] = {name = 'Turtle Rock Compass', type = 'bool'}, - [0xDB3B] = {name = 'Turtle Rock Owl\'s Beak', type = 'bool'}, - [0xDB3C] = {name = 'Turtle Rock Nightmare Key', type = 'bool'}, - [0xDB3D] = {name = 'Turtle Rock Small Keys', type = 'num'}, - [0xDB43] = {name = 'Power bracelet level', type = 'num', maxVal = MAX_BRACELET_LEVEL}, - [0xDB44] = {name = 'Shield level', type = 'num', maxVal = MAX_SHIELD_LEVEL}, - [0xDB45] = {name = 'Number of arrows', type = 'num', flag = 'ammo'}, + [0xCFF2] = {name = 'Overworld Sword', type = 'num', reconcileBehavior = RECONCILE_INCREASE_ONLY}, + [0xDB0C] = {name = 'Flippers', type = 'bool', reconcileBehavior = RECONCILE_INCREASE_ONLY}, + [0xDB0D] = {name = 'Potion', type = 'bool', reconcileBehavior = RECONCILE_INCREASE_ON_CONFLICT}, + [0xDB0E] = {name = 'Trading Item', type = 'num', maxVal = MAX_TRADING_ITEM, reconcileBehavior = RECONCILE_INCREASE_ONLY}, + [0xDB0F] = {name = 'Number of secret shells', type = 'num', reconcileBehavior = RECONCILE_INCREASE_ONLY}, + [0xDB11] = {name = 'Tail Key', type = 'bool', reconcileBehavior = RECONCILE_INCREASE_ONLY}, + [0xDB12] = {name = 'Angler Key', type = 'bool', reconcileBehavior = RECONCILE_INCREASE_ONLY}, + [0xDB13] = {name = 'Face Key', type = 'bool', reconcileBehavior = RECONCILE_INCREASE_ONLY}, + [0xDB14] = {name = 'Birdie Key', type = 'bool', reconcileBehavior = RECONCILE_INCREASE_ONLY}, + [0xDB15] = {name = 'Number of golden leaves', type = 'num', maxVal = MAX_GOLDEN_LEAVES, reconcileBehavior = RECONCILE_INCREASE_ONLY}, + [0xDB43] = {name = 'Power bracelet level', type = 'num', maxVal = MAX_BRACELET_LEVEL, reconcileBehavior = RECONCILE_INCREASE_ONLY}, + [0xDB44] = {name = 'Shield level', type = 'num', maxVal = MAX_SHIELD_LEVEL, reconcileBehavior = RECONCILE_INCREASE_ONLY}, + [0xDB45] = {name = 'Number of arrows', type = 'num', flag = 'ammo', reconcileBehavior = RECONCILE_AVERAGE}, [0xDB49] = {name = { [0] = 'Frog\'s Song of Soul', [1] = 'Manbo Mambo', [2] = 'Ballad of the Wind Fish' - }, type = 'bitmask'}, + }, type = 'bitmask', reconcileBehavior = RECONCILE_INCREASE_ONLY}, -- [0xDB4A] = {name = 'Ocarina selected song', type = 'num'}, Add only for inventory insanity - [0xDB4B] = {name = 'Toadstool', type = 'bool'}, - [0xDB4C] = {name = 'Magic powder quantity', type = 'num', flag = 'ammo'}, - [0xDB4D] = {name = 'Number of bombs', type = 'num', flag = 'ammo'}, - [0xDB4E] = {name = 'Sword level', type = 'num', maxVal = MAX_SWORD_LEVEL}, + [0xDB4B] = {name = 'Toadstool', type = 'bool', reconcileBehavior = RECONCILE_DECREASE_ON_CONFLICT}, + [0xDB4C] = {name = 'Magic powder quantity', type = 'num', flag = 'ammo', reconcileBehavior = RECONCILE_AVERAGE}, + [0xDB4D] = {name = 'Number of bombs', type = 'num', flag = 'ammo', reconcileBehavior = RECONCILE_AVERAGE}, + [0xDB4E] = {name = 'Sword level', type = 'num', maxVal = MAX_SWORD_LEVEL, reconcileBehavior = RECONCILE_INCREASE_ONLY}, -- DB56-DB58 Number of times the character died for each save slot (one byte per save slot) - --[0xDB5A] = {name = 'Current health', type = 'num', flag = 'life'}, --Each increment of 08 is one full heart, each increment of 04 is one-half heart (Don't set this directly. Use the health buffers) - [0xDB5B] = {name = 'Maximum health', type = 'num', maxVal = MAX_NUM_HEART_CONTAINERS}, --Max recommended value is 0E (14 hearts) - [0xDB5C] = {name = 'Number of heart pieces', type = 'num', maxVal = MAX_NUM_HEART_PIECES}, - --[0xDB5D] = {name = 'Rupees', type = 'num', flag = 'money', size = 2}, --2 bytes, decimal value (Don't set this directly. Use the buffers) + [0xDB5A] = {name = 'Current health', type = 'reconcileOnly', flag = 'life', reconcileBehavior = RECONCILE_AVERAGE}, --Each increment of 08 is one full heart, each increment of 04 is one-half heart (Don't set this directly. Use the health buffers) + [0xDB5B] = {name = 'Maximum health', type = 'num', maxVal = MAX_NUM_HEART_CONTAINERS, reconcileBehavior = RECONCILE_INCREASE_ONLY}, --Max recommended value is 0E (14 hearts) + [0xDB5C] = {name = 'Number of heart pieces', type = 'num', maxVal = MAX_NUM_HEART_PIECES, reconcileBehavior = RECONCILE_AVERAGE}, + [0xDB5D] = {name = 'Rupees', type = 'reconcileOnly', flag = 'rupees', size = 2, reconcileBehavior = RECONCILE_AVERAGE}, --2 bytes, decimal value (Don't set this directly. Use the buffers) -- [0xDBAE] = {name = 'Dungeon map grid position', type = 'num'}, - [0xDB65] = {name = 'Tail Cave', type = 'num', instrumentName = 'Full Moon Cello'}, -- 00=starting state, 01=defeated miniboss, 02=???, 03=have instrument - [0xDB66] = {name = 'Bottle Grotto', type = 'num', instrumentName = 'Conch Horn'}, - [0xDB67] = {name = 'Key Cavern', type = 'num', instrumentName = 'Sea Lily\'s Bell'}, - [0xDB68] = {name = 'Angler\'s Tunnel', type = 'num', instrumentName = 'Surf Harp'}, - [0xDB69] = {name = 'Catfish\'s Maw', type = 'num', instrumentName = 'Wind Marimba'}, - [0xDB6A] = {name = 'Face Shrine', type = 'num', instrumentName = 'Coral Triangle'}, - [0xDB6B] = {name = 'Eagle\'s Tower', type = 'num', instrumentName = 'Organ of Evening Calm'}, - [0xDB6C] = {name = 'Turtle Rock', type = 'num', instrumentName = 'Thunder Drum'}, - [0xDB76] = {name = 'Max magic powder', type = 'num'}, - [0xDB77] = {name = 'Max bombs', type = 'num'}, - [0xDB78] = {name = 'Max arrows', type = 'num'}, + [0xDB65] = {name = 'Tail Cave', type = 'num', instrumentName = 'Full Moon Cello', reconcileBehavior = RECONCILE_INCREASE_ONLY}, -- 00=starting state, 01=defeated miniboss, 02=???, 03=have instrument + [0xDB66] = {name = 'Bottle Grotto', type = 'num', instrumentName = 'Conch Horn', reconcileBehavior = RECONCILE_INCREASE_ONLY}, + [0xDB67] = {name = 'Key Cavern', type = 'num', instrumentName = 'Sea Lily\'s Bell', reconcileBehavior = RECONCILE_INCREASE_ONLY}, + [0xDB68] = {name = 'Angler\'s Tunnel', type = 'num', instrumentName = 'Surf Harp', reconcileBehavior = RECONCILE_INCREASE_ONLY}, + [0xDB69] = {name = 'Catfish\'s Maw', type = 'num', instrumentName = 'Wind Marimba', reconcileBehavior = RECONCILE_INCREASE_ONLY}, + [0xDB6A] = {name = 'Face Shrine', type = 'num', instrumentName = 'Coral Triangle', reconcileBehavior = RECONCILE_INCREASE_ONLY}, + [0xDB6B] = {name = 'Eagle\'s Tower', type = 'num', instrumentName = 'Organ of Evening Calm', reconcileBehavior = RECONCILE_INCREASE_ONLY}, + [0xDB6C] = {name = 'Turtle Rock', type = 'num', instrumentName = 'Thunder Drum', reconcileBehavior = RECONCILE_INCREASE_ONLY}, + [0xDB76] = {name = 'Max magic powder', type = 'num', reconcileBehavior = RECONCILE_INCREASE_ONLY}, + [0xDB77] = {name = 'Max bombs', type = 'num', reconcileBehavior = RECONCILE_INCREASE_ONLY}, + [0xDB78] = {name = 'Max arrows', type = 'num', reconcileBehavior = RECONCILE_INCREASE_ONLY}, -- Buffers are rupee/health amounts that are to be added to your total over time. -- Picking up rupees/health adds to the "add" buffers. Paying money/taking damage adds to the "subtract" buffers. -- The game subtracts from these buffers over time, applying their effect to your money/health totals -- Only additions to buffer values should be transmitted - [0xDB8F] = {name = 'Rupees Added', type = 'buffer', flag = 'rupees', size = 2, displayFunc = function(user, val) return string.format("%s found %s rupees", user, val) end }, - [0xDB91] = {name = 'Rupees Spent', type = 'buffer', flag = 'rupees', size = 2, displayFunc = function(user, val) return string.format("%s spent %s rupees", user, val) end }, - [ADD_HEALTH_BUFFER_ADDR] = {name = 'Health Added', type = 'buffer', flag = 'health', displayFunc = function(user, val) return string.format("%s got %s hearts of health", user, healthToString(val)) end }, - [0xDB94] = {name = 'Health Lost', type = 'buffer', flag = 'health', displayFunc = function(user, val) return string.format("%s lost %s hearts of health", user, healthToString(val)) end }, - [0xDC0F] = {name = 'Tunic Color', type = 'num'}, - [0xDDDA] = {name = 'Color Dungeon Map', type = 'bool'}, - [0xDDDB] = {name = 'Color Dungeon Compass', type = 'bool'}, - [0xDDDC] = {name = 'Color Dungeon Owl\'s Beak', type = 'bool'}, - [0xDDDD] = {name = 'Color Dungeon Nightmare Key', type = 'bool'}, - [0xDDDE] = {name = 'Color Dungeon Small Keys', type = 'num'}, + [0xDB8F] = {name = 'Rupees Added', type = 'buffer', flag = 'rupees', size = 2, displayFunc = function(user, val) return string.format("%s found %s rupees", user, val) end , reconcileBehavior = RECONCILE_SPECIAL}, + [0xDB91] = {name = 'Rupees Spent', type = 'buffer', flag = 'rupees', size = 2, displayFunc = function(user, val) return string.format("%s spent %s rupees", user, val) end , reconcileBehavior = RECONCILE_SPECIAL}, + [ADD_HEALTH_BUFFER_ADDR] = {name = 'Health Added', type = 'buffer', flag = 'health', displayFunc = function(user, val) return string.format("%s got %s hearts of health", user, healthToString(val)) end , reconcileBehavior = RECONCILE_SPECIAL}, + [0xDB94] = {name = 'Health Lost', type = 'buffer', flag = 'health', displayFunc = function(user, val) return string.format("%s lost %s hearts of health", user, healthToString(val)) end , reconcileBehavior = RECONCILE_SPECIAL}, + [0xDC0F] = {name = 'Tunic Color', type = 'num', reconcileBehavior = RECONCILE_INCREASE_ON_CONFLICT}, } +for _, dungeonFlag in pairs(dungeonFlags) do + ramItemAddrs[dungeonFlag.startingAddr + 0] = {name = dungeonFlag.name .. ' Map', type = 'bool', reconcileBehavior = RECONCILE_INCREASE_ONLY} + ramItemAddrs[dungeonFlag.startingAddr + 1] = {name = dungeonFlag.name .. ' Compass', type = 'bool', reconcileBehavior = RECONCILE_INCREASE_ONLY} + ramItemAddrs[dungeonFlag.startingAddr + 2] = {name = dungeonFlag.name .. ' Owl\'s Beak', type = 'bool', reconcileBehavior = RECONCILE_INCREASE_ONLY} + ramItemAddrs[dungeonFlag.startingAddr + 3] = {name = dungeonFlag.name .. ' Nightmare Key', type = 'bool', reconcileBehavior = RECONCILE_INCREASE_ONLY} + ramItemAddrs[dungeonFlag.startingAddr + 4] = {name = dungeonFlag.name .. ' Small Keys', type = 'num', reconcileBehavior = RECONCILE_INCREASE_ON_CONFLICT} +end + for _, slotInfo in pairs(inventorySlotInfos) do - ramItemAddrs[slotInfo['address']] = {name = slotInfo['name'], type = 'Inventory Slot'} + ramItemAddrs[slotInfo['address']] = {name = slotInfo['name'], type = 'Inventory Slot', reconcileBehavior = RECONCILE_SPECIAL} end -- Add each map tile's explored flag to the sync locations for mapTileIndex=0,256 do ramItemAddrs[MAP_FLAGS_BASE_ADDR + mapTileIndex] = {name = { [7] = 'Map tile explored' - }, type = 'bitmask', silent = true} + }, type = 'bitmask', silent = true, reconcileBehavior = RECONCILE_INCREASE_ONLY} end @@ -363,13 +348,15 @@ function getGUImessage(address, prevVal, newVal, user) if newVal > prevVal then gui.addmessage(ramItemAddrs[address].displayFunc(user, newVal - prevVal)) end + elseif itemType == 'reconcileOnly' then + -- Do nothing. This type is only used in reconciliation messages else gui.addmessage(string.format('Unknown item ram type %s', itemType)) end end end -function giveInventoryItem(itemVal, prevRAM, their_user) +function giveInventoryItem(itemVal, prevRAM, theirUser) local firstEmptySlotAddr = nil @@ -396,13 +383,13 @@ function giveInventoryItem(itemVal, prevRAM, their_user) writeRAM(firstEmptySlotAddr, 1, itemVal) - getGUImessage(firstEmptySlotAddr, prevRAM[address], itemVal, their_user) + getGUImessage(firstEmptySlotAddr, prevRAM[address], itemVal, theirUser) -- Write the itemVal to the previous memory state so that the update check doesn't think we found this item prevRAM[firstEmptySlotAddr] = itemVal end -function removeInventoryItem(itemVal, prevRAM, their_user) +function removeInventoryItem(itemVal, prevRAM, theirUser) for _, slotInfo in ipairs(inventorySlotInfos) do local slotAddr = slotInfo['address'] @@ -501,6 +488,133 @@ function isBigFairyHealing() return false end +function applyReconciliationMessage(prevRAM, theirUser, theirRAM) + + local updatedValues = {} + + for address, theirVal in pairs(theirRAM) do + + local ourVal = prevRAM[address] + + if theirVal ~= ourVal then + + local itemInfo = ramItemAddrs[address] + + if itemInfo then + local itemName = itemInfo.name + local itemType = itemInfo.type + local reconcileBehavior = itemInfo.reconcileBehavior + local valToWrite = nil + + if reconcileBehavior == RECONCILE_INCREASE_ONLY or reconcileBehavior == RECONCILE_INCREASE_ON_CONFLICT then + if itemType == 'bitmask' then + local bitChanges = false + local ourBitmask = ourVal + for b=0,7 do + if ramItemAddrs[address].name[b] then + local ourBit = bit.check(ourVal, b) + local theirBit = bit.check(theirVal, b) + + if theirBit and not ourBit then + bit.set(ourBitmask, b) + bitChanges = true + end + end + end + if bitChanges then + valToWrite = ourBitmask + end + elseif theirVal > ourVal then + valToWrite = theirVal + end + + elseif reconcileBehavior == RECONCILE_DECREASE_ON_CONFLICT then + if itemType == 'bitmask' then + local bitChanges = false + local ourBitmask = ourVal + for b=0,7 do + if ramItemAddrs[address].name[b] then + local ourBit = bit.check(ourVal, b) + local theirBit = bit.check(theirVal, b) + + if ourBit and not theirBit then + bit.clear(ourBitmask, b) + bitChanges = true + end + end + end + if bitChanges then + valToWrite = ourBitmask + end + elseif theirVal < ourVal then + valToWrite = theirVal + end + + elseif reconcileBehavior == RECONCILE_AVERAGE then + valToWrite = math.ceil((theirVal + ourVal) / 2) + + elseif reconcileBehavior == RECONCILE_SPECIAL then + -- Do nothing for now. All specials are handled at the end. + else + error(string.format('Unknown reconciliation behavior %s for address %s (Our value: %s, their value %s', reconcileBehavior, address, ourVal, theirVal)) + end + + -- If any changes need to be applied, write them now + if valToWrite then + if ramItemAddrs[address].flag == 'rupees' then + valToWrite = reverseU16Int(valToWrite) + end + writeRAM(address, ramItemAddrs[address].size, valToWrite) + end + + else + error(string.format('Received reconciliation for unknown memory address %s. Their value: %s, our value: %s', asString(address), asString(theirVal), asString(ourVal))) + end -- if itemInfo + end -- if theirVal ~= ourVal + end -- for address, theirVal in pairs(theirRAM) + + -- Do item reconciliation + local theirItems = getPossessedItemsTable(theirRAM) + local ourItems = getPossessedItemsTable(prevRAM) + local itemsToBeAdded = {} + local itemsTheyDontHave = {} + local ourPosessedCount = 0 + local itemToBeRemoved = nil + + -- Get the list of items they have and we don't + for theirItemVal, theirPossessed in pairs(theirItems) do + local ourPossessed = ourItems[theirItemVal] + if theirPossessed and not ourPossessed then + table.insert(itemsToBeAdded, theirItemVal) + end + end + + -- Get the list of items we have that they don't + for ourItemVal, ourPossessed in pairs(ourItems) do + local theirPossessed = theirItems[ourItemVal] + if ourPossessed and not theirPossessed then + table.insert(itemsTheyDontHave, ourItemVal) + end + if ourPossessed then + ourPosessedCount = ourPosessedCount + 1 + end + end + + -- Check to see if we would add more inventory items than we'd have space for (boomerang conflict) + if ourPosessedCount + tableCount(itemsToBeAdded) > tableCount(inventorySlotInfos) then + table.sort(itemsTheyDontHave) + -- Remove the item with the largest value (boomerang, powder, shovel, etc.) + itemToBeRemoved = itemsTheyDontHave[tableCount(itemsTheyDontHave)] + end + if itemToBeRemoved then + removeInventoryItem(itemToBeRemoved, prevRAM, theirUser) + end + + for _,itemVal in pairs(itemsToBeAdded) do + giveInventoryItem(itemVal, prevRAM, theirUser) + end +end + -- Get a list of changed ram events function getItemStateChanges(prevState, newState) @@ -575,7 +689,9 @@ function getItemStateChanges(prevState, newState) elseif itemType == 'Inventory Slot' then -- Do nothing. We do a separate check for new inventory items below - else + elseif itemType == 'reconcileOnly' then + -- Do nothing. This type is only used in reconciliation messages + else console.log(string.format('Unknown item type [%s] for item %s (Address: %s)', itemType, ramItemAddrs[address].name, address)) end end @@ -627,7 +743,7 @@ end -- set a list of ram events -function applyItemStateChanges(prevRAM, their_user, newEvents) +function applyItemStateChanges(prevRAM, theirUser, newEvents) -- First, handle the newly acquired inventory items local invItemChanges = newEvents[NEW_INV_ITEMS_KEY] @@ -637,7 +753,7 @@ function applyItemStateChanges(prevRAM, their_user, newEvents) if invItemChanges then for itemVal, eventType in pairs(invItemChanges) do if config.ramconfig.verbose then - printOutput(string.format('From %s: Item: %s was %s', their_user, asString(inventoryItemVals[itemVal]), eventType)) + printOutput(string.format('From %s: Item: %s was %s', theirUser, asString(inventoryItemVals[itemVal]), eventType)) end if eventType == 'Added' then table.insert(itemAdds, itemVal) @@ -657,36 +773,55 @@ function applyItemStateChanges(prevRAM, their_user, newEvents) -- Item removes should only be done to the boomerang or if the boomerang is received if itemRemoved == BOOMERANG_ITEM_VALUE or boomerangAdded then - removeInventoryItem(itemRemoved, prevRAM, their_user) + removeInventoryItem(itemRemoved, prevRAM, theirUser) end for _,itemVal in pairs(itemAdds) do - giveInventoryItem(itemVal, prevRAM, their_user) + giveInventoryItem(itemVal, prevRAM, theirUser) end + -- If they are healing at a great fairy, deal with that if newEvents[BIG_FAIRY_HEALING_KEY] then prevRAM[ADD_HEALTH_BUFFER_ADDR] = BIG_FAIRY_HEALING_BUFFER_VAL writeRAM(ADD_HEALTH_BUFFER_ADDR, ramItemAddrs[ADD_HEALTH_BUFFER_ADDR].size, BIG_FAIRY_HEALING_BUFFER_VAL) - if not prevRemotePlayerBigFairyHealing[their_user] then - gui.addmessage(string.format('%s: Started healing at a Great Fairy', their_user)) + if not prevRemotePlayerBigFairyHealing[theirUser] then + gui.addmessage(string.format('%s: Started healing at a Great Fairy', theirUser)) end - prevRemotePlayerBigFairyHealing[their_user] = true + prevRemotePlayerBigFairyHealing[theirUser] = true else - prevRemotePlayerBigFairyHealing[their_user] = false + prevRemotePlayerBigFairyHealing[theirUser] = false end newEvents[BIG_FAIRY_HEALING_KEY] = nil + -- Save the reconciliation message for the very end + local reconciliationMessage = newEvents[RECONCILILIATION_MESSAGE_KEY] + newEvents[RECONCILILIATION_MESSAGE_KEY] = nil + + -- Do the bulk of address location applications for address, val in pairs(newEvents) do local itemType = ramItemAddrs[address].type - local newval + local itemName = ramItemAddrs[address].name + local increaseOnly = ramItemAddrs[address].reconcileBehavior == RECONCILE_INCREASE_ONLY + local prevval = prevRAM[address] + local newval = prevval if config.ramconfig.verbose then printOutput(string.format('Applying state change [%s=%s]', asString(address), asString(val))) end -- If boolean type value if itemType == 'bool' then - newval = (val and 1 or 0) -- Coercing booleans back to 1 or 0 numeric + if increaseOnly then + + if val then + newval = 1 + + elseif config.ramconfig.verbose then + printOutput(string.format('Refused to downgrade boolean %s from %s to %s', asString(itemName), asString(prevval), asString(val))) + end + else + newval = (val and 1 or 0) -- Coercing booleans back to 1 or 0 numeric + end -- If numeric type value elseif itemType == 'num' then @@ -694,7 +829,11 @@ function applyItemStateChanges(prevRAM, their_user, newEvents) if maxVal and val > maxVal then newval = maxVal else - newval = val + if increaseOnly and val > prevval then + newval = val + elseif config.ramconfig.verbose then + printOutput(string.format('Refused to downgrade number %s from %s to %s', asString(itemName), asString(prevval), asString(val))) + end end -- If bitflag update each bit @@ -704,37 +843,46 @@ function applyItemStateChanges(prevRAM, their_user, newEvents) if bitval then newval = bit.set(newval, b) else - newval = bit.clear(newval, b) + if not increaseOnly then + newval = bit.clear(newval, b) + + elseif config.ramconfig.verbose then + printOutput(string.format('Refused to downgrade bit %s from 1 to 0', asString(name[b]))) + end end end elseif itemType == 'buffer' then newval = prevRAM[address] + val - else + elseif itemType == 'reconcileOnly' then + -- Do nothing. This type is only used in reconciliation messages + else printOutput(string.format('Unknown item type [%s] for item %s (Address: %s)', itemType, ramItemAddrs[address].name, address)) newval = prevRAM[address] end -- Write the new value - getGUImessage(address, prevRAM[address], newval, their_user) + getGUImessage(address, prevRAM[address], newval, theirUser) prevRAM[address] = newval - local gameLoaded = isGameLoadedWithFetch() - if gameLoaded then - local valToWrite = newval - if ramItemAddrs[address].flag == 'rupees' then + local valToWrite = newval + if ramItemAddrs[address].flag == 'rupees' then - if config.ramconfig.verbose then - printOutput(string.format('About to reverse rupees: %s', asString(valToWrite))) - end - valToWrite = reverseU16Int(valToWrite) - if config.ramconfig.verbose then - printOutput(string.format('About to write rupees: %s', asString(valToWrite))) - end + if config.ramconfig.verbose then + printOutput(string.format('About to reverse rupees: %s', asString(valToWrite))) + end + valToWrite = reverseU16Int(valToWrite) + if config.ramconfig.verbose then + printOutput(string.format('About to write rupees: %s', asString(valToWrite))) end - writeRAM(address, ramItemAddrs[address].size, valToWrite) end - end + writeRAM(address, ramItemAddrs[address].size, valToWrite) + end + + -- Finally, if there was a reconciliation message, apply any other changes they had that we never picked up + if reconciliationMessage then + applyReconciliationMessage(prevRAM, theirUser, reconciliationMessage) + end return prevRAM end @@ -827,13 +975,23 @@ function ramController.getMessage() printOutput('Processing incoming message') end local nextmessage = messageQueue.popLeft() - ramController.processMessage(nextmessage.their_user, nextmessage.message) + ramController.processMessage(nextmessage.theirUser, nextmessage.message) end -- Get current RAM events local newItemState = getTransmittableItemsState() local message = getItemStateChanges(prevItemState, newItemState) + local currentTime = os.time() + if currentTime > lastReconcileTime + RECONCILE_PERIOD_SECONDS then + -- message is false if there are otherwise no changes + if not message then + message = {} + end + lastReconcileTime = currentTime + message[RECONCILILIATION_MESSAGE_KEY] = newItemState + end + -- Update the RAM frame pointer prevItemState = newItemState prevGameLoaded = gameLoaded @@ -843,26 +1001,26 @@ end -- Process a message from another player and update RAM -function ramController.processMessage(their_user, message) +function ramController.processMessage(theirUser, message) if message['i'] then message['i'] = nil -- Item splitting is not supported yet end if config.ramconfig.verbose then - printOutput(string.format('Processing message [%s] from [%s].', asString(message), asString(their_user))) + printOutput(string.format('Processing message [%s] from [%s].', asString(message), asString(theirUser))) end if isGameLoadedWithFetch() then if config.ramconfig.verbose then printOutput("Game loaded. About to do the message") end - prevItemState = applyItemStateChanges(prevItemState, their_user, message) + prevItemState = applyItemStateChanges(prevItemState, theirUser, message) else if config.ramconfig.verbose then printOutput("Game not loaded. Putting the message back on the queue") end - messageQueue.pushRight({['their_user']=their_user, ['message']=message}) -- Put the message back in the queue so we reprocess it once the game is loaded + messageQueue.pushRight({['theirUser']=theirUser, ['message']=message}) -- Put the message back in the queue so we reprocess it once the game is loaded end end From 66486f75baca496495b3a46b5db30541bf2734c6 Mon Sep 17 00:00:00 2001 From: Samuel Flynn Date: Sun, 26 Apr 2020 20:48:22 -0400 Subject: [PATCH 13/14] Removed reconciler. It does bad in high latency situations --- .../ramcontroller/Links Awakening.lua | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/bizhawk-co-op/ramcontroller/Links Awakening.lua b/bizhawk-co-op/ramcontroller/Links Awakening.lua index 373c900..64dbc98 100644 --- a/bizhawk-co-op/ramcontroller/Links Awakening.lua +++ b/bizhawk-co-op/ramcontroller/Links Awakening.lua @@ -829,10 +829,14 @@ function applyItemStateChanges(prevRAM, theirUser, newEvents) if maxVal and val > maxVal then newval = maxVal else - if increaseOnly and val > prevval then + if increaseOnly then + if val > prevval then + newval = val + elseif config.ramconfig.verbose then + printOutput(string.format('Refused to downgrade number %s from %s to %s', asString(itemName), asString(prevval), asString(val))) + end + else newval = val - elseif config.ramconfig.verbose then - printOutput(string.format('Refused to downgrade number %s from %s to %s', asString(itemName), asString(prevval), asString(val))) end end @@ -982,15 +986,16 @@ function ramController.getMessage() local newItemState = getTransmittableItemsState() local message = getItemStateChanges(prevItemState, newItemState) - local currentTime = os.time() - if currentTime > lastReconcileTime + RECONCILE_PERIOD_SECONDS then - -- message is false if there are otherwise no changes - if not message then - message = {} - end - lastReconcileTime = currentTime - message[RECONCILILIATION_MESSAGE_KEY] = newItemState - end + -- Reconciliation was a neat idea, but performs badly + -- local currentTime = os.time() + -- if currentTime > lastReconcileTime + RECONCILE_PERIOD_SECONDS then + -- -- message is false if there are otherwise no changes + -- if not message then + -- message = {} + -- end + -- lastReconcileTime = currentTime + -- message[RECONCILILIATION_MESSAGE_KEY] = newItemState + -- end -- Update the RAM frame pointer prevItemState = newItemState From c1ee6c12797ca79bfb95e8ec077bec04fa5642e9 Mon Sep 17 00:00:00 2001 From: Samuel Flynn Date: Sat, 23 May 2020 11:03:57 -0400 Subject: [PATCH 14/14] Removed reboot_core to circumvent https://github.com/TASVideos/BizHawk/issues/1716 --- bizhawk-co-op/ramcontroller/Links Awakening.lua | 2 -- 1 file changed, 2 deletions(-) diff --git a/bizhawk-co-op/ramcontroller/Links Awakening.lua b/bizhawk-co-op/ramcontroller/Links Awakening.lua index 64dbc98..7f8b28d 100644 --- a/bizhawk-co-op/ramcontroller/Links Awakening.lua +++ b/bizhawk-co-op/ramcontroller/Links Awakening.lua @@ -890,8 +890,6 @@ function applyItemStateChanges(prevRAM, theirUser, newEvents) return prevRAM end - -client.reboot_core() ramController.itemcount = tableCount(ramItemAddrs) local messageQueue = {first = 0, last = -1}