diff --git a/data/lua/connector_mmbn3.lua b/data/lua/connector_mmbn3.lua index 8482bf85b1a8..876ab8a460f0 100644 --- a/data/lua/connector_mmbn3.lua +++ b/data/lua/connector_mmbn3.lua @@ -27,14 +27,9 @@ local mmbn3Socket = nil local frame = 0 -- States -local ITEMSTATE_NONINITIALIZED = "Game Not Yet Started" -- Game has not yet started local ITEMSTATE_NONITEM = "Non-Itemable State" -- Do not send item now. RAM is not capable of holding local ITEMSTATE_IDLE = "Item State Ready" -- Ready for the next item if there are any -local ITEMSTATE_SENT = "Item Sent Not Claimed" -- The ItemBit is set, but the dialog has not been closed yet -local itemState = ITEMSTATE_NONINITIALIZED - -local itemQueued = nil -local itemQueueCounter = 120 +local itemState = ITEMSTATE_NONITEM local debugEnabled = false local game_complete = false @@ -104,21 +99,19 @@ end local IsInBattle = function() return memory.read_u8(0x020097F8) == 0x08 end -local IsItemQueued = function() - return memory.read_u8(0x2000224) == 0x01 -end - -- This function actually determines when you're on ANY full-screen menu (navi cust, link battle, etc.) but we -- don't want to check any locations there either so it's fine. local IsOnTitle = function() return bit.band(memory.read_u8(0x020097F8),0x04) == 0 end + local IsItemable = function() - return not IsInMenu() and not IsInTransition() and not IsInDialog() and not IsInBattle() and not IsOnTitle() and not IsItemQueued() + return not IsInMenu() and not IsInTransition() and not IsInDialog() and not IsInBattle() and not IsOnTitle() end local is_game_complete = function() - if IsOnTitle() or itemState == ITEMSTATE_NONINITIALIZED then return game_complete end + -- If on the title screen don't read RAM, RAM can't be trusted yet + if IsOnTitle() then return game_complete end -- If the game is already marked complete, do not read memory if game_complete then return true end @@ -177,14 +170,6 @@ local Check_Progressive_Undernet_ID = function() end return 9 end -local GenerateTextBytes = function(message) - bytes = {} - for i = 1, #message do - local c = message:sub(i,i) - table.insert(bytes, charDict[c]) - end - return bytes -end -- Item Message Generation functions local Next_Progressive_Undernet_ID = function(index) @@ -196,150 +181,6 @@ local Next_Progressive_Undernet_ID = function(index) item_index=ordered_IDs[index] return item_index end -local Extra_Progressive_Undernet = function() - fragBytes = int32ToByteList_le(20) - bytes = { - 0xF6, 0x50, fragBytes[1], fragBytes[2], fragBytes[3], fragBytes[4], 0xFF, 0xFF, 0xFF - } - bytes = TableConcat(bytes, GenerateTextBytes("The extra data\ndecompiles into:\n\"20 BugFrags\"!!")) - return bytes -end - -local GenerateChipGet = function(chip, code, amt) - chipBytes = int16ToByteList_le(chip) - bytes = { - 0xF6, 0x10, chipBytes[1], chipBytes[2], code, amt, - charDict['G'], charDict['o'], charDict['t'], charDict[' '], charDict['a'], charDict[' '], charDict['c'], charDict['h'], charDict['i'], charDict['p'], charDict[' '], charDict['f'], charDict['o'], charDict['r'], charDict['\n'], - - } - if chip < 256 then - bytes = TableConcat(bytes, { - charDict['\"'], 0xF9,0x00,chipBytes[1],0x01,0x00,0xF9,0x00,code,0x03, charDict['\"'],charDict['!'],charDict['!'] - }) - else - bytes = TableConcat(bytes, { - charDict['\"'], 0xF9,0x00,chipBytes[1],0x02,0x00,0xF9,0x00,code,0x03, charDict['\"'],charDict['!'],charDict['!'] - }) - end - return bytes -end -local GenerateKeyItemGet = function(item, amt) - bytes = { - 0xF6, 0x00, item, amt, - charDict['G'], charDict['o'], charDict['t'], charDict[' '], charDict['a'], charDict['\n'], - charDict['\"'], 0xF9, 0x00, item, 0x00, charDict['\"'],charDict['!'],charDict['!'] - } - return bytes -end -local GenerateSubChipGet = function(subchip, amt) - -- SubChips have an extra bit of trouble. If you have too many, they're supposed to skip to another text bank that doesn't give you the item - -- Instead, I'm going to just let it get eaten - bytes = { - 0xF6, 0x20, subchip, amt, 0xFF, 0xFF, 0xFF, - charDict['G'], charDict['o'], charDict['t'], charDict[' '], charDict['a'], charDict['\n'], - charDict['S'], charDict['u'], charDict['b'], charDict['C'], charDict['h'], charDict['i'], charDict['p'], charDict[' '], charDict['f'], charDict['o'], charDict['r'], charDict['\n'], - charDict['\"'], 0xF9, 0x00, subchip, 0x00, charDict['\"'],charDict['!'],charDict['!'] - } - return bytes -end -local GenerateZennyGet = function(amt) - zennyBytes = int32ToByteList_le(amt) - bytes = { - 0xF6, 0x30, zennyBytes[1], zennyBytes[2], zennyBytes[3], zennyBytes[4], 0xFF, 0xFF, 0xFF, - charDict['G'], charDict['o'], charDict['t'], charDict[' '], charDict['a'], charDict['\n'], charDict['\"'] - } - -- The text needs to be added one char at a time, so we need to convert the number to a string then iterate through it - zennyStr = tostring(amt) - for i = 1, #zennyStr do - local c = zennyStr:sub(i,i) - table.insert(bytes, charDict[c]) - end - bytes = TableConcat(bytes, { - charDict[' '], charDict['Z'], charDict['e'], charDict['n'], charDict['n'], charDict['y'], charDict['s'], charDict['\"'],charDict['!'],charDict['!'] - }) - return bytes -end -local GenerateProgramGet = function(program, color, amt) - bytes = { - 0xF6, 0x40, (program * 4), amt, color, - charDict['G'], charDict['o'], charDict['t'], charDict[' '], charDict['a'], charDict[' '], charDict['N'], charDict['a'], charDict['v'], charDict['i'], charDict['\n'], - charDict['C'], charDict['u'], charDict['s'], charDict['t'], charDict['o'], charDict['m'], charDict['i'], charDict['z'], charDict['e'], charDict['r'], charDict[' '], charDict['P'], charDict['r'], charDict['o'], charDict['g'], charDict['r'], charDict['a'], charDict['m'], charDict[':'], charDict['\n'], - charDict['\"'], 0xF9, 0x00, program, 0x05, charDict['\"'],charDict['!'],charDict['!'] - } - - return bytes -end -local GenerateBugfragGet = function(amt) - fragBytes = int32ToByteList_le(amt) - bytes = { - 0xF6, 0x50, fragBytes[1], fragBytes[2], fragBytes[3], fragBytes[4], 0xFF, 0xFF, 0xFF, - charDict['G'], charDict['o'], charDict['t'], charDict[':'], charDict['\n'], charDict['\"'] - } - -- The text needs to be added one char at a time, so we need to convert the number to a string then iterate through it - bugFragStr = tostring(amt) - for i = 1, #bugFragStr do - local c = bugFragStr:sub(i,i) - table.insert(bytes, charDict[c]) - end - bytes = TableConcat(bytes, { - charDict[' '], charDict['B'], charDict['u'], charDict['g'], charDict['F'], charDict['r'], charDict['a'], charDict['g'], charDict['s'], charDict['\"'],charDict['!'],charDict['!'] - }) - return bytes -end -local GenerateGetMessageFromItem = function(item) - --Special case for progressive undernet - if item["type"] == "undernet" then - undernet_id = Check_Progressive_Undernet_ID() - if undernet_id > 8 then - return Extra_Progressive_Undernet() - end - return GenerateKeyItemGet(Next_Progressive_Undernet_ID(undernet_id),1) - elseif item["type"] == "chip" then - return GenerateChipGet(item["itemID"], item["subItemID"], item["count"]) - elseif item["type"] == "key" then - return GenerateKeyItemGet(item["itemID"], item["count"]) - elseif item["type"] == "subchip" then - return GenerateSubChipGet(item["itemID"], item["count"]) - elseif item["type"] == "zenny" then - return GenerateZennyGet(item["count"]) - elseif item["type"] == "program" then - return GenerateProgramGet(item["itemID"], item["subItemID"], item["count"]) - elseif item["type"] == "bugfrag" then - return GenerateBugfragGet(item["count"]) - end - - return GenerateTextBytes("Empty Message") -end - -local GetMessage = function(item) - startBytes = {0x02, 0x00} - playerLockBytes = {0xF8,0x00, 0xF8, 0x10} - msgOpenBytes = {0xF1, 0x02} - textBytes = GenerateTextBytes("Receiving\ndata from\n"..item["sender"]..".") - dotdotWaitBytes = {0xEA,0x00,0x0A,0x00,0x4D,0xEA,0x00,0x0A,0x00,0x4D} - continueBytes = {0xEB, 0xE9} - -- continueBytes = {0xE9} - playReceiveAnimationBytes = {0xF8,0x04,0x18} - chipGiveBytes = GenerateGetMessageFromItem(item) - playerFinishBytes = {0xF8, 0x0C} - playerUnlockBytes={0xEB, 0xF8, 0x08} - -- playerUnlockBytes={0xF8, 0x08} - endMessageBytes = {0xF8, 0x10, 0xE7} - - bytes = {} - bytes = TableConcat(bytes,startBytes) - bytes = TableConcat(bytes,playerLockBytes) - bytes = TableConcat(bytes,msgOpenBytes) - bytes = TableConcat(bytes,textBytes) - bytes = TableConcat(bytes,dotdotWaitBytes) - bytes = TableConcat(bytes,continueBytes) - bytes = TableConcat(bytes,playReceiveAnimationBytes) - bytes = TableConcat(bytes,chipGiveBytes) - bytes = TableConcat(bytes,playerFinishBytes) - bytes = TableConcat(bytes,playerUnlockBytes) - bytes = TableConcat(bytes,endMessageBytes) - return bytes -end local getChipCodeIndex = function(chip_id, chip_code) chipCodeArrayStartAddress = 0x8011510 + (0x20 * chip_id) @@ -353,6 +194,10 @@ local getChipCodeIndex = function(chip_id, chip_code) end local getProgramColorIndex = function(program_id, program_color) + -- For whatever reason, OilBody (ID 24) does not follow the rules and should be color index 3 + if program_id == 24 then + return 3 + end -- The general case, most programs use white pink or yellow. This is the values the enums already have if program_id >= 20 and program_id <= 47 then return program_color-1 @@ -401,11 +246,11 @@ local changeZenny = function(val) return 0 end if memory.read_u32_le(0x20018F4) <= math.abs(tonumber(val)) and tonumber(val) < 0 then - memory.write_u32_le(0x20018f4, 0) + memory.write_u32_le(0x20018F4, 0) val = 0 return "empty" end - memory.write_u32_le(0x20018f4, memory.read_u32_le(0x20018F4) + tonumber(val)) + memory.write_u32_le(0x20018F4, memory.read_u32_le(0x20018F4) + tonumber(val)) if memory.read_u32_le(0x20018F4) > 999999 then memory.write_u32_le(0x20018F4, 999999) end @@ -417,30 +262,17 @@ local changeFrags = function(val) return 0 end if memory.read_u16_le(0x20018F8) <= math.abs(tonumber(val)) and tonumber(val) < 0 then - memory.write_u16_le(0x20018f8, 0) + memory.write_u16_le(0x20018F8, 0) val = 0 return "empty" end - memory.write_u16_le(0x20018f8, memory.read_u16_le(0x20018F8) + tonumber(val)) + memory.write_u16_le(0x20018F8, memory.read_u16_le(0x20018F8) + tonumber(val)) if memory.read_u16_le(0x20018F8) > 9999 then memory.write_u16_le(0x20018F8, 9999) end return val end --- Fix Health Pools -local fix_hp = function() - -- Current Health fix - if IsInBattle() and not (memory.read_u16_le(0x20018A0) == memory.read_u16_le(0x2037294)) then - memory.write_u16_le(0x20018A0, memory.read_u16_le(0x2037294)) - end - - -- Max Health Fix - if IsInBattle() and not (memory.read_u16_le(0x20018A2) == memory.read_u16_le(0x2037296)) then - memory.write_u16_le(0x20018A2, memory.read_u16_le(0x2037296)) - end -end - local changeRegMemory = function(amt) regMemoryAddress = 0x02001897 currentRegMem = memory.read_u8(regMemoryAddress) @@ -448,34 +280,18 @@ local changeRegMemory = function(amt) end local changeMaxHealth = function(val) - fix_hp() - if val == nil then - fix_hp() + if val == nil then return 0 end - if math.abs(tonumber(val)) >= memory.read_u16_le(0x20018A2) and tonumber(val) < 0 then - memory.write_u16_le(0x20018A2, 0) - if IsInBattle() then - memory.write_u16_le(0x2037296, memory.read_u16_le(0x20018A2)) - if memory.read_u16_le(0x2037296) >= memory.read_u16_le(0x20018A2) then - memory.write_u16_le(0x2037296, memory.read_u16_le(0x20018A2)) - end - end - fix_hp() - return "lethal" - end + memory.write_u16_le(0x20018A2, memory.read_u16_le(0x20018A2) + tonumber(val)) if memory.read_u16_le(0x20018A2) > 9999 then memory.write_u16_le(0x20018A2, 9999) end - if IsInBattle() then - memory.write_u16_le(0x2037296, memory.read_u16_le(0x20018A2)) - end - fix_hp() return val end -local SendItem = function(item) +local SendItemToGame = function(item) if item["type"] == "undernet" then undernet_id = Check_Progressive_Undernet_ID() if undernet_id > 8 then @@ -553,13 +369,6 @@ local OpenShortcuts = function() end end -local RestoreItemRam = function() - if backup_bytes ~= nil then - memory.write_bytes_as_array(0x203fe10, backup_bytes) - end - backup_bytes = nil -end - local process_block = function(block) -- Sometimes the block is nothing, if this is the case then quietly stop processing if block == nil then @@ -574,14 +383,7 @@ local process_block = function(block) end local itemStateMachineProcess = function() - if itemState == ITEMSTATE_NONINITIALIZED then - itemQueueCounter = 120 - -- Only exit this state the first time a dialog window pops up. This way we know for sure that we're ready to receive - if not IsInMenu() and (IsInDialog() or IsInTransition()) then - itemState = ITEMSTATE_NONITEM - end - elseif itemState == ITEMSTATE_NONITEM then - itemQueueCounter = 120 + if itemState == ITEMSTATE_NONITEM then -- Always attempt to restore the previously stored memory in this state -- Exit this state whenever the game is in an itemable status if IsItemable() then @@ -592,26 +394,11 @@ local itemStateMachineProcess = function() if not IsItemable() then itemState = ITEMSTATE_NONITEM end - if itemQueueCounter == 0 then - if #itemsReceived > loadItemIndexFromRAM() and not IsItemQueued() then - itemQueued = itemsReceived[loadItemIndexFromRAM()+1] - SendItem(itemQueued) - itemState = ITEMSTATE_SENT - end - else - itemQueueCounter = itemQueueCounter - 1 - end - elseif itemState == ITEMSTATE_SENT then - -- Once the item is sent, wait for the dialog to close. Then clear the item bit and be ready for the next item. - if IsInTransition() or IsInMenu() or IsOnTitle() then - itemState = ITEMSTATE_NONITEM - itemQueued = nil - RestoreItemRam() - elseif not IsInDialog() then - itemState = ITEMSTATE_IDLE + if #itemsReceived > loadItemIndexFromRAM() then + itemQueued = itemsReceived[loadItemIndexFromRAM()+1] + SendItemToGame(itemQueued) saveItemIndexToRAM(itemQueued["itemIndex"]) - itemQueued = nil - RestoreItemRam() + itemState = ITEMSTATE_NONITEM end end end @@ -702,18 +489,8 @@ function main() -- Handle the debug data display gui.cleartext() if debugEnabled then - -- gui.text(0,0,"Item Queued: "..tostring(IsItemQueued())) - -- gui.text(0,16,"In Battle: "..tostring(IsInBattle())) - -- gui.text(0,32,"In Dialog: "..tostring(IsInDialog())) - -- gui.text(0,48,"In Menu: "..tostring(IsInMenu())) - gui.text(0,48,"Item Wait Time: "..tostring(itemQueueCounter)) - gui.text(0,64,itemState) - if itemQueued == nil then - gui.text(0,80,"No item queued") - else - gui.text(0,80,itemQueued["type"].." "..itemQueued["itemID"]) - end - gui.text(0,96,"Item Index: "..loadItemIndexFromRAM()) + gui.text(0,0,itemState) + gui.text(0,16,"Item Index: "..loadItemIndexFromRAM()) end emu.frameadvance() diff --git a/worlds/mmbn3/Options.py b/worlds/mmbn3/Options.py index 96a01290a5c7..4ed64e3d9dbf 100644 --- a/worlds/mmbn3/Options.py +++ b/worlds/mmbn3/Options.py @@ -1,4 +1,5 @@ -from Options import Choice, Range, DefaultOnToggle +from dataclasses import dataclass +from Options import Choice, Range, DefaultOnToggle, PerGameCommonOptions class ExtraRanks(Range): @@ -41,8 +42,9 @@ class TradeQuestHinting(Choice): default = 2 -MMBN3Options = { - "extra_ranks": ExtraRanks, - "include_jobs": IncludeJobs, - "trade_quest_hinting": TradeQuestHinting, -} +@dataclass +class MMBN3Options(PerGameCommonOptions): + extra_ranks: ExtraRanks + include_jobs: IncludeJobs + trade_quest_hinting: TradeQuestHinting + \ No newline at end of file diff --git a/worlds/mmbn3/__init__.py b/worlds/mmbn3/__init__.py index 762bfd11ae4a..eac8a37bf06d 100644 --- a/worlds/mmbn3/__init__.py +++ b/worlds/mmbn3/__init__.py @@ -7,6 +7,7 @@ LocationProgressType from worlds.AutoWorld import WebWorld, World + from .Rom import MMBN3DeltaPatch, LocalRom, get_base_rom_path from .Items import MMBN3Item, ItemData, item_table, all_items, item_frequencies, items_by_id, ItemType from .Locations import Location, MMBN3Location, all_locations, location_table, location_data_table, \ @@ -51,7 +52,8 @@ class MMBN3World(World): threat the Internet has ever faced! """ game = "MegaMan Battle Network 3" - option_definitions = MMBN3Options + options_dataclass = MMBN3Options + options: MMBN3Options settings: typing.ClassVar[MMBN3Settings] topology_present = False @@ -71,10 +73,10 @@ def generate_early(self) -> None: Already has access to player options and RNG. """ self.item_frequencies = item_frequencies.copy() - if self.multiworld.extra_ranks[self.player] > 0: - self.item_frequencies[ItemName.Progressive_Undernet_Rank] = 8 + self.multiworld.extra_ranks[self.player] + if self.options.extra_ranks > 0: + self.item_frequencies[ItemName.Progressive_Undernet_Rank] = 8 + self.options.extra_ranks - if not self.multiworld.include_jobs[self.player]: + if not self.options.include_jobs: self.excluded_locations = always_excluded_locations + [job.name for job in jobs] else: self.excluded_locations = always_excluded_locations @@ -160,7 +162,7 @@ def create_items(self) -> None: remaining = len(all_locations) - len(required_items) for i in range(remaining): - filler_item_name = self.multiworld.random.choice(filler_items) + filler_item_name = self.random.choice(filler_items) item = self.create_item(filler_item_name) self.multiworld.itempool.append(item) filler_items.remove(filler_item_name) @@ -411,10 +413,10 @@ def generate_output(self, output_directory: str) -> None: long_item_text = "" # No item hinting - if self.multiworld.trade_quest_hinting[self.player] == 0: + if self.options.trade_quest_hinting == 0: item_name_text = "Check" # Partial item hinting - elif self.multiworld.trade_quest_hinting[self.player] == 1: + elif self.options.trade_quest_hinting == 1: if item.progression == ItemClassification.progression \ or item.progression == ItemClassification.progression_skip_balancing: item_name_text = "Progress" @@ -466,7 +468,7 @@ def create_event(self, event: str): return MMBN3Item(event, ItemClassification.progression, None, self.player) def fill_slot_data(self): - return {name: getattr(self.multiworld, name)[self.player].value for name in self.option_definitions} + return self.options.as_dict("extra_ranks", "include_jobs", "trade_quest_hinting") def explore_score(self, state): diff --git a/worlds/mmbn3/docs/setup_en.md b/worlds/mmbn3/docs/setup_en.md index 44a6b9c14448..b26403f78bb9 100644 --- a/worlds/mmbn3/docs/setup_en.md +++ b/worlds/mmbn3/docs/setup_en.md @@ -18,11 +18,12 @@ on Steam, you can obtain a copy of this ROM from the game's files, see instructi Once Bizhawk has been installed, open Bizhawk and change the following settings: -- Go to Config > Customize. Switch to the Advanced tab, then switch the Lua Core from "NLua+KopiLua" to - "Lua+LuaInterface". This is required for the Lua script to function correctly. - **NOTE: Even if "Lua+LuaInterface" is already selected, toggle between the two options and reselect it. Fresh installs** - **of newer versions of Bizhawk have a tendency to show "Lua+LuaInterface" as the default selected option but still load** - **"NLua+KopiLua" until this step is done.** +- **If you are using a version of BizHawk older than 2.9**, you will need to modify the Lua Core. + Go to Config > Customize. Switch to the Advanced tab, then switch the Lua Core from "NLua+KopiLua" to + "Lua+LuaInterface". This is required for the Lua script to function correctly. + **NOTE:** Even if "Lua+LuaInterface" is already selected, toggle between the two options and reselect it. Fresh installs + of newer versions of Bizhawk have a tendency to show "Lua+LuaInterface" as the default selected option but still load + "NLua+KopiLua" until this step is done. - Under Config > Customize > Advanced, make sure the box for AutoSaveRAM is checked, and click the 5s button. This reduces the possibility of losing save data in emulator crashes. - Under Config > Customize, check the "Run in background" and "Accept background input" boxes. This will allow you to @@ -37,7 +38,7 @@ and select EmuHawk.exe. ## Extracting a ROM from the Legacy Collection -The Steam version of the Legacy Collection contains unmodified GBA ROMs in its files. You can extract these for use with Archipelago. +The Steam version of the Battle Network Legacy Collection contains unmodified GBA ROMs in its files. You can extract these for use with Archipelago. 1. Open the Legacy Collection Vol. 1's Game Files (Right click on the game in your Library, then open Properties -> Installed Files -> Browse) 2. Open the file `exe/data/exe3b.dat` in a zip-extracting program such as 7-Zip or WinRAR. @@ -73,7 +74,9 @@ to the emulator as recommended). Once both the client and the emulator are started, you must connect them. Within the emulator click on the "Tools" menu and select "Lua Console". Click the folder button or press Ctrl+O to open a Lua script. -Navigate to your Archipelago install folder and open `data/lua/connector_mmbn3.lua`. +Navigate to your Archipelago install folder and open `data/lua/connector_mmbn3.lua`. +**NOTE:** The MMBN3 Lua file depends on other shared Lua files inside of the `data` directory in the Archipelago +installation. Do not move this Lua file from its default location or you may run into issues connecting. To connect the client to the multiserver simply put `
:` on the textfield on top and press enter (if the server uses password, type in the bottom textfield `/connect
: [password]`)