MMBN3: Modernizations and Minor Bugfixes (#2991)

This commit is contained in:
digiholic 2024-04-18 11:02:01 -06:00 committed by GitHub
parent 727915040d
commit f89cee4b15
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 51 additions and 267 deletions

View File

@ -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()

View File

@ -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

View File

@ -7,6 +7,7 @@ from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification, Region,
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 @@ class MMBN3World(World):
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 @@ class MMBN3World(World):
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 @@ class MMBN3World(World):
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 @@ class MMBN3World(World):
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):

View File

@ -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 `<address>:<port>` on the textfield on top and press enter (if the
server uses password, type in the bottom textfield `/connect <address>:<port> [password]`)