Files
Grinch-AP/worlds/sm/variaRandomizer/rom/rompatcher.py

1442 lines
62 KiB
Python
Raw Normal View History

import os, random, re, json
from math import ceil
from enum import IntFlag
2023-04-08 16:52:34 -04:00
from ..rando.Items import ItemManager
from ..rom.ips import IPS_Patch
from ..utils.doorsmanager import DoorsManager, IndicatorFlag
from ..utils.objectives import Objectives
from ..graph.graph_utils import GraphUtils, getAccessPoint, locIdsByAreaAddresses, graphAreas
2023-04-08 16:52:34 -04:00
from ..logic.logic import Logic
SM generate without rom (#3460) * - SM now displays message when getting an item outside for someone else (fills ROM item table) This is dependant on modifications done to sm_randomizer_rom project * First working MultiWorld SM * some missing things: - player name inject in ROM and get in client - end game get from ROM in client - send self item to server - add player names table in ROM * replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better) * - reenabled balancing * post rebase fixes * updated SmClient.py * + added VariaRandomizer LICENSE * + added sm_randomizer_rom project (which builds sm.ips) * Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning * properly revert change made to CollectionState and more cleaning * Fixed multiworld support patch not working with VariaRandomizer's * missing file commit * Fixed syntax error in unused code to satisfy Linter * Revert "Fixed multiworld support patch not working with VariaRandomizer's" This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b. * many fixes and improovement - fixed seeded generation - fixed broken logic when more than one SM world - added missing rules for inter-area transitions - added basic patch presence for logic - added DoorManager init call to reflect present patches for logic - moved CollectionState addition out of BaseClasses into SM world - added condition to apply progitempool presorting only if SM world is present - set Bosses item id to None to prevent them going into multidata - now use get_game_players * first working (most of the time) progression generation for SM using VariaRandomizer's rules, items, locations and accessPoint (as regions) * first working single-world randomized SM rom patches * - SM now displays message when getting an item outside for someone else (fills ROM item table) This is dependant on modifications done to sm_randomizer_rom project * First working MultiWorld SM * some missing things: - player name inject in ROM and get in client - end game get from ROM in client - send self item to server - add player names table in ROM * replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better) * - reenabled balancing * post rebase fixes * updated SmClient.py * + added VariaRandomizer LICENSE * + added sm_randomizer_rom project (which builds sm.ips) * Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning * properly revert change made to CollectionState and more cleaning * Fixed multiworld support patch not working with VariaRandomizer's * missing file commit * Fixed syntax error in unused code to satisfy Linter * Revert "Fixed multiworld support patch not working with VariaRandomizer's" This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b. * many fixes and improovement - fixed seeded generation - fixed broken logic when more than one SM world - added missing rules for inter-area transitions - added basic patch presence for logic - added DoorManager init call to reflect present patches for logic - moved CollectionState addition out of BaseClasses into SM world - added condition to apply progitempool presorting only if SM world is present - set Bosses item id to None to prevent them going into multidata - now use get_game_players * Fixed multiworld support patch not working with VariaRandomizer's Added stage_fill_hook to set morph first in progitempool Added back VariaRandomizer's standard patches * + added missing files from variaRandomizer project * + added missing variaRandomizer files (custom sprites) + started integrating VariaRandomizer options (WIP) * Some fixes for player and server name display - fixed player name of 16 characters reading too far in SM client - fixed 12 bytes SM player name limit (now 16) - fixed server name not being displayed in SM when using server cheat ( now displays RECEIVED FROM ARCHIPELAGO) - request: temporarly changed default seed names displayed in SM main menu to OWTCH * Fixed Goal completion not triggering in smClient * integrated VariaRandomizer's options into AP (WIP) - startAP is working - door rando is working - skillset is working * - fixed itemsounds.ips crash by always including nofanfare.ips into multiworld.ips (itemsounds is now always applied and "itemsounds" preset must always be "off") * skillset are now instanced per player instead of being a singleton class * RomPatches are now instanced per player instead of being a singleton class * DoorManager is now instanced per player instead of being a singleton class * - fixed the last bugs that prevented generation of >1 SM world * fixed crash when no skillset preset is specified in randoPreset (default to "casual") * maxDifficulty support and itemsounds removal - added support for maxDifficulty - removed itemsounds patch as its always applied from multiworld patch for now * Fixed bad merge * Post merge adaptation * fixed player name length fix that got lost with the merge * fixed generation with other game type than SM * added default randoPreset json for SM in playerSettings.yaml * fixed broken SM client following merge * beautified json skillset presets * Fixed ArchipelagoSmClient not building * Fixed conflict between mutliworld patch and beam_doors_plms patch - doorsColorsRando now working * SM generation now outputs APBP - Fixed paths for patches and presets when frozen * added missing file and fixed multithreading issue * temporarily set data_version = 0 * more work - added support for AP starting items - fixed client crash with gamemode being None - patch.py "compatible_version" is now 3 * commited missing asm files fixed start item reserve breaking game (was using bad write offset when patching) * Nothing item are now handled game-side. the game will now skip displaying a message box for received Nothing item (but the client will still receive it). fixed crash in SMClient when loosing connection to SNI * fixed No Energy Item missing its ID fixed Plando * merge post fixes * fixed start item Grapple, XRay and Reserve HUD, as well as graphic beams (except ice palette color) * fixed freeze in blue brinstar caused by Varia's custom PLM not being filled with proper Multiworld PLM address (altLocsAddresses) * fixed start item x-ray HUD display * Fixed start items being sent by the server (is all handled in ROM) Start items are now not removed from itempool anymore Nothing Item is now local_items so no player will ever pickup Nothing. Doing so reduces contribution of this world to the Multiworld the more Nothing there is though. Fixed crash (and possibly passing but broken) at generation where the static list of IPSPatches used by all SM worlds was being modified * fixed settings that could be applied to any SM players * fixed auth to server only using player name (now does as ALTTP to authenticate) * - fixed End Credits broken text * added non SM item name display * added all supported SM options in playerSettings.yaml * fixed locations needing a list of parent regions (now generate a region for each location with one-way exits to each (previously) parent region did some cleaning (mainly reverts on unnecessary core classes * minor setting fixes and tweaks - merged Area and lightArea settings - made missileQty, superQty and powerBombQty use value from 10 to 90 and divide value by float(10) when generating - fixed inverted layoutPatch setting * added option start_inventory_removes_from_pool fixed option names formatting fixed lint errors small code and repo cleanup * Hopefully fixed ROR2 that could not send any items * - fixed missing required change to ROR2 * fixed 0 hp when respawning without having ever saved (start items were not updating the save checksum) * fixed typo with doors_colors_rando * fixed checksum * added custom sprites for off-world items (progression or not) the original AP sprite was made with PierRoulette's SM Item Sprite Utility by ijwu * - added missing change following upstream merge - changed patch filename extension from apbp to apm3 so patch can be used with the new client * added morph placement options: early means local and sphere 1 * fixed failing unit tests * - fixed broken custom_preset options * - big cleanup to remove unnecessary or unsupported features * - more cleanup * - moved sm_randomizer_rom and all always applied patches into an external project that outputs basepatch.ips - small cleanup * - added comment to refer to project for generating basepatch.ips (https://github.com/lordlou/SMBasepatch) * fixed g4_skip patch that can be not applied if hud is enabled * - fixed off world sprite that can have broken graphics (restricted to use only first 2 palette) * - updated basepatch to reflect g4_skip removal - moved more asm files to SMBasepatch project * - tourian grey doors at baby metroid are now always flashing (allowing to go back if needed) * fixed wrong path if using built as exe * - cleaned exposed maxDifficulty options - removed always enabled Knows * Merged LttPClient and SMClient into SNIClient * added varia_custom Preset Option that fetch a preset (read from a new varia_custom_preset Option) from varia's web service * small doc precision * - added death_link support - fixed broken Goal Completion - post merge fix * - removed now useless presets * - fixed bad internal mapping with maxDiff - increases maxDiff if only Bosses is preventing beating the game * - added support for lowercase custom preset sections (knows, settings and controller) - fixed controller settings not applying to ROM * - fixed death loop when dying with Door rando, bomb or speed booster as starting items - varia's backup save should now be usable (automatically enabled when doing door rando) * -added docstring for generated yaml * fixed bad merge * fixed broken infinity max difficulty * commented debug prints * adjusted credits to mark progression speed and difficulty as Non Available * added support for more than 255 players (will print Archipelago for higher player number) * fixed missing cleanup * added support for 65535 different player names in ROM * fixed generations failing when only bosses are unreachable * - replaced setting maxDiff to infinity with a bool only affecting boss logics if only bosses are left to finish * fixed failling generations when using 'fun' settings Accessibility checks are forced to 'items' if restricted locations are used by VARIA following usage of 'fun' settings * fixed debug logger * removed unsupported "suits_restriction" option * fixed generations failing when only bosses are unreachable (using a less intrusive approach for AP) * - fixed deathlink emptying reserves - added death_link_survive option that lets player survive when receiving a deathlink if the have non-empty reserves * - merged death_link and death_link_survive options * fixed death_link * added a fallback default starting location instead of failing generation if an invalid one was chosen * added Nothing and NoEnergy as hint blacklist added missing NoEnergy as local items and removed it from progression * SM Varia can now generate without ROM * removed stage_assert_generate
2025-01-10 15:46:17 -05:00
from ..rom.rom import FakeROM, snes_to_pc, pc_to_snes
from ..rom.addresses import Addresses
from ..rom.rom_patches import RomPatches
2023-04-08 16:52:34 -04:00
from ..patches.patchaccess import PatchAccess
from ..utils.parameters import appDir
from ..utils import log
def getWord(w):
return (w & 0x00FF, (w & 0xFF00) >> 8)
class RomPatcher:
# possible patches. see patches asm source if applicable and available for more information
IPSPatches = {
# applied on all seeds
'Standard': [
# faster MB cutscene transitions
'Mother_Brain_Cutscene_Edits',
# "Balanced" suit mode
'Removes_Gravity_Suit_heat_protection',
# new PLMs for indicating the color of the door on the other side
'door_indicators_plms.ips'
],
# VARIA tweaks
'VariaTweaks' : ['WS_Etank', 'LN_Chozo_SpaceJump_Check_Disable', 'ln_chozo_platform.ips', 'bomb_torizo.ips'],
# anti-softlock/game opening layout patches
'Layout': ['dachora.ips', 'early_super_bridge.ips', 'high_jump.ips', 'moat.ips', 'spospo_save.ips',
'nova_boost_platform.ips', 'red_tower.ips', 'spazer.ips',
'brinstar_map_room.ips', 'kraid_save.ips', 'mission_impossible.ips'],
# base patchset+optional layout for area rando
'Area': ['area_rando_layout.ips', 'door_transition.ips', 'area_rando_doors.ips',
'Sponge_Bath_Blinking_Door', 'east_ocean.ips', 'area_rando_warp_door.ips', 'aqueduct_bomb_blocks.ips',
'crab_shaft.ips', 'Save_Crab_Shaft', 'Save_Main_Street', 'no_demo.ips'],
# patches for boss rando
'Bosses': ['door_transition.ips', 'no_demo.ips'],
# patches for escape rando
'Escape' : ['rando_escape.ips', 'rando_escape_ws_fix.ips', 'door_transition.ips'],
# patches for minimizer with fast Tourian
'MinimizerTourian': ['minimizer_tourian.ips', 'open_zebetites.ips'],
# patches for door color rando
'DoorsColors': ['beam_doors_plms.ips', 'beam_doors_gfx.ips', 'red_doors.ips']
}
def __init__(self, settings=None, romFileName=None, magic=None, player=0):
2023-03-25 14:30:38 -04:00
self.log = log.get('RomPatcher')
self.settings = settings
SM generate without rom (#3460) * - SM now displays message when getting an item outside for someone else (fills ROM item table) This is dependant on modifications done to sm_randomizer_rom project * First working MultiWorld SM * some missing things: - player name inject in ROM and get in client - end game get from ROM in client - send self item to server - add player names table in ROM * replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better) * - reenabled balancing * post rebase fixes * updated SmClient.py * + added VariaRandomizer LICENSE * + added sm_randomizer_rom project (which builds sm.ips) * Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning * properly revert change made to CollectionState and more cleaning * Fixed multiworld support patch not working with VariaRandomizer's * missing file commit * Fixed syntax error in unused code to satisfy Linter * Revert "Fixed multiworld support patch not working with VariaRandomizer's" This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b. * many fixes and improovement - fixed seeded generation - fixed broken logic when more than one SM world - added missing rules for inter-area transitions - added basic patch presence for logic - added DoorManager init call to reflect present patches for logic - moved CollectionState addition out of BaseClasses into SM world - added condition to apply progitempool presorting only if SM world is present - set Bosses item id to None to prevent them going into multidata - now use get_game_players * first working (most of the time) progression generation for SM using VariaRandomizer's rules, items, locations and accessPoint (as regions) * first working single-world randomized SM rom patches * - SM now displays message when getting an item outside for someone else (fills ROM item table) This is dependant on modifications done to sm_randomizer_rom project * First working MultiWorld SM * some missing things: - player name inject in ROM and get in client - end game get from ROM in client - send self item to server - add player names table in ROM * replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better) * - reenabled balancing * post rebase fixes * updated SmClient.py * + added VariaRandomizer LICENSE * + added sm_randomizer_rom project (which builds sm.ips) * Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning * properly revert change made to CollectionState and more cleaning * Fixed multiworld support patch not working with VariaRandomizer's * missing file commit * Fixed syntax error in unused code to satisfy Linter * Revert "Fixed multiworld support patch not working with VariaRandomizer's" This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b. * many fixes and improovement - fixed seeded generation - fixed broken logic when more than one SM world - added missing rules for inter-area transitions - added basic patch presence for logic - added DoorManager init call to reflect present patches for logic - moved CollectionState addition out of BaseClasses into SM world - added condition to apply progitempool presorting only if SM world is present - set Bosses item id to None to prevent them going into multidata - now use get_game_players * Fixed multiworld support patch not working with VariaRandomizer's Added stage_fill_hook to set morph first in progitempool Added back VariaRandomizer's standard patches * + added missing files from variaRandomizer project * + added missing variaRandomizer files (custom sprites) + started integrating VariaRandomizer options (WIP) * Some fixes for player and server name display - fixed player name of 16 characters reading too far in SM client - fixed 12 bytes SM player name limit (now 16) - fixed server name not being displayed in SM when using server cheat ( now displays RECEIVED FROM ARCHIPELAGO) - request: temporarly changed default seed names displayed in SM main menu to OWTCH * Fixed Goal completion not triggering in smClient * integrated VariaRandomizer's options into AP (WIP) - startAP is working - door rando is working - skillset is working * - fixed itemsounds.ips crash by always including nofanfare.ips into multiworld.ips (itemsounds is now always applied and "itemsounds" preset must always be "off") * skillset are now instanced per player instead of being a singleton class * RomPatches are now instanced per player instead of being a singleton class * DoorManager is now instanced per player instead of being a singleton class * - fixed the last bugs that prevented generation of >1 SM world * fixed crash when no skillset preset is specified in randoPreset (default to "casual") * maxDifficulty support and itemsounds removal - added support for maxDifficulty - removed itemsounds patch as its always applied from multiworld patch for now * Fixed bad merge * Post merge adaptation * fixed player name length fix that got lost with the merge * fixed generation with other game type than SM * added default randoPreset json for SM in playerSettings.yaml * fixed broken SM client following merge * beautified json skillset presets * Fixed ArchipelagoSmClient not building * Fixed conflict between mutliworld patch and beam_doors_plms patch - doorsColorsRando now working * SM generation now outputs APBP - Fixed paths for patches and presets when frozen * added missing file and fixed multithreading issue * temporarily set data_version = 0 * more work - added support for AP starting items - fixed client crash with gamemode being None - patch.py "compatible_version" is now 3 * commited missing asm files fixed start item reserve breaking game (was using bad write offset when patching) * Nothing item are now handled game-side. the game will now skip displaying a message box for received Nothing item (but the client will still receive it). fixed crash in SMClient when loosing connection to SNI * fixed No Energy Item missing its ID fixed Plando * merge post fixes * fixed start item Grapple, XRay and Reserve HUD, as well as graphic beams (except ice palette color) * fixed freeze in blue brinstar caused by Varia's custom PLM not being filled with proper Multiworld PLM address (altLocsAddresses) * fixed start item x-ray HUD display * Fixed start items being sent by the server (is all handled in ROM) Start items are now not removed from itempool anymore Nothing Item is now local_items so no player will ever pickup Nothing. Doing so reduces contribution of this world to the Multiworld the more Nothing there is though. Fixed crash (and possibly passing but broken) at generation where the static list of IPSPatches used by all SM worlds was being modified * fixed settings that could be applied to any SM players * fixed auth to server only using player name (now does as ALTTP to authenticate) * - fixed End Credits broken text * added non SM item name display * added all supported SM options in playerSettings.yaml * fixed locations needing a list of parent regions (now generate a region for each location with one-way exits to each (previously) parent region did some cleaning (mainly reverts on unnecessary core classes * minor setting fixes and tweaks - merged Area and lightArea settings - made missileQty, superQty and powerBombQty use value from 10 to 90 and divide value by float(10) when generating - fixed inverted layoutPatch setting * added option start_inventory_removes_from_pool fixed option names formatting fixed lint errors small code and repo cleanup * Hopefully fixed ROR2 that could not send any items * - fixed missing required change to ROR2 * fixed 0 hp when respawning without having ever saved (start items were not updating the save checksum) * fixed typo with doors_colors_rando * fixed checksum * added custom sprites for off-world items (progression or not) the original AP sprite was made with PierRoulette's SM Item Sprite Utility by ijwu * - added missing change following upstream merge - changed patch filename extension from apbp to apm3 so patch can be used with the new client * added morph placement options: early means local and sphere 1 * fixed failing unit tests * - fixed broken custom_preset options * - big cleanup to remove unnecessary or unsupported features * - more cleanup * - moved sm_randomizer_rom and all always applied patches into an external project that outputs basepatch.ips - small cleanup * - added comment to refer to project for generating basepatch.ips (https://github.com/lordlou/SMBasepatch) * fixed g4_skip patch that can be not applied if hud is enabled * - fixed off world sprite that can have broken graphics (restricted to use only first 2 palette) * - updated basepatch to reflect g4_skip removal - moved more asm files to SMBasepatch project * - tourian grey doors at baby metroid are now always flashing (allowing to go back if needed) * fixed wrong path if using built as exe * - cleaned exposed maxDifficulty options - removed always enabled Knows * Merged LttPClient and SMClient into SNIClient * added varia_custom Preset Option that fetch a preset (read from a new varia_custom_preset Option) from varia's web service * small doc precision * - added death_link support - fixed broken Goal Completion - post merge fix * - removed now useless presets * - fixed bad internal mapping with maxDiff - increases maxDiff if only Bosses is preventing beating the game * - added support for lowercase custom preset sections (knows, settings and controller) - fixed controller settings not applying to ROM * - fixed death loop when dying with Door rando, bomb or speed booster as starting items - varia's backup save should now be usable (automatically enabled when doing door rando) * -added docstring for generated yaml * fixed bad merge * fixed broken infinity max difficulty * commented debug prints * adjusted credits to mark progression speed and difficulty as Non Available * added support for more than 255 players (will print Archipelago for higher player number) * fixed missing cleanup * added support for 65535 different player names in ROM * fixed generations failing when only bosses are unreachable * - replaced setting maxDiff to infinity with a bool only affecting boss logics if only bosses are left to finish * fixed failling generations when using 'fun' settings Accessibility checks are forced to 'items' if restricted locations are used by VARIA following usage of 'fun' settings * fixed debug logger * removed unsupported "suits_restriction" option * fixed generations failing when only bosses are unreachable (using a less intrusive approach for AP) * - fixed deathlink emptying reserves - added death_link_survive option that lets player survive when receiving a deathlink if the have non-empty reserves * - merged death_link and death_link_survive options * fixed death_link * added a fallback default starting location instead of failing generation if an invalid one was chosen * added Nothing and NoEnergy as hint blacklist added missing NoEnergy as local items and removed it from progression * SM Varia can now generate without ROM * removed stage_assert_generate
2025-01-10 15:46:17 -05:00
#self.romFileName = romFileName
self.patchAccess = PatchAccess()
self.race = None
SM generate without rom (#3460) * - SM now displays message when getting an item outside for someone else (fills ROM item table) This is dependant on modifications done to sm_randomizer_rom project * First working MultiWorld SM * some missing things: - player name inject in ROM and get in client - end game get from ROM in client - send self item to server - add player names table in ROM * replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better) * - reenabled balancing * post rebase fixes * updated SmClient.py * + added VariaRandomizer LICENSE * + added sm_randomizer_rom project (which builds sm.ips) * Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning * properly revert change made to CollectionState and more cleaning * Fixed multiworld support patch not working with VariaRandomizer's * missing file commit * Fixed syntax error in unused code to satisfy Linter * Revert "Fixed multiworld support patch not working with VariaRandomizer's" This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b. * many fixes and improovement - fixed seeded generation - fixed broken logic when more than one SM world - added missing rules for inter-area transitions - added basic patch presence for logic - added DoorManager init call to reflect present patches for logic - moved CollectionState addition out of BaseClasses into SM world - added condition to apply progitempool presorting only if SM world is present - set Bosses item id to None to prevent them going into multidata - now use get_game_players * first working (most of the time) progression generation for SM using VariaRandomizer's rules, items, locations and accessPoint (as regions) * first working single-world randomized SM rom patches * - SM now displays message when getting an item outside for someone else (fills ROM item table) This is dependant on modifications done to sm_randomizer_rom project * First working MultiWorld SM * some missing things: - player name inject in ROM and get in client - end game get from ROM in client - send self item to server - add player names table in ROM * replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better) * - reenabled balancing * post rebase fixes * updated SmClient.py * + added VariaRandomizer LICENSE * + added sm_randomizer_rom project (which builds sm.ips) * Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning * properly revert change made to CollectionState and more cleaning * Fixed multiworld support patch not working with VariaRandomizer's * missing file commit * Fixed syntax error in unused code to satisfy Linter * Revert "Fixed multiworld support patch not working with VariaRandomizer's" This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b. * many fixes and improovement - fixed seeded generation - fixed broken logic when more than one SM world - added missing rules for inter-area transitions - added basic patch presence for logic - added DoorManager init call to reflect present patches for logic - moved CollectionState addition out of BaseClasses into SM world - added condition to apply progitempool presorting only if SM world is present - set Bosses item id to None to prevent them going into multidata - now use get_game_players * Fixed multiworld support patch not working with VariaRandomizer's Added stage_fill_hook to set morph first in progitempool Added back VariaRandomizer's standard patches * + added missing files from variaRandomizer project * + added missing variaRandomizer files (custom sprites) + started integrating VariaRandomizer options (WIP) * Some fixes for player and server name display - fixed player name of 16 characters reading too far in SM client - fixed 12 bytes SM player name limit (now 16) - fixed server name not being displayed in SM when using server cheat ( now displays RECEIVED FROM ARCHIPELAGO) - request: temporarly changed default seed names displayed in SM main menu to OWTCH * Fixed Goal completion not triggering in smClient * integrated VariaRandomizer's options into AP (WIP) - startAP is working - door rando is working - skillset is working * - fixed itemsounds.ips crash by always including nofanfare.ips into multiworld.ips (itemsounds is now always applied and "itemsounds" preset must always be "off") * skillset are now instanced per player instead of being a singleton class * RomPatches are now instanced per player instead of being a singleton class * DoorManager is now instanced per player instead of being a singleton class * - fixed the last bugs that prevented generation of >1 SM world * fixed crash when no skillset preset is specified in randoPreset (default to "casual") * maxDifficulty support and itemsounds removal - added support for maxDifficulty - removed itemsounds patch as its always applied from multiworld patch for now * Fixed bad merge * Post merge adaptation * fixed player name length fix that got lost with the merge * fixed generation with other game type than SM * added default randoPreset json for SM in playerSettings.yaml * fixed broken SM client following merge * beautified json skillset presets * Fixed ArchipelagoSmClient not building * Fixed conflict between mutliworld patch and beam_doors_plms patch - doorsColorsRando now working * SM generation now outputs APBP - Fixed paths for patches and presets when frozen * added missing file and fixed multithreading issue * temporarily set data_version = 0 * more work - added support for AP starting items - fixed client crash with gamemode being None - patch.py "compatible_version" is now 3 * commited missing asm files fixed start item reserve breaking game (was using bad write offset when patching) * Nothing item are now handled game-side. the game will now skip displaying a message box for received Nothing item (but the client will still receive it). fixed crash in SMClient when loosing connection to SNI * fixed No Energy Item missing its ID fixed Plando * merge post fixes * fixed start item Grapple, XRay and Reserve HUD, as well as graphic beams (except ice palette color) * fixed freeze in blue brinstar caused by Varia's custom PLM not being filled with proper Multiworld PLM address (altLocsAddresses) * fixed start item x-ray HUD display * Fixed start items being sent by the server (is all handled in ROM) Start items are now not removed from itempool anymore Nothing Item is now local_items so no player will ever pickup Nothing. Doing so reduces contribution of this world to the Multiworld the more Nothing there is though. Fixed crash (and possibly passing but broken) at generation where the static list of IPSPatches used by all SM worlds was being modified * fixed settings that could be applied to any SM players * fixed auth to server only using player name (now does as ALTTP to authenticate) * - fixed End Credits broken text * added non SM item name display * added all supported SM options in playerSettings.yaml * fixed locations needing a list of parent regions (now generate a region for each location with one-way exits to each (previously) parent region did some cleaning (mainly reverts on unnecessary core classes * minor setting fixes and tweaks - merged Area and lightArea settings - made missileQty, superQty and powerBombQty use value from 10 to 90 and divide value by float(10) when generating - fixed inverted layoutPatch setting * added option start_inventory_removes_from_pool fixed option names formatting fixed lint errors small code and repo cleanup * Hopefully fixed ROR2 that could not send any items * - fixed missing required change to ROR2 * fixed 0 hp when respawning without having ever saved (start items were not updating the save checksum) * fixed typo with doors_colors_rando * fixed checksum * added custom sprites for off-world items (progression or not) the original AP sprite was made with PierRoulette's SM Item Sprite Utility by ijwu * - added missing change following upstream merge - changed patch filename extension from apbp to apm3 so patch can be used with the new client * added morph placement options: early means local and sphere 1 * fixed failing unit tests * - fixed broken custom_preset options * - big cleanup to remove unnecessary or unsupported features * - more cleanup * - moved sm_randomizer_rom and all always applied patches into an external project that outputs basepatch.ips - small cleanup * - added comment to refer to project for generating basepatch.ips (https://github.com/lordlou/SMBasepatch) * fixed g4_skip patch that can be not applied if hud is enabled * - fixed off world sprite that can have broken graphics (restricted to use only first 2 palette) * - updated basepatch to reflect g4_skip removal - moved more asm files to SMBasepatch project * - tourian grey doors at baby metroid are now always flashing (allowing to go back if needed) * fixed wrong path if using built as exe * - cleaned exposed maxDifficulty options - removed always enabled Knows * Merged LttPClient and SMClient into SNIClient * added varia_custom Preset Option that fetch a preset (read from a new varia_custom_preset Option) from varia's web service * small doc precision * - added death_link support - fixed broken Goal Completion - post merge fix * - removed now useless presets * - fixed bad internal mapping with maxDiff - increases maxDiff if only Bosses is preventing beating the game * - added support for lowercase custom preset sections (knows, settings and controller) - fixed controller settings not applying to ROM * - fixed death loop when dying with Door rando, bomb or speed booster as starting items - varia's backup save should now be usable (automatically enabled when doing door rando) * -added docstring for generated yaml * fixed bad merge * fixed broken infinity max difficulty * commented debug prints * adjusted credits to mark progression speed and difficulty as Non Available * added support for more than 255 players (will print Archipelago for higher player number) * fixed missing cleanup * added support for 65535 different player names in ROM * fixed generations failing when only bosses are unreachable * - replaced setting maxDiff to infinity with a bool only affecting boss logics if only bosses are left to finish * fixed failling generations when using 'fun' settings Accessibility checks are forced to 'items' if restricted locations are used by VARIA following usage of 'fun' settings * fixed debug logger * removed unsupported "suits_restriction" option * fixed generations failing when only bosses are unreachable (using a less intrusive approach for AP) * - fixed deathlink emptying reserves - added death_link_survive option that lets player survive when receiving a deathlink if the have non-empty reserves * - merged death_link and death_link_survive options * fixed death_link * added a fallback default starting location instead of failing generation if an invalid one was chosen * added Nothing and NoEnergy as hint blacklist added missing NoEnergy as local items and removed it from progression * SM Varia can now generate without ROM * removed stage_assert_generate
2025-01-10 15:46:17 -05:00
self.romFile = FakeROM()
#if magic is not None:
# from rom.race_mode import RaceModePatcher
# self.race = RaceModePatcher(self, magic)
# IPS_Patch objects list
self.ipsPatches = []
# loc name to alternate address. we still write to original
# address to help the RomReader.
self.altLocsAddresses = {}
# specific fixes for area rando connections
self.roomConnectionSpecific = {
# fix scrolling sky when transitioning to west ocean
0x93fe: self.patchWestOcean
}
self.doorConnectionSpecific = {
# get out of kraid room: reload CRE
0x91ce: self.forceRoomCRE,
# get out of croc room: reload CRE
0x93ea: self.forceRoomCRE
}
self.player = player
def patchRom(self):
self.applyIPSPatches()
self.commitIPS()
def end(self):
self.romFile.fillToNextBank()
self.romFile.close()
def writeItemCode(self, item, visibility, address):
itemCode = ItemManager.getItemTypeCode(item, visibility)
self.writePlmWord(itemCode, address)
def writePlmWord(self, word, address):
if self.race is None:
self.romFile.writeWord(word, address)
else:
self.race.writePlmWord(word, address)
def getLocAddresses(self, loc):
ret = [loc.Address]
if loc.Name in self.altLocsAddresses:
ret.append(self.altLocsAddresses[loc.Name])
return ret
def writeItem(self, itemLoc):
loc = itemLoc.Location
if loc.isBoss():
raise ValueError('Cannot write Boss location')
#print('write ' + itemLoc.Item.Type + ' at ' + loc.Name)
for addr in self.getLocAddresses(loc):
self.writeItemCode(itemLoc.Item, loc.Visibility, addr)
def writeItemsLocs(self, itemLocs):
self.nItems = 0
for itemLoc in itemLocs:
loc = itemLoc.Location
item = itemLoc.Item
if loc.isBoss():
continue
self.writeItem(itemLoc)
if item.Category != 'Nothing':
if not loc.restricted:
self.nItems += 1
if loc.Name == 'Morphing Ball':
self.patchMorphBallEye(item)
def writeSplitLocs(self, split, itemLocs, progItemLocs):
majChozoCheck = lambda itemLoc: itemLoc.Item.Class == split and itemLoc.Location.isClass(split)
fullCheck = lambda itemLoc: itemLoc.Location.Id is not None and itemLoc.Location.BossItemType is None
splitChecks = {
'Full': fullCheck,
'Scavenger': fullCheck,
'Major': majChozoCheck,
'Chozo': majChozoCheck,
'FullWithHUD': lambda itemLoc: itemLoc.Item.Category not in ['Energy', 'Ammo', 'Boss', 'MiniBoss']
}
itemLocCheck = lambda itemLoc: itemLoc.Item.Category != "Nothing" and splitChecks[split](itemLoc)
for area,addr in locIdsByAreaAddresses.items():
locs = [il.Location for il in itemLocs if itemLocCheck(il) and il.Location.GraphArea == area and not il.Location.restricted]
self.log.debug("writeSplitLocs. area="+area)
self.log.debug(str([loc.Name for loc in locs]))
self.romFile.seek(addr)
for loc in locs:
self.romFile.writeByte(loc.Id)
self.romFile.writeByte(0xff)
if split == "Scavenger":
# write required major item order
self.romFile.seek(Addresses.getOne('scavengerOrder'))
for itemLoc in progItemLocs:
self.romFile.writeWord((itemLoc.Location.Id << 8) | itemLoc.Location.HUD)
# bogus loc ID | "HUNT OVER" index
self.romFile.writeWord(0xff11)
# fill remaining list with 0xFFFF to avoid issue with plandomizer having less items than in the base seed
for i in range(18-len(progItemLocs)):
self.romFile.writeWord(0xffff)
# trigger morph eye enemy on whatever item we put there,
# not just morph ball
def patchMorphBallEye(self, item):
# print('Eye item = ' + item.Type)
isAmmo = item.Category == 'Ammo'
# category to check
if ItemManager.isBeam(item):
cat = 0xA8 # collected beams
elif item.Type == 'ETank':
cat = 0xC4 # max health
elif item.Type == 'Reserve':
cat = 0xD4 # max reserves
elif item.Type == 'Missile':
cat = 0xC8 # max missiles
elif item.Type == 'Super':
cat = 0xCC # max supers
elif item.Type == 'PowerBomb':
cat = 0xD0 # max PBs
else:
cat = 0xA4 # collected items
# comparison/branch instruction
# the branch is taken if we did NOT collect item yet
if item.Category == 'Energy' or isAmmo:
comp = 0xC9 # CMP (immediate)
branch = 0x30 # BMI
else:
comp = 0x89 # BIT (immediate)
branch = 0xF0 # BEQ
# what to compare to
if item.Type == 'ETank':
operand = 0x65 # < 100
elif item.Type == 'Reserve' or isAmmo:
operand = 0x1 # < 1
elif ItemManager.isBeam(item):
operand = item.BeamBits
else:
operand = item.ItemBits
self.patchMorphBallCheck(snes_to_pc(0xa890e6), cat, comp, operand, branch) # eye main AI
self.patchMorphBallCheck(snes_to_pc(0xa8e8b2), cat, comp, operand, branch) # head main AI
def patchMorphBallCheck(self, offset, cat, comp, operand, branch):
# actually patch enemy AI
self.romFile.writeByte(cat, offset)
self.romFile.writeByte(comp, offset+2)
self.romFile.writeWord(operand)
self.romFile.writeByte(branch)
def writeItemsNumber(self):
# write total number of actual items for item percentage patch (patch the patch)
for addr in Addresses.getAll('totalItems'):
self.romFile.writeByte(self.nItems, addr)
# for X% collected items objectives, precompute values and write them in objectives functions
for percent, addr in zip([25, 50, 75, 100], Addresses.getAll('totalItemsPercent')):
self.romFile.writeWord(ceil((self.nItems * percent)/100), addr)
def addIPSPatches(self, patches):
for patchName in patches:
self.applyIPSPatch(patchName)
def applyIPSPatches(self):
try:
# apply standard patches
stdPatches = []
plms = []
stdPatches += RomPatcher.IPSPatches['Standard'][:]
if not self.settings["layout"]:
# when disabling anti softlock protection also disable doors indicators
stdPatches.remove('door_indicators_plms.ips')
if self.race is not None:
stdPatches.append('race_mode_post.ips')
if self.settings["suitsMode"] != "Balanced":
stdPatches.remove('Removes_Gravity_Suit_heat_protection')
if self.settings["suitsMode"] == "Progressive":
stdPatches.append('progressive_suits.ips')
if self.settings["nerfedCharge"] == True:
stdPatches.append('nerfed_charge.ips')
if self.settings["nerfedRainbowBeam"] == True:
stdPatches.append('nerfed_rainbow_beam.ips')
if self.settings["boss"] == True or self.settings["area"] == True:
stdPatches += ["WS_Main_Open_Grey", "WS_Save_Active"]
plms.append('WS_Save_Blinking_Door')
if self.settings["boss"] == True:
stdPatches.append("Phantoon_Eye_Door")
# rolling saves is not required anymore since the addition of fast_save_reload
# also, both arent completely compatible as-is
#if (self.settings["area"] == True
# or self.settings["doorsColorsRando"] == True
# or not GraphUtils.isStandardStart(self.settings["startLocation"])):
# stdPatches.append("Enable_Backup_Saves")
if 'varia_hud.ips' in self.settings["optionalPatches"]:
# varia hud can make demos glitch out
self.applyIPSPatch("no_demo.ips")
for patchName in stdPatches:
self.applyIPSPatch(patchName)
if not self.settings["vanillaObjectives"]:
self.applyIPSPatch("Objectives_sfx")
# show objectives and Tourian status in a shortened intro sequence
# if not full vanilla objectives+tourian
if not self.settings["vanillaObjectives"] or self.settings["tourian"] != "Vanilla":
self.applyIPSPatch("Restore_Intro") # important to apply this after new_game.ips
self.applyIPSPatch("intro_text.ips")
if self.settings["layout"]:
# apply layout patches
for patchName in RomPatcher.IPSPatches['Layout']:
self.applyIPSPatch(patchName)
if self.settings["variaTweaks"]:
# VARIA tweaks
for patchName in RomPatcher.IPSPatches['VariaTweaks']:
self.applyIPSPatch(patchName)
if (self.settings["majorsSplit"] == 'Scavenger'
and any(il for il in self.settings["progItemLocs"] if il.Location.Name == "Ridley")):
# ridley as scav loc
self.applyIPSPatch("Blinking[RidleyRoomIn]")
# apply optional patches
for patchName in self.settings["optionalPatches"]:
self.applyIPSPatch(patchName)
# random escape
if self.settings["escapeAttr"] is not None:
for patchName in RomPatcher.IPSPatches['Escape']:
self.applyIPSPatch(patchName)
# animals and timer
self.applyEscapeAttributes(self.settings["escapeAttr"], plms)
# apply area patches
if self.settings["area"] == True:
areaPatches = list(RomPatcher.IPSPatches['Area'])
if not self.settings["areaLayout"]:
for p in ['area_rando_layout.ips', 'Sponge_Bath_Blinking_Door', 'east_ocean.ips', 'aqueduct_bomb_blocks.ips']:
areaPatches.remove(p)
areaPatches.append('area_rando_layout_base.ips')
for patchName in areaPatches:
self.applyIPSPatch(patchName)
else:
self.applyIPSPatch('area_ids_alt.ips')
if self.settings["boss"] == True:
for patchName in RomPatcher.IPSPatches['Bosses']:
self.applyIPSPatch(patchName)
if self.settings["minimizerN"] is not None:
self.applyIPSPatch('minimizer_bosses.ips')
if self.settings["tourian"] == "Fast":
for patchName in RomPatcher.IPSPatches['MinimizerTourian']:
self.applyIPSPatch(patchName)
elif self.settings["tourian"] == "Disabled":
self.applyIPSPatch("Escape_Trigger")
doors = self.getStartDoors(plms, self.settings["area"], self.settings["minimizerN"])
if self.settings["doorsColorsRando"] == True:
for patchName in RomPatcher.IPSPatches['DoorsColors']:
self.applyIPSPatch(patchName)
self.writeDoorsColor(doors, self.player)
if self.settings["layout"]:
self.writeDoorIndicators(plms, self.settings["area"], self.settings["doorsColorsRando"])
self.applyStartAP(self.settings["startLocation"], plms, doors)
self.applyPLMs(plms)
except Exception as e:
SM generate without rom (#3460) * - SM now displays message when getting an item outside for someone else (fills ROM item table) This is dependant on modifications done to sm_randomizer_rom project * First working MultiWorld SM * some missing things: - player name inject in ROM and get in client - end game get from ROM in client - send self item to server - add player names table in ROM * replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better) * - reenabled balancing * post rebase fixes * updated SmClient.py * + added VariaRandomizer LICENSE * + added sm_randomizer_rom project (which builds sm.ips) * Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning * properly revert change made to CollectionState and more cleaning * Fixed multiworld support patch not working with VariaRandomizer's * missing file commit * Fixed syntax error in unused code to satisfy Linter * Revert "Fixed multiworld support patch not working with VariaRandomizer's" This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b. * many fixes and improovement - fixed seeded generation - fixed broken logic when more than one SM world - added missing rules for inter-area transitions - added basic patch presence for logic - added DoorManager init call to reflect present patches for logic - moved CollectionState addition out of BaseClasses into SM world - added condition to apply progitempool presorting only if SM world is present - set Bosses item id to None to prevent them going into multidata - now use get_game_players * first working (most of the time) progression generation for SM using VariaRandomizer's rules, items, locations and accessPoint (as regions) * first working single-world randomized SM rom patches * - SM now displays message when getting an item outside for someone else (fills ROM item table) This is dependant on modifications done to sm_randomizer_rom project * First working MultiWorld SM * some missing things: - player name inject in ROM and get in client - end game get from ROM in client - send self item to server - add player names table in ROM * replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better) * - reenabled balancing * post rebase fixes * updated SmClient.py * + added VariaRandomizer LICENSE * + added sm_randomizer_rom project (which builds sm.ips) * Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning * properly revert change made to CollectionState and more cleaning * Fixed multiworld support patch not working with VariaRandomizer's * missing file commit * Fixed syntax error in unused code to satisfy Linter * Revert "Fixed multiworld support patch not working with VariaRandomizer's" This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b. * many fixes and improovement - fixed seeded generation - fixed broken logic when more than one SM world - added missing rules for inter-area transitions - added basic patch presence for logic - added DoorManager init call to reflect present patches for logic - moved CollectionState addition out of BaseClasses into SM world - added condition to apply progitempool presorting only if SM world is present - set Bosses item id to None to prevent them going into multidata - now use get_game_players * Fixed multiworld support patch not working with VariaRandomizer's Added stage_fill_hook to set morph first in progitempool Added back VariaRandomizer's standard patches * + added missing files from variaRandomizer project * + added missing variaRandomizer files (custom sprites) + started integrating VariaRandomizer options (WIP) * Some fixes for player and server name display - fixed player name of 16 characters reading too far in SM client - fixed 12 bytes SM player name limit (now 16) - fixed server name not being displayed in SM when using server cheat ( now displays RECEIVED FROM ARCHIPELAGO) - request: temporarly changed default seed names displayed in SM main menu to OWTCH * Fixed Goal completion not triggering in smClient * integrated VariaRandomizer's options into AP (WIP) - startAP is working - door rando is working - skillset is working * - fixed itemsounds.ips crash by always including nofanfare.ips into multiworld.ips (itemsounds is now always applied and "itemsounds" preset must always be "off") * skillset are now instanced per player instead of being a singleton class * RomPatches are now instanced per player instead of being a singleton class * DoorManager is now instanced per player instead of being a singleton class * - fixed the last bugs that prevented generation of >1 SM world * fixed crash when no skillset preset is specified in randoPreset (default to "casual") * maxDifficulty support and itemsounds removal - added support for maxDifficulty - removed itemsounds patch as its always applied from multiworld patch for now * Fixed bad merge * Post merge adaptation * fixed player name length fix that got lost with the merge * fixed generation with other game type than SM * added default randoPreset json for SM in playerSettings.yaml * fixed broken SM client following merge * beautified json skillset presets * Fixed ArchipelagoSmClient not building * Fixed conflict between mutliworld patch and beam_doors_plms patch - doorsColorsRando now working * SM generation now outputs APBP - Fixed paths for patches and presets when frozen * added missing file and fixed multithreading issue * temporarily set data_version = 0 * more work - added support for AP starting items - fixed client crash with gamemode being None - patch.py "compatible_version" is now 3 * commited missing asm files fixed start item reserve breaking game (was using bad write offset when patching) * Nothing item are now handled game-side. the game will now skip displaying a message box for received Nothing item (but the client will still receive it). fixed crash in SMClient when loosing connection to SNI * fixed No Energy Item missing its ID fixed Plando * merge post fixes * fixed start item Grapple, XRay and Reserve HUD, as well as graphic beams (except ice palette color) * fixed freeze in blue brinstar caused by Varia's custom PLM not being filled with proper Multiworld PLM address (altLocsAddresses) * fixed start item x-ray HUD display * Fixed start items being sent by the server (is all handled in ROM) Start items are now not removed from itempool anymore Nothing Item is now local_items so no player will ever pickup Nothing. Doing so reduces contribution of this world to the Multiworld the more Nothing there is though. Fixed crash (and possibly passing but broken) at generation where the static list of IPSPatches used by all SM worlds was being modified * fixed settings that could be applied to any SM players * fixed auth to server only using player name (now does as ALTTP to authenticate) * - fixed End Credits broken text * added non SM item name display * added all supported SM options in playerSettings.yaml * fixed locations needing a list of parent regions (now generate a region for each location with one-way exits to each (previously) parent region did some cleaning (mainly reverts on unnecessary core classes * minor setting fixes and tweaks - merged Area and lightArea settings - made missileQty, superQty and powerBombQty use value from 10 to 90 and divide value by float(10) when generating - fixed inverted layoutPatch setting * added option start_inventory_removes_from_pool fixed option names formatting fixed lint errors small code and repo cleanup * Hopefully fixed ROR2 that could not send any items * - fixed missing required change to ROR2 * fixed 0 hp when respawning without having ever saved (start items were not updating the save checksum) * fixed typo with doors_colors_rando * fixed checksum * added custom sprites for off-world items (progression or not) the original AP sprite was made with PierRoulette's SM Item Sprite Utility by ijwu * - added missing change following upstream merge - changed patch filename extension from apbp to apm3 so patch can be used with the new client * added morph placement options: early means local and sphere 1 * fixed failing unit tests * - fixed broken custom_preset options * - big cleanup to remove unnecessary or unsupported features * - more cleanup * - moved sm_randomizer_rom and all always applied patches into an external project that outputs basepatch.ips - small cleanup * - added comment to refer to project for generating basepatch.ips (https://github.com/lordlou/SMBasepatch) * fixed g4_skip patch that can be not applied if hud is enabled * - fixed off world sprite that can have broken graphics (restricted to use only first 2 palette) * - updated basepatch to reflect g4_skip removal - moved more asm files to SMBasepatch project * - tourian grey doors at baby metroid are now always flashing (allowing to go back if needed) * fixed wrong path if using built as exe * - cleaned exposed maxDifficulty options - removed always enabled Knows * Merged LttPClient and SMClient into SNIClient * added varia_custom Preset Option that fetch a preset (read from a new varia_custom_preset Option) from varia's web service * small doc precision * - added death_link support - fixed broken Goal Completion - post merge fix * - removed now useless presets * - fixed bad internal mapping with maxDiff - increases maxDiff if only Bosses is preventing beating the game * - added support for lowercase custom preset sections (knows, settings and controller) - fixed controller settings not applying to ROM * - fixed death loop when dying with Door rando, bomb or speed booster as starting items - varia's backup save should now be usable (automatically enabled when doing door rando) * -added docstring for generated yaml * fixed bad merge * fixed broken infinity max difficulty * commented debug prints * adjusted credits to mark progression speed and difficulty as Non Available * added support for more than 255 players (will print Archipelago for higher player number) * fixed missing cleanup * added support for 65535 different player names in ROM * fixed generations failing when only bosses are unreachable * - replaced setting maxDiff to infinity with a bool only affecting boss logics if only bosses are left to finish * fixed failling generations when using 'fun' settings Accessibility checks are forced to 'items' if restricted locations are used by VARIA following usage of 'fun' settings * fixed debug logger * removed unsupported "suits_restriction" option * fixed generations failing when only bosses are unreachable (using a less intrusive approach for AP) * - fixed deathlink emptying reserves - added death_link_survive option that lets player survive when receiving a deathlink if the have non-empty reserves * - merged death_link and death_link_survive options * fixed death_link * added a fallback default starting location instead of failing generation if an invalid one was chosen * added Nothing and NoEnergy as hint blacklist added missing NoEnergy as local items and removed it from progression * SM Varia can now generate without ROM * removed stage_assert_generate
2025-01-10 15:46:17 -05:00
raise Exception("Error patching. ({})".format(e))
def applyIPSPatch(self, patchName, patchDict=None, ipsDir=None):
if patchDict is None:
patchDict = self.patchAccess.getDictPatches()
# print("Apply patch {}".format(patchName))
if patchName in patchDict:
patch = IPS_Patch(patchDict[patchName])
else:
# look for ips file
if ipsDir is None:
patch = IPS_Patch.load(self.patchAccess.getPatchPath(patchName))
else:
patch = IPS_Patch.load(os.path.join(appDir, ipsDir, patchName))
self.ipsPatches.append(patch)
def applyIPSPatchDict(self, patchDict):
for patchName in patchDict.keys():
# print("Apply patch {}".format(patchName))
patch = IPS_Patch(patchDict[patchName])
self.ipsPatches.append(patch)
def getStartDoors(self, plms, area, minimizerN):
doors = [0x10] # red brin elevator
def addBlinking(name):
key = 'Blinking[{}]'.format(name)
if key in self.patchAccess.getDictPatches():
self.applyIPSPatch(key)
if key in self.patchAccess.getAdditionalPLMs():
plms.append(key)
if area == True:
plms += ['Maridia Sand Hall Seal', "Save_Main_Street", "Save_Crab_Shaft"]
for accessPoint in Logic.accessPoints:
if accessPoint.Internal == True or accessPoint.Boss == True:
continue
addBlinking(accessPoint.Name)
addBlinking("West Sand Hall Left")
addBlinking("Below Botwoon Energy Tank Right")
if minimizerN is not None:
# add blinking doors inside and outside boss rooms
for accessPoint in Logic.accessPoints:
if accessPoint.Boss == True:
addBlinking(accessPoint.Name)
return doors
def applyStartAP(self, apName, plms, doors):
ap = getAccessPoint(apName)
# if start loc is not Ceres or Landing Site, or the ceiling loc picked up before morph loc,
# Zebes will be awake and morph loc item will disappear.
# this PLM ensures the item will be here whenever zebes awakes
plms.append('Morph_Zebes_Awake')
(w0, w1) = getWord(ap.Start['spawn'])
if 'doors' in ap.Start:
doors += ap.Start['doors']
doors.append(0x0)
addr = Addresses.getOne('startAP')
patch = [w0, w1] + doors
assert (addr + len(patch)) < addr + 0x10, "Stopped before new_game overwrite"
patchDict = {
'StartAP': {
addr: patch
},
}
self.applyIPSPatch('StartAP', patchDict)
# handle custom saves
if 'save' in ap.Start:
self.applyIPSPatch(ap.Start['save'])
plms.append(ap.Start['save'])
# handle optional rom patches
if 'rom_patches' in ap.Start:
for patch in ap.Start['rom_patches']:
self.applyIPSPatch(patch)
def applyEscapeAttributes(self, escapeAttr, plms):
# timer
escapeTimer = escapeAttr['Timer']
if escapeTimer is not None:
patchDict = { 'Escape_Timer': {} }
timerPatch = patchDict["Escape_Timer"]
def getTimerBytes(t):
minute = int(t / 60)
second = t % 60
minute = int(minute / 10) * 16 + minute % 10
second = int(second / 10) * 16 + second % 10
return [second, minute]
timerPatch[Addresses.getOne('escapeTimer')] = getTimerBytes(escapeTimer)
# timer table for Disabled Tourian escape
if 'TimerTable' in escapeAttr:
tableBytes = []
timerPatch[Addresses.getOne('escapeTimerTable')] = tableBytes
for area in graphAreas[1:-1]: # no Ceres or Tourian
t = escapeAttr['TimerTable'][area]
tableBytes += getTimerBytes(t)
self.applyIPSPatch('Escape_Timer', patchDict)
# animals door to open
if escapeAttr['Animals'] is not None:
escapeOpenPatches = {
'Green Brinstar Main Shaft Top Left':'Escape_Animals_Open_Brinstar',
'Business Center Mid Left':"Escape_Animals_Open_Norfair",
'Crab Hole Bottom Right':"Escape_Animals_Open_Maridia",
}
if escapeAttr['Animals'] in escapeOpenPatches:
plms.append("WS_Map_Grey_Door")
self.applyIPSPatch(escapeOpenPatches[escapeAttr['Animals']])
else:
plms.append("WS_Map_Grey_Door_Openable")
else:
plms.append("WS_Map_Grey_Door")
# optional patches (enemies, scavenger)
for patch in escapeAttr['patches']:
self.applyIPSPatch(patch)
# adds ad-hoc "IPS patches" for additional PLM tables
def applyPLMs(self, plms):
# compose a dict (room, state, door) => PLM array
# 'PLMs' being a 6 byte arrays
plmDict = {}
# we might need to update locations addresses on the fly
plmLocs = {} # room key above => loc name
additionalPLMs = self.patchAccess.getAdditionalPLMs()
for p in plms:
plm = additionalPLMs[p]
room = plm['room']
state = 0
if 'state' in plm:
state = plm['state']
door = 0
if 'door' in plm:
door = plm['door']
k = (room, state, door)
if k not in plmDict:
plmDict[k] = []
plmDict[k] += plm['plm_bytes_list']
if 'locations' in plm:
locList = plm['locations']
for locName, locIndex in locList:
plmLocs[(k, locIndex)] = locName
# make two patches out of this dict
plmTblAddr = Addresses.getOne('plmSpawnTable') # moves downwards
plmPatchData = []
roomTblAddr = Addresses.getOne('plmSpawnRoomTable') # moves upwards
roomPatchData = []
plmTblOffset = plmTblAddr
def appendPlmBytes(bytez):
nonlocal plmPatchData, plmTblOffset
plmPatchData += bytez
plmTblOffset += len(bytez)
def addRoomPatchData(bytez):
nonlocal roomPatchData, roomTblAddr
roomPatchData = bytez + roomPatchData
roomTblAddr -= len(bytez)
for roomKey, plmList in plmDict.items():
entryAddr = plmTblOffset
roomData = []
for i in range(len(plmList)):
plmBytes = plmList[i]
assert len(plmBytes) == 6, "Invalid PLM entry for roomKey " + str(roomKey) + ": PLM list len is " + str(len(plmBytes))
if (roomKey, i) in plmLocs:
self.altLocsAddresses[plmLocs[(roomKey, i)]] = plmTblOffset
appendPlmBytes(plmBytes)
appendPlmBytes([0x0, 0x0]) # list terminator
def appendRoomWord(w, data):
(w0, w1) = getWord(w)
data += [w0, w1]
for i in range(3):
appendRoomWord(roomKey[i], roomData)
appendRoomWord(entryAddr, roomData)
addRoomPatchData(roomData)
# write room table terminator
addRoomPatchData([0x0] * 8)
assert plmTblOffset < roomTblAddr, "Spawn PLM table overlap. PLM table offset is 0x%x, Room table address is 0x%x" % (plmTblOffset,roomTblAddr)
patchDict = {
"PLM_Spawn_Tables" : {
plmTblAddr: plmPatchData,
roomTblAddr: roomPatchData
}
}
self.applyIPSPatch("PLM_Spawn_Tables", patchDict)
def commitIPS(self):
self.romFile.ipsPatch(self.ipsPatches)
SM generate without rom (#3460) * - SM now displays message when getting an item outside for someone else (fills ROM item table) This is dependant on modifications done to sm_randomizer_rom project * First working MultiWorld SM * some missing things: - player name inject in ROM and get in client - end game get from ROM in client - send self item to server - add player names table in ROM * replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better) * - reenabled balancing * post rebase fixes * updated SmClient.py * + added VariaRandomizer LICENSE * + added sm_randomizer_rom project (which builds sm.ips) * Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning * properly revert change made to CollectionState and more cleaning * Fixed multiworld support patch not working with VariaRandomizer's * missing file commit * Fixed syntax error in unused code to satisfy Linter * Revert "Fixed multiworld support patch not working with VariaRandomizer's" This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b. * many fixes and improovement - fixed seeded generation - fixed broken logic when more than one SM world - added missing rules for inter-area transitions - added basic patch presence for logic - added DoorManager init call to reflect present patches for logic - moved CollectionState addition out of BaseClasses into SM world - added condition to apply progitempool presorting only if SM world is present - set Bosses item id to None to prevent them going into multidata - now use get_game_players * first working (most of the time) progression generation for SM using VariaRandomizer's rules, items, locations and accessPoint (as regions) * first working single-world randomized SM rom patches * - SM now displays message when getting an item outside for someone else (fills ROM item table) This is dependant on modifications done to sm_randomizer_rom project * First working MultiWorld SM * some missing things: - player name inject in ROM and get in client - end game get from ROM in client - send self item to server - add player names table in ROM * replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better) * - reenabled balancing * post rebase fixes * updated SmClient.py * + added VariaRandomizer LICENSE * + added sm_randomizer_rom project (which builds sm.ips) * Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning * properly revert change made to CollectionState and more cleaning * Fixed multiworld support patch not working with VariaRandomizer's * missing file commit * Fixed syntax error in unused code to satisfy Linter * Revert "Fixed multiworld support patch not working with VariaRandomizer's" This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b. * many fixes and improovement - fixed seeded generation - fixed broken logic when more than one SM world - added missing rules for inter-area transitions - added basic patch presence for logic - added DoorManager init call to reflect present patches for logic - moved CollectionState addition out of BaseClasses into SM world - added condition to apply progitempool presorting only if SM world is present - set Bosses item id to None to prevent them going into multidata - now use get_game_players * Fixed multiworld support patch not working with VariaRandomizer's Added stage_fill_hook to set morph first in progitempool Added back VariaRandomizer's standard patches * + added missing files from variaRandomizer project * + added missing variaRandomizer files (custom sprites) + started integrating VariaRandomizer options (WIP) * Some fixes for player and server name display - fixed player name of 16 characters reading too far in SM client - fixed 12 bytes SM player name limit (now 16) - fixed server name not being displayed in SM when using server cheat ( now displays RECEIVED FROM ARCHIPELAGO) - request: temporarly changed default seed names displayed in SM main menu to OWTCH * Fixed Goal completion not triggering in smClient * integrated VariaRandomizer's options into AP (WIP) - startAP is working - door rando is working - skillset is working * - fixed itemsounds.ips crash by always including nofanfare.ips into multiworld.ips (itemsounds is now always applied and "itemsounds" preset must always be "off") * skillset are now instanced per player instead of being a singleton class * RomPatches are now instanced per player instead of being a singleton class * DoorManager is now instanced per player instead of being a singleton class * - fixed the last bugs that prevented generation of >1 SM world * fixed crash when no skillset preset is specified in randoPreset (default to "casual") * maxDifficulty support and itemsounds removal - added support for maxDifficulty - removed itemsounds patch as its always applied from multiworld patch for now * Fixed bad merge * Post merge adaptation * fixed player name length fix that got lost with the merge * fixed generation with other game type than SM * added default randoPreset json for SM in playerSettings.yaml * fixed broken SM client following merge * beautified json skillset presets * Fixed ArchipelagoSmClient not building * Fixed conflict between mutliworld patch and beam_doors_plms patch - doorsColorsRando now working * SM generation now outputs APBP - Fixed paths for patches and presets when frozen * added missing file and fixed multithreading issue * temporarily set data_version = 0 * more work - added support for AP starting items - fixed client crash with gamemode being None - patch.py "compatible_version" is now 3 * commited missing asm files fixed start item reserve breaking game (was using bad write offset when patching) * Nothing item are now handled game-side. the game will now skip displaying a message box for received Nothing item (but the client will still receive it). fixed crash in SMClient when loosing connection to SNI * fixed No Energy Item missing its ID fixed Plando * merge post fixes * fixed start item Grapple, XRay and Reserve HUD, as well as graphic beams (except ice palette color) * fixed freeze in blue brinstar caused by Varia's custom PLM not being filled with proper Multiworld PLM address (altLocsAddresses) * fixed start item x-ray HUD display * Fixed start items being sent by the server (is all handled in ROM) Start items are now not removed from itempool anymore Nothing Item is now local_items so no player will ever pickup Nothing. Doing so reduces contribution of this world to the Multiworld the more Nothing there is though. Fixed crash (and possibly passing but broken) at generation where the static list of IPSPatches used by all SM worlds was being modified * fixed settings that could be applied to any SM players * fixed auth to server only using player name (now does as ALTTP to authenticate) * - fixed End Credits broken text * added non SM item name display * added all supported SM options in playerSettings.yaml * fixed locations needing a list of parent regions (now generate a region for each location with one-way exits to each (previously) parent region did some cleaning (mainly reverts on unnecessary core classes * minor setting fixes and tweaks - merged Area and lightArea settings - made missileQty, superQty and powerBombQty use value from 10 to 90 and divide value by float(10) when generating - fixed inverted layoutPatch setting * added option start_inventory_removes_from_pool fixed option names formatting fixed lint errors small code and repo cleanup * Hopefully fixed ROR2 that could not send any items * - fixed missing required change to ROR2 * fixed 0 hp when respawning without having ever saved (start items were not updating the save checksum) * fixed typo with doors_colors_rando * fixed checksum * added custom sprites for off-world items (progression or not) the original AP sprite was made with PierRoulette's SM Item Sprite Utility by ijwu * - added missing change following upstream merge - changed patch filename extension from apbp to apm3 so patch can be used with the new client * added morph placement options: early means local and sphere 1 * fixed failing unit tests * - fixed broken custom_preset options * - big cleanup to remove unnecessary or unsupported features * - more cleanup * - moved sm_randomizer_rom and all always applied patches into an external project that outputs basepatch.ips - small cleanup * - added comment to refer to project for generating basepatch.ips (https://github.com/lordlou/SMBasepatch) * fixed g4_skip patch that can be not applied if hud is enabled * - fixed off world sprite that can have broken graphics (restricted to use only first 2 palette) * - updated basepatch to reflect g4_skip removal - moved more asm files to SMBasepatch project * - tourian grey doors at baby metroid are now always flashing (allowing to go back if needed) * fixed wrong path if using built as exe * - cleaned exposed maxDifficulty options - removed always enabled Knows * Merged LttPClient and SMClient into SNIClient * added varia_custom Preset Option that fetch a preset (read from a new varia_custom_preset Option) from varia's web service * small doc precision * - added death_link support - fixed broken Goal Completion - post merge fix * - removed now useless presets * - fixed bad internal mapping with maxDiff - increases maxDiff if only Bosses is preventing beating the game * - added support for lowercase custom preset sections (knows, settings and controller) - fixed controller settings not applying to ROM * - fixed death loop when dying with Door rando, bomb or speed booster as starting items - varia's backup save should now be usable (automatically enabled when doing door rando) * -added docstring for generated yaml * fixed bad merge * fixed broken infinity max difficulty * commented debug prints * adjusted credits to mark progression speed and difficulty as Non Available * added support for more than 255 players (will print Archipelago for higher player number) * fixed missing cleanup * added support for 65535 different player names in ROM * fixed generations failing when only bosses are unreachable * - replaced setting maxDiff to infinity with a bool only affecting boss logics if only bosses are left to finish * fixed failling generations when using 'fun' settings Accessibility checks are forced to 'items' if restricted locations are used by VARIA following usage of 'fun' settings * fixed debug logger * removed unsupported "suits_restriction" option * fixed generations failing when only bosses are unreachable (using a less intrusive approach for AP) * - fixed deathlink emptying reserves - added death_link_survive option that lets player survive when receiving a deathlink if the have non-empty reserves * - merged death_link and death_link_survive options * fixed death_link * added a fallback default starting location instead of failing generation if an invalid one was chosen * added Nothing and NoEnergy as hint blacklist added missing NoEnergy as local items and removed it from progression * SM Varia can now generate without ROM * removed stage_assert_generate
2025-01-10 15:46:17 -05:00
self.ipsPatches = []
def writeSeed(self, seed):
random.seed(seed)
seedInfo = random.randint(0, 0xFFFF)
seedInfo2 = random.randint(0, 0xFFFF)
self.romFile.writeWord(seedInfo, snes_to_pc(0xdfff00))
self.romFile.writeWord(seedInfo2)
def writeMagic(self):
if self.race is not None:
self.race.writeMagic()
def writeMajorsSplit(self, majorsSplit):
address = Addresses.getOne('majorsSplit')
splits = {
'Chozo': 'Z',
'Major': 'M',
'FullWithHUD': 'H',
'Scavenger': 'S'
}
char = splits.get(majorsSplit, 'F')
self.romFile.writeByte(ord(char), address)
def getItemQty(self, itemLocs, itemType):
return len([il for il in itemLocs if il.Accessible and il.Item.Type == itemType])
def getMinorsDistribution(self, itemLocs):
dist = {}
minQty = 100
minors = ['Missile', 'Super', 'PowerBomb']
for m in minors:
# in vcr mode if the seed has stuck we may not have these items, return at least 1
q = float(max(self.getItemQty(itemLocs, m), 1))
dist[m] = {'Quantity' : q }
if q < minQty:
minQty = q
for m in minors:
dist[m]['Proportion'] = dist[m]['Quantity']/minQty
return dist
def getAmmoPct(self, minorsDist):
q = 0
for m,v in minorsDist.items():
q += v['Quantity']
return 100*q/66
def writeRandoSettings(self, settings, itemLocs):
dist = self.getMinorsDistribution(itemLocs)
totalAmmo = sum(d['Quantity'] for ammo,d in dist.items())
totalItemLocs = sum(1 for il in itemLocs if il.Accessible and not il.Location.isBoss())
totalNothing = sum(1 for il in itemLocs if il.Accessible and il.Item.Category == 'Nothing')
totalEnergy = self.getItemQty(itemLocs, 'ETank')+self.getItemQty(itemLocs, 'Reserve')
totalMajors = max(totalItemLocs - totalEnergy - totalAmmo - totalNothing, 0)
address = snes_to_pc(0xceb6c0)
value = "{:>2}".format(totalItemLocs)
line = " ITEM LOCATIONS %s " % value
self.writeCreditsStringBig(address, line, top=True)
address += 0x40
line = " item locations ............ %s " % value
self.writeCreditsStringBig(address, line, top=False)
address += 0x40
maj = "{:>2}".format(int(totalMajors))
htanks = "{:>2}".format(int(totalEnergy))
ammo = "{:>2}".format(int(totalAmmo))
blank = "{:>2}".format(int(totalNothing))
line = " MAJ %s EN %s AMMO %s BLANK %s " % (maj, htanks, ammo, blank)
self.writeCreditsStringBig(address, line, top=True)
address += 0x40
line = " maj %s en %s ammo %s blank %s " % (maj, htanks, ammo, blank)
self.writeCreditsStringBig(address, line, top=False)
address += 0x40
pbs = "{:>2}".format(int(dist['PowerBomb']['Quantity']))
miss = "{:>2}".format(int(dist['Missile']['Quantity']))
supers = "{:>2}".format(int(dist['Super']['Quantity']))
line = " AMMO PACKS MI %s SUP %s PB %s " % (miss, supers, pbs)
self.writeCreditsStringBig(address, line, top=True)
address += 0x40
line = " ammo packs mi %s sup %s pb %s " % (miss, supers, pbs)
self.writeCreditsStringBig(address, line, top=False)
address += 0x40
etanks = "{:>2}".format(int(self.getItemQty(itemLocs, 'ETank')))
reserves = "{:>2}".format(int(self.getItemQty(itemLocs, 'Reserve')))
line = " HEALTH TANKS E %s R %s " % (etanks, reserves)
self.writeCreditsStringBig(address, line, top=True)
address += 0x40
line = " health tanks ...... e %s r %s " % (etanks, reserves)
self.writeCreditsStringBig(address, line, top=False)
address += 0x80
value = " "+"NA" # settings.progSpeed.upper()
line = " PROGRESSION SPEED ....%s " % value.rjust(8, '.')
self.writeCreditsString(address, 0x04, line)
address += 0x40
line = " PROGRESSION DIFFICULTY %s " % value.rjust(7, '.') # settings.progDiff.upper()
self.writeCreditsString(address, 0x04, line)
address += 0x80 # skip item distrib title
param = (' SUITS RESTRICTION ........%s', 'Suits')
line = param[0] % ('. ON' if settings.restrictions[param[1]] == True else ' OFF')
self.writeCreditsString(address, 0x04, line)
address += 0x40
value = " "+settings.restrictions['Morph'].upper()
line = " MORPH PLACEMENT .....%s" % value.rjust(9, '.')
self.writeCreditsString(address, 0x04, line)
address += 0x40
for superFun in [(' SUPER FUN COMBAT .........%s', 'Combat'),
(' SUPER FUN MOVEMENT .......%s', 'Movement'),
(' SUPER FUN SUITS ..........%s', 'Suits')]:
line = superFun[0] % ('. ON' if superFun[1] in settings.superFun else ' OFF')
self.writeCreditsString(address, 0x04, line)
address += 0x40
value = "%.1f %.1f %.1f" % (dist['Missile']['Proportion'], dist['Super']['Proportion'], dist['PowerBomb']['Proportion'])
line = " AMMO DISTRIBUTION %s " % value
self.writeCreditsStringBig(address, line, top=True)
address += 0x40
line = " ammo distribution %s " % value
self.writeCreditsStringBig(address, line, top=False)
address += 0x40
# write ammo/energy pct
address = snes_to_pc(0xcebc40)
(ammoPct, energyPct) = (int(self.getAmmoPct(dist)), int(100*totalEnergy/18))
line = " AVAILABLE AMMO {:>3}% ENERGY {:>3}%".format(ammoPct, energyPct)
self.writeCreditsStringBig(address, line, top=True)
address += 0x40
line = " available ammo {:>3}% energy {:>3}%".format(ammoPct, energyPct)
self.writeCreditsStringBig(address, line, top=False)
def writeSpoiler(self, itemLocs, progItemLocs=None):
# keep only majors
fItemLocs = [il for il in itemLocs if il.Item.Category not in ['Ammo', 'Nothing', 'Energy', 'Boss']]
# add location of the first instance of each minor
for t in ['Missile', 'Super', 'PowerBomb']:
itLoc = None
if progItemLocs is not None:
itLoc = next((il for il in progItemLocs if il.Item.Type == t), None)
if itLoc is None:
itLoc = next((il for il in itemLocs if il.Item.Type == t), None)
if itLoc is not None: # in vcr mode if the seed has stucked we may not have these minors
fItemLocs.append(itLoc)
regex = re.compile(r"[^A-Z0-9\.,'!: ]+")
itemLocs = {}
for iL in fItemLocs:
itemLocs[iL.Item.Name] = iL.Location.Name
def prepareString(s, isItem=True):
s = s.upper()
# remove chars not displayable
s = regex.sub('', s)
# remove space before and after
s = s.strip()
# limit to 30 chars, add one space before
# pad to 32 chars
if isItem is True:
s = " " + s[0:30]
s = s.ljust(32)
else:
s = " " + s[0:30] + " "
s = " " + s.rjust(31, '.')
return s
isRace = self.race is not None
startCreditAddress = snes_to_pc(0xded240)
address = startCreditAddress
if isRace:
addr = address - 0x40
data = [0x007f, 0x007f, 0x007f, 0x007f, 0x007f, 0x007f, 0x007f, 0x007f, 0x007f, 0x1008, 0x1013, 0x1004, 0x100c, 0x007f, 0x100b, 0x100e, 0x1002, 0x1000, 0x1013, 0x1008, 0x100e, 0x100d, 0x1012, 0x007f, 0x007f, 0x007f, 0x007f, 0x007f, 0x007f, 0x007f, 0x007f, 0x007f]
for i in range(0x20):
w = data[i]
self.romFile.seek(addr)
self.race.writeWordMagic(w)
addr += 0x2
# standard item order
items = ["Missile", "Super Missile", "Power Bomb",
"Charge Beam", "Ice Beam", "Wave Beam", "Spazer", "Plasma Beam",
"Varia Suit", "Gravity Suit",
"Morph Ball", "Bomb", "Spring Ball", "Screw Attack",
"Hi-Jump Boots", "Space Jump", "Speed Booster",
"Grappling Beam", "X-Ray Scope"]
displayNames = {}
if progItemLocs is not None:
# reorder it with progression indices
prog = ord('A')
idx = 0
progNames = [il.Item.Name for il in progItemLocs if il.Item.Category != 'Boss']
for i in range(len(progNames)):
item = progNames[i]
if item in items and item not in displayNames:
items.remove(item)
items.insert(idx, item)
displayNames[item] = chr(prog + i) + ": " + item
idx += 1
for item in items:
# super fun removes items
if item not in itemLocs:
continue
display = item
if item in displayNames:
display = displayNames[item]
itemName = prepareString(display)
locationName = prepareString(itemLocs[item], isItem=False)
self.writeCreditsString(address, 0x04, itemName, isRace)
self.writeCreditsString((address + 0x40), 0x18, locationName, isRace)
address += 0x80
# we need 19 items displayed, if we've removed majors, add some blank text
while address < startCreditAddress + len(items)*0x80:
self.writeCreditsString(address, 0x04, prepareString(""), isRace)
self.writeCreditsString((address + 0x40), 0x18, prepareString(""), isRace)
address += 0x80
self.patchBytes(address, [0, 0, 0, 0], isRace)
def writeCreditsString(self, address, color, string, isRace=False):
array = [self.convertCreditsChar(color, char) for char in string]
self.patchBytes(address, array, isRace)
def writeCreditsStringBig(self, address, string, top=True):
array = [self.convertCreditsCharBig(char, top) for char in string]
self.patchBytes(address, array)
def convertCreditsChar(self, color, byte):
if byte == ' ':
ib = 0x7f
elif byte == '!':
ib = 0x1F
elif byte == ':':
ib = 0x1E
elif byte == '\\':
ib = 0x1D
elif byte == '_':
ib = 0x1C
elif byte == ',':
ib = 0x1B
elif byte == '.':
ib = 0x1A
else:
ib = ord(byte) - 0x41
if ib == 0x7F:
return 0x007F
else:
return (color << 8) + ib
def convertCreditsCharBig(self, byte, top=True):
# from: https://jathys.zophar.net/supermetroid/kejardon/TextFormat.txt
# 2-tile high characters:
# A-P = $XX20-$XX2F(TOP) and $XX30-$XX3F(BOTTOM)
# Q-Z = $XX40-$XX49(TOP) and $XX50-$XX59(BOTTOM)
# ' = $XX4A, $XX7F
# " = $XX4B, $XX7F
# . = $XX7F, $XX5A
# 0-9 = $XX60-$XX69(TOP) and $XX70-$XX79(BOTTOM)
# % = $XX6A, $XX7A
if byte == ' ':
ib = 0x7F
elif byte == "'":
if top == True:
ib = 0x4A
else:
ib = 0x7F
elif byte == '"':
if top == True:
ib = 0x4B
else:
ib = 0x7F
elif byte == '.':
if top == True:
ib = 0x7F
else:
ib = 0x5A
elif byte == '%':
if top == True:
ib = 0x6A
else:
ib = 0x7A
byte = ord(byte)
if byte >= ord('A') and byte <= ord('P'):
ib = byte - 0x21
elif byte >= ord('Q') and byte <= ord('Z'):
ib = byte - 0x11
elif byte >= ord('a') and byte <= ord('p'):
ib = byte - 0x31
elif byte >= ord('q') and byte <= ord('z'):
ib = byte - 0x21
elif byte >= ord('0') and byte <= ord('9'):
if top == True:
ib = byte + 0x30
else:
ib = byte + 0x40
return ib
def patchBytes(self, address, array, isRace=False):
self.romFile.seek(address)
for w in array:
if not isRace:
self.romFile.writeWord(w)
else:
self.race.writeWordMagic(w)
def writeDoorTransition(self, roomPtr):
if self.race is None:
self.romFile.writeWord(roomPtr)
else:
self.race.writeDoorTransition(roomPtr)
# write area randomizer transitions to ROM
# doorConnections : a list of connections. each connection is a dictionary describing
# - where to write in the ROM :
# DoorPtr : door pointer to write to
# - what to write in the ROM :
# RoomPtr, direction, bitflag, cap, screen, distanceToSpawn : door properties
# * if SamusX and SamusY are defined in the dict, custom ASM has to be written
# to reposition samus, and call doorAsmPtr if non-zero. The written Door ASM
# property shall point to this custom ASM.
# * if not, just write doorAsmPtr as the door property directly.
def writeDoorConnections(self, doorConnections):
asmAddress = Addresses.getOne('customDoorsAsm')
for conn in doorConnections:
# write door ASM for transition doors (code and pointers)
# print('Writing door connection ' + conn['ID'] + ". doorPtr="+hex(doorPtr))
doorPtr = conn['DoorPtr']
roomPtr = conn['RoomPtr']
if doorPtr in self.doorConnectionSpecific:
self.doorConnectionSpecific[doorPtr](roomPtr)
if roomPtr in self.roomConnectionSpecific:
self.roomConnectionSpecific[roomPtr](doorPtr)
self.romFile.seek(0x10000 + doorPtr)
# write room ptr
self.writeDoorTransition(roomPtr & 0xFFFF)
# write bitflag (if area switch we have to set bit 0x40, and remove it if same area)
self.romFile.writeByte(conn['bitFlag'])
# write direction
self.romFile.writeByte(conn['direction'])
# write door cap x
self.romFile.writeByte(conn['cap'][0])
# write door cap y
self.romFile.writeByte(conn['cap'][1])
# write screen x
self.romFile.writeByte(conn['screen'][0])
# write screen y
self.romFile.writeByte(conn['screen'][1])
# write distance to spawn
self.romFile.writeWord(conn['distanceToSpawn'] & 0xFFFF)
# write door asm
asmPatch = []
# call original door asm ptr if needed
if conn['doorAsmPtr'] != 0x0000:
# endian convert
(D0, D1) = (conn['doorAsmPtr'] & 0x00FF, (conn['doorAsmPtr'] & 0xFF00) >> 8)
asmPatch += [ 0x20, D0, D1 ] # JSR $doorAsmPtr
# special ASM hook point for VARIA needs when taking the door (used for animals)
if 'exitAsmPtr' in conn:
# endian convert
(D0, D1) = (conn['exitAsmPtr'] & 0x00FF, (conn['exitAsmPtr'] & 0xFF00) >> 8)
asmPatch += [ 0x20, D0, D1 ] # JSR $exitAsmPtr
# incompatible transition
if 'SamusX' in conn:
# endian convert
(X0, X1) = (conn['SamusX'] & 0x00FF, (conn['SamusX'] & 0xFF00) >> 8)
(Y0, Y1) = (conn['SamusY'] & 0x00FF, (conn['SamusY'] & 0xFF00) >> 8)
# force samus position
# see door_transition.asm. assemble it to print routines SNES addresses.
asmPatch += [ 0x20, 0x00, 0xF6 ] # JSR incompatible_doors
asmPatch += [ 0xA9, X0, X1 ] # LDA #$SamusX ; fixed Samus X position
asmPatch += [ 0x8D, 0xF6, 0x0A ] # STA $0AF6 ; update Samus X position in memory
asmPatch += [ 0xA9, Y0, Y1 ] # LDA #$SamusY ; fixed Samus Y position
asmPatch += [ 0x8D, 0xFA, 0x0A ] # STA $0AFA ; update Samus Y position in memory
else:
# still give I-frames
asmPatch += [ 0x20, 0x40, 0xF6 ] # JSR giveiframes
# return
asmPatch += [ 0x60 ] # RTS
self.romFile.writeWord(asmAddress & 0xFFFF)
self.romFile.seek(asmAddress)
for byte in asmPatch:
self.romFile.writeByte(byte)
# print("asmAddress=%x" % asmAddress)
# print("asmPatch=" + str(["%02x" % b for b in asmPatch]))
asmAddress += len(asmPatch)
# update room state header with song changes
# TODO just do an IPS patch for this as it is completely static
# this would get rid of both 'song' and 'songs' fields
# as well as this code
if 'song' in conn:
for addr in conn["songs"]:
self.romFile.seek(0x70000 + addr)
self.romFile.writeByte(conn['song'])
self.romFile.writeByte(0x5)
# change BG table to avoid scrolling sky bug when transitioning to west ocean
def patchWestOcean(self, doorPtr):
self.romFile.writeWord(doorPtr, snes_to_pc(0x8fb7bb))
# forces CRE graphics refresh when exiting kraid's or croc room
def forceRoomCRE(self, roomPtr, creFlag=0x2):
# Room ptr in bank 8F + CRE flag offset
offset = 0x70000 + roomPtr + 0x8
self.romFile.writeByte(creFlag, offset)
buttons = {
"Select" : [0x00, 0x20],
"A" : [0x80, 0x00],
"B" : [0x00, 0x80],
"X" : [0x40, 0x00],
"Y" : [0x00, 0x40],
"L" : [0x20, 0x00],
"R" : [0x10, 0x00],
"None" : [0x00, 0x00]
}
controls = {
"Shoot" : [0xb331, 0x1722d],
"Jump" : [0xb325, 0x17233],
"Dash" : [0xb32b, 0x17239],
"Item Select" : [0xb33d, 0x17245],
"Item Cancel" : [0xb337, 0x1723f],
"Angle Up" : [0xb343, 0x1724b],
"Angle Down" : [0xb349, 0x17251]
}
# write custom contols to ROM.
# controlsDict : possible keys are "Shot", "Jump", "Dash", "ItemSelect", "ItemCancel", "AngleUp", "AngleDown"
# possible values are "A", "B", "X", "Y", "L", "R", "Select", "None"
def writeControls(self, controlsDict):
for ctrl, button in controlsDict.items():
if ctrl not in RomPatcher.controls:
raise ValueError("Invalid control name : " + str(ctrl))
if button not in RomPatcher.buttons:
raise ValueError("Invalid button name : " + str(button))
for addr in RomPatcher.controls[ctrl]:
self.romFile.writeByte(RomPatcher.buttons[button][0], addr)
self.romFile.writeByte(RomPatcher.buttons[button][1])
def writePlandoAddresses(self, locations):
self.romFile.seek(Addresses.getOne('plandoAddresses'))
for loc in locations:
self.romFile.writeWord(loc.Address & 0xFFFF)
# fill remaining addresses with 0xFFFF
maxLocsNumber = 128
for i in range(0, maxLocsNumber-len(locations)):
self.romFile.writeWord(0xFFFF)
def writePlandoTransitions(self, transitions, doorsPtrs, maxTransitions):
self.romFile.seek(Addresses.getOne('plandoTransitions'))
for (src, dest) in transitions:
self.romFile.writeWord(doorsPtrs[src])
self.romFile.writeWord(doorsPtrs[dest])
# fill remaining addresses with 0xFFFF
for i in range(0, maxTransitions-len(transitions)):
self.romFile.writeWord(0xFFFF)
self.romFile.writeWord(0xFFFF)
def enableMoonWalk(self):
# replace STZ with STA since A is non-zero at this point
self.romFile.writeByte(0x8D, Addresses.getOne('moonwalk'))
def writeAdditionalETanks(self, additionalETanks):
self.romFile.writeByte(additionalETanks, Addresses.getOne("additionalETanks"))
def writeHellrunRate(self, hellrunRatePct):
hellrunRateVal = min(int(0x40*float(hellrunRatePct)/100.0), 0xff)
self.romFile.writeByte(hellrunRateVal, Addresses.getOne("hellrunRate"))
def setOamTile(self, nth, middle, newTile, y=0xFC):
# an oam entry is made of five bytes: (s000000 xxxxxxxxx) (yyyyyyyy) (YXpp000t tttttttt)
# after and before the middle of the screen is not handle the same
if nth >= middle:
x = (nth - middle) * 0x08
else:
x = 0x200 - (0x08 * (middle - nth))
self.romFile.writeWord(x)
self.romFile.writeByte(y)
self.romFile.writeWord(0x3100+newTile)
def writeVersion(self, version, addRotation=False):
# max 32 chars
# new oamlist address in free space at the end of bank 8C
self.romFile.writeWord(0xF3E9, snes_to_pc(0x8ba0e3))
self.romFile.writeWord(0xF3E9, snes_to_pc(0x8ba0e9))
# string length
versionLength = len(version)
if addRotation:
rotationLength = len('rotation')
length = versionLength + rotationLength
else:
length = versionLength
self.romFile.writeWord(length, snes_to_pc(0x8cf3e9))
versionMiddle = int(versionLength / 2) + versionLength % 2
# oams
for (i, char) in enumerate(version):
self.setOamTile(i, versionMiddle, char2tile[char])
if addRotation:
rotationMiddle = int(rotationLength / 2) + rotationLength % 2
for (i, char) in enumerate('rotation'):
self.setOamTile(i, rotationMiddle, char2tile[char], y=0x8e)
def writeDoorsColor(self, doorsStart, player):
if self.race is None:
DoorsManager.writeDoorsColor(self.romFile, doorsStart, player, self.romFile.writeWord)
else:
DoorsManager.writeDoorsColor(self.romFile, doorsStart, player, self.writePlmWord)
def writeDoorIndicators(self, plms, area, door):
indicatorFlags = IndicatorFlag.Standard | (IndicatorFlag.AreaRando if area else 0) | (IndicatorFlag.DoorRando if door else 0)
patchDict = self.patchAccess.getDictPatches()
additionalPLMs = self.patchAccess.getAdditionalPLMs()
def updateIndicatorPLM(door, doorType):
nonlocal additionalPLMs, patchDict
plmName = 'Indicator[%s]' % door
addPlm = False
if plmName in patchDict:
for addr,bytez in patchDict[plmName].items():
plmBytes = bytez
break
else:
plmBytes = additionalPLMs[plmName]['plm_bytes_list'][0]
addPlm = True
w = getWord(doorType)
plmBytes[0] = w[0]
plmBytes[1] = w[1]
return plmName, addPlm
indicatorPLMs = DoorsManager.getIndicatorPLMs(self.player, indicatorFlags)
for doorName,plmType in indicatorPLMs.items():
plmName,addPlm = updateIndicatorPLM(doorName, plmType)
if addPlm:
plms.append(plmName)
else:
self.applyIPSPatch(plmName)
def writeObjectives(self, itemLocs, tourian):
objectives = Objectives.objDict[self.player]
objectives.writeGoals(self.romFile)
objectives.writeIntroObjectives(self.romFile, tourian)
self.writeItemsMasks(itemLocs)
# hack bomb_torizo.ips to wake BT in all cases if necessary, ie chozo bots objective is on, and nothing at bombs
SM: 0.4.1 Fixes and Additional Objective Options (#1859) * first working (most of the time) progression generation for SM using VariaRandomizer's rules, items, locations and accessPoint (as regions) * first working single-world randomized SM rom patches * - SM now displays message when getting an item outside for someone else (fills ROM item table) This is dependant on modifications done to sm_randomizer_rom project * First working MultiWorld SM * some missing things: - player name inject in ROM and get in client - end game get from ROM in client - send self item to server - add player names table in ROM * replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better) * - reenabled balancing * post rebase fixes * updated SmClient.py * + added VariaRandomizer LICENSE * + added sm_randomizer_rom project (which builds sm.ips) * Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning * properly revert change made to CollectionState and more cleaning * Fixed multiworld support patch not working with VariaRandomizer's * missing file commit * Fixed syntax error in unused code to satisfy Linter * Revert "Fixed multiworld support patch not working with VariaRandomizer's" This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b. * many fixes and improovement - fixed seeded generation - fixed broken logic when more than one SM world - added missing rules for inter-area transitions - added basic patch presence for logic - added DoorManager init call to reflect present patches for logic - moved CollectionState addition out of BaseClasses into SM world - added condition to apply progitempool presorting only if SM world is present - set Bosses item id to None to prevent them going into multidata - now use get_game_players * first working (most of the time) progression generation for SM using VariaRandomizer's rules, items, locations and accessPoint (as regions) * first working single-world randomized SM rom patches * - SM now displays message when getting an item outside for someone else (fills ROM item table) This is dependant on modifications done to sm_randomizer_rom project * First working MultiWorld SM * some missing things: - player name inject in ROM and get in client - end game get from ROM in client - send self item to server - add player names table in ROM * replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better) * - reenabled balancing * post rebase fixes * updated SmClient.py * + added VariaRandomizer LICENSE * + added sm_randomizer_rom project (which builds sm.ips) * Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning * properly revert change made to CollectionState and more cleaning * Fixed multiworld support patch not working with VariaRandomizer's * missing file commit * Fixed syntax error in unused code to satisfy Linter * Revert "Fixed multiworld support patch not working with VariaRandomizer's" This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b. * many fixes and improovement - fixed seeded generation - fixed broken logic when more than one SM world - added missing rules for inter-area transitions - added basic patch presence for logic - added DoorManager init call to reflect present patches for logic - moved CollectionState addition out of BaseClasses into SM world - added condition to apply progitempool presorting only if SM world is present - set Bosses item id to None to prevent them going into multidata - now use get_game_players * Fixed multiworld support patch not working with VariaRandomizer's Added stage_fill_hook to set morph first in progitempool Added back VariaRandomizer's standard patches * + added missing files from variaRandomizer project * + added missing variaRandomizer files (custom sprites) + started integrating VariaRandomizer options (WIP) * Some fixes for player and server name display - fixed player name of 16 characters reading too far in SM client - fixed 12 bytes SM player name limit (now 16) - fixed server name not being displayed in SM when using server cheat ( now displays RECEIVED FROM ARCHIPELAGO) - request: temporarly changed default seed names displayed in SM main menu to OWTCH * Fixed Goal completion not triggering in smClient * integrated VariaRandomizer's options into AP (WIP) - startAP is working - door rando is working - skillset is working * - fixed itemsounds.ips crash by always including nofanfare.ips into multiworld.ips (itemsounds is now always applied and "itemsounds" preset must always be "off") * skillset are now instanced per player instead of being a singleton class * RomPatches are now instanced per player instead of being a singleton class * DoorManager is now instanced per player instead of being a singleton class * - fixed the last bugs that prevented generation of >1 SM world * fixed crash when no skillset preset is specified in randoPreset (default to "casual") * maxDifficulty support and itemsounds removal - added support for maxDifficulty - removed itemsounds patch as its always applied from multiworld patch for now * Fixed bad merge * Post merge adaptation * fixed player name length fix that got lost with the merge * fixed generation with other game type than SM * added default randoPreset json for SM in playerSettings.yaml * fixed broken SM client following merge * beautified json skillset presets * Fixed ArchipelagoSmClient not building * Fixed conflict between mutliworld patch and beam_doors_plms patch - doorsColorsRando now working * SM generation now outputs APBP - Fixed paths for patches and presets when frozen * added missing file and fixed multithreading issue * temporarily set data_version = 0 * more work - added support for AP starting items - fixed client crash with gamemode being None - patch.py "compatible_version" is now 3 * commited missing asm files fixed start item reserve breaking game (was using bad write offset when patching) * Nothing item are now handled game-side. the game will now skip displaying a message box for received Nothing item (but the client will still receive it). fixed crash in SMClient when loosing connection to SNI * fixed No Energy Item missing its ID fixed Plando * merge post fixes * fixed start item Grapple, XRay and Reserve HUD, as well as graphic beams (except ice palette color) * fixed freeze in blue brinstar caused by Varia's custom PLM not being filled with proper Multiworld PLM address (altLocsAddresses) * fixed start item x-ray HUD display * Fixed start items being sent by the server (is all handled in ROM) Start items are now not removed from itempool anymore Nothing Item is now local_items so no player will ever pickup Nothing. Doing so reduces contribution of this world to the Multiworld the more Nothing there is though. Fixed crash (and possibly passing but broken) at generation where the static list of IPSPatches used by all SM worlds was being modified * fixed settings that could be applied to any SM players * fixed auth to server only using player name (now does as ALTTP to authenticate) * - fixed End Credits broken text * added non SM item name display * added all supported SM options in playerSettings.yaml * fixed locations needing a list of parent regions (now generate a region for each location with one-way exits to each (previously) parent region did some cleaning (mainly reverts on unnecessary core classes * minor setting fixes and tweaks - merged Area and lightArea settings - made missileQty, superQty and powerBombQty use value from 10 to 90 and divide value by float(10) when generating - fixed inverted layoutPatch setting * added option start_inventory_removes_from_pool fixed option names formatting fixed lint errors small code and repo cleanup * Hopefully fixed ROR2 that could not send any items * - fixed missing required change to ROR2 * fixed 0 hp when respawning without having ever saved (start items were not updating the save checksum) * fixed typo with doors_colors_rando * fixed checksum * added custom sprites for off-world items (progression or not) the original AP sprite was made with PierRoulette's SM Item Sprite Utility by ijwu * - added missing change following upstream merge - changed patch filename extension from apbp to apm3 so patch can be used with the new client * added morph placement options: early means local and sphere 1 * fixed failing unit tests * - fixed broken custom_preset options * - big cleanup to remove unnecessary or unsupported features * - more cleanup * - moved sm_randomizer_rom and all always applied patches into an external project that outputs basepatch.ips - small cleanup * - added comment to refer to project for generating basepatch.ips (https://github.com/lordlou/SMBasepatch) * fixed g4_skip patch that can be not applied if hud is enabled * - fixed off world sprite that can have broken graphics (restricted to use only first 2 palette) * - updated basepatch to reflect g4_skip removal - moved more asm files to SMBasepatch project * - tourian grey doors at baby metroid are now always flashing (allowing to go back if needed) * fixed wrong path if using built as exe * - cleaned exposed maxDifficulty options - removed always enabled Knows * Merged LttPClient and SMClient into SNIClient * added varia_custom Preset Option that fetch a preset (read from a new varia_custom_preset Option) from varia's web service * small doc precision * - added death_link support - fixed broken Goal Completion - post merge fix * - removed now useless presets * - fixed bad internal mapping with maxDiff - increases maxDiff if only Bosses is preventing beating the game * - added support for lowercase custom preset sections (knows, settings and controller) - fixed controller settings not applying to ROM * - fixed death loop when dying with Door rando, bomb or speed booster as starting items - varia's backup save should now be usable (automatically enabled when doing door rando) * -added docstring for generated yaml * fixed bad merge * fixed broken infinity max difficulty * commented debug prints * adjusted credits to mark progression speed and difficulty as Non Available * added support for more than 255 players (will print Archipelago for higher player number) * fixed missing cleanup * added support for 65535 different player names in ROM * fixed generations failing when only bosses are unreachable * - replaced setting maxDiff to infinity with a bool only affecting boss logics if only bosses are left to finish * fixed failling generations when using 'fun' settings Accessibility checks are forced to 'items' if restricted locations are used by VARIA following usage of 'fun' settings * fixed debug logger * removed unsupported "suits_restriction" option * fixed generations failing when only bosses are unreachable (using a less intrusive approach for AP) * - fixed deathlink emptying reserves - added death_link_survive option that lets player survive when receiving a deathlink if the have non-empty reserves * - merged death_link and death_link_survive options * fixed death_link * added a fallback default starting location instead of failing generation if an invalid one was chosen * added Nothing and NoEnergy as hint blacklist added missing NoEnergy as local items and removed it from progression * fixed broken Item links * fixed failing generation that could happen with Disabled Tourian fixed shared Location list that could be modified for each world * added missing force disable of EscapeRando if an escape solution cant be found * fixed broken animal surprise patches * prevent receiving items when in the first room of Ceres (message box in mode7 is broken) * fixed generating with "activate chozo robots" Objective * added soft reset that saves to initial starting location reverted code change applied to fix softlocks from comeback checks reverted forcing all beam local when using door rando * replaced "save and reset" with "save and fast reload" (using same Start+Select+L+R) * added documentation about Save and Reload removed forgotten docstring about forcing beams as local items when using door rando * fixed frequent failing generation on WebHost (KeyError: 'Kraid') * added "objectiveRandom", "nbObjective", objectiveList and adapted Objective selection options to better reflect VARIA's. fixed "collect 100% items" not being excluded when objectiveRandom is used added Exception when VARIA initial layout fails * fixed broken non-AP items fixed determinism caused by the use of a set * fixed generation failing on Webhost with string as a OptionSet (replaced default with a list of string) cleaned doc and naming of Objective related Options
2023-06-29 08:51:09 -04:00
if objectives.isGoalActive("activate chozo robots") and RomPatches.has(self.player, RomPatches.BombTorizoWake):
bomb = next((il for il in itemLocs if il.Location.Name == "Bomb"), None)
if bomb is not None and bomb.Item.Category == "Nothing":
for addrName in ["BTtweaksHack1", "BTtweaksHack2"]:
self.romFile.seek(Addresses.getOne(addrName))
for b in [0xA9,0x00,0x00]: # LDA #$0000 ; set zero flag to wake BT
self.romFile.writeByte(b)
def writeItemsMasks(self, itemLocs):
# write items/beams masks for "collect all major" objective
itemsMask = 0
beamsMask = 0
for il in itemLocs:
if not il.Location.restricted:
item = il.Item
itemsMask |= item.ItemBits
beamsMask |= item.BeamBits
self.romFile.writeWord(itemsMask, Addresses.getOne('itemsMask'))
self.romFile.writeWord(beamsMask, Addresses.getOne('beamsMask'))
# tile number in tileset
char2tile = {
'-': 207,
'a': 208,
'.': 243,
'0': 244
}
for i in range(1, ord('z')-ord('a')+1):
char2tile[chr(ord('a')+i)] = char2tile['a']+i
for i in range(1, ord('9')-ord('0')+1):
char2tile[chr(ord('0')+i)] = char2tile['0']+i
class MessageBox(object):
def __init__(self, rom):
self.rom = rom
# in message boxes the char a is at offset 0xe0 in the tileset
self.char2tile = {'1': 0x00, '2': 0x01, '3': 0x02, '4': 0x03, '5': 0x04, '6': 0x05, '7': 0x06, '8': 0x07, '9': 0x08, '0': 0x09,
' ': 0x4e, '-': 0xcf, 'a': 0xe0, '.': 0xfa, ',': 0xfb, '`': 0xfc, "'": 0xfd, '?': 0xfe, '!': 0xff}
for i in range(1, ord('z')-ord('a')+1):
self.char2tile[chr(ord('a')+i)] = self.char2tile['a']+i
# add 0x0c/0x06 to offsets as there's 12/6 bytes before the strings, string length is either 0x13/0x1a
self.offsets = {
'ETank': (snes_to_pc(0x85877f)+0x0c, 0x13),
'Missile': (0x287bf+0x06, 0x1a),
'Super': (0x288bf+0x06, 0x1a),
'PowerBomb': (0x289bf+0x06, 0x1a),
'Grapple': (0x28abf+0x06, 0x1a),
'XRayScope': (0x28bbf+0x06, 0x1a),
'Varia': (0x28cbf+0x0c, 0x13),
'SpringBall': (0x28cff+0x0c, 0x13),
'Morph': (0x28d3f+0x0c, 0x13),
'ScrewAttack': (0x28d7f+0x0c, 0x13),
'HiJump': (0x28dbf+0x0c, 0x13),
'SpaceJump': (0x28dff+0x0c, 0x13),
'SpeedBooster': (0x28e3f+0x06, 0x1a),
'Charge': (0x28f3f+0x0c, 0x13),
'Ice': (0x28f7f+0x0c, 0x13),
'Wave': (0x28fbf+0x0c, 0x13),
'Spazer': (0x28fff+0x0c, 0x13),
'Plasma': (0x2903f+0x0c, 0x13),
'Bomb': (0x2907f+0x06, 0x1a),
'Reserve': (0x294ff+0x0c, 0x13),
'Gravity': (0x2953f+0x0c, 0x13)
}
def updateMessage(self, box, message, vFlip=False, hFlip=False):
(address, oldLength) = self.offsets[box]
newLength = len(message)
assert newLength <= oldLength, "string '{}' is too long, max {}".format(message, oldLength)
padding = oldLength - newLength
paddingLeft = int(padding / 2)
paddingRight = int(padding / 2)
paddingRight += padding % 2
attr = self.getAttr(vFlip, hFlip)
# write spaces for padding left
for i in range(paddingLeft):
self.writeChar(address, ' ')
address += 0x02
# write message
for char in message:
self.writeChar(address, char)
address += 0x01
self.updateAttr(attr, address)
address += 0x01
# write spaces for padding right
for i in range(paddingRight):
self.writeChar(address, ' ')
address += 0x02
def writeChar(self, address, char):
self.rom.writeByte(self.char2tile[char], address)
def getAttr(self, vFlip, hFlip):
# vanilla is 0x28:
byte = 0x28
if vFlip:
byte |= 0b10000000
if hFlip:
byte |= 0b01000000
return byte
def updateAttr(self, byte, address):
self.rom.writeByte(byte, address)
class RomTypeForMusic(IntFlag):
VariaSeed = 1
AreaSeed = 2
BossSeed = 4
class MusicPatcher(object):
# rom: ROM object to patch
# romType: 0 if not varia seed, or bitwise or of RomTypeForMusic enum
# baseDir: directory containing all music data/descriptors/constraints
# constraintsFile: file to constraints JSON descriptor, relative to baseDir/constraints.
# if None, will be determined automatically from romType
def __init__(self, rom, romType,
baseDir=os.path.join(appDir, 'varia_custom_sprites', 'music'),
constraintsFile=None):
self.rom = rom
self.baseDir = baseDir
variaSeed = bool(romType & RomTypeForMusic.VariaSeed)
self.area = variaSeed and bool(romType & RomTypeForMusic.AreaSeed)
self.boss = variaSeed and bool(romType & RomTypeForMusic.BossSeed)
metaDir = os.path.join(baseDir, "_metadata")
constraintsDir = os.path.join(baseDir, "_constraints")
if constraintsFile is None:
constraintsFile = 'varia.json' if variaSeed else 'vanilla.json'
with open(os.path.join(constraintsDir, constraintsFile), 'r') as f:
self.constraints = json.load(f)
nspcInfoPath = os.path.join(baseDir, "nspc_metadata.json")
with open(nspcInfoPath, "r") as f:
nspcInfo = json.load(f)
self.nspcInfo = {}
for nspc,info in nspcInfo.items():
self.nspcInfo[self._nspc_path(nspc)] = info
self.allTracks = {}
self.vanillaTracks = None
for metaFile in os.listdir(metaDir):
metaPath = os.path.join(metaDir, metaFile)
if not metaPath.endswith(".json"):
continue
with open(metaPath, 'r') as f:
meta = json.load(f)
# will silently overwrite entries with same name, so avoid
# conflicting descriptor files ...
self.allTracks.update(meta)
if metaFile == "vanilla.json":
self.vanillaTracks = meta
assert self.vanillaTracks is not None, "MusicPatcher: missing vanilla JSON descriptor"
self.replaceableTracks = [track for track in self.vanillaTracks if track not in self.constraints['preserve'] and track not in self.constraints['discard']]
self.musicDataTableAddress = snes_to_pc(0x8FE7E4)
self.musicDataTableMaxSize = 45 # to avoid overwriting useful data in bank 8F
# tracks: dict with track name to replace as key, and replacing track name as value
# updateReferences: change room state headers and special tracks. may be False if you're patching a rom hack or something
# output: if not None, dump a JSON file with what was done
# replaced tracks must be in
# replaceableTracks, and new tracks must be in allTracks
# tracks not in the dict will be kept vanilla
# raise RuntimeError if not possible
def replace(self, tracks, updateReferences=True, output=None):
for track in tracks:
if track not in self.replaceableTracks:
raise RuntimeError("Cannot replace track %s" % track)
trackList = self._getTrackList(tracks)
replacedVanilla = [t for t in self.replaceableTracks if t in trackList and t not in tracks]
for van in replacedVanilla:
tracks[van] = van
# print("trackList="+str(trackList))
musicData = self._getMusicData(trackList)
# print("musicData="+str(musicData))
if len(musicData) > self.musicDataTableMaxSize:
raise RuntimeError("Music data table too long. %d entries, max is %d" % (len(musicData, self.musicDataTableMaxSize)))
musicDataAddresses = self._getMusicDataAddresses(musicData)
self._writeMusicData(musicDataAddresses)
self._writeMusicDataTable(musicData, musicDataAddresses)
if updateReferences == True:
self._updateReferences(trackList, musicData, tracks)
if output is not None:
self._dump(output, trackList, musicData, musicDataAddresses)
# compose a track list from vanilla tracks, replaced tracks, and constraints
def _getTrackList(self, replacedTracks):
trackList = set()
for track in self.vanillaTracks:
if track in replacedTracks:
trackList.add(replacedTracks[track])
elif track not in self.constraints['discard']:
trackList.add(track)
return list(trackList)
def _nspc_path(self, nspc_path):
return os.path.join(self.baseDir, nspc_path)
# get list of music data files to include in the ROM
# can contain empty entries, marked with a None, to account
# for fixed place data ('preserve' constraint)
def _getMusicData(self, trackList):
# first, make musicData the minimum size wrt preserved tracks
preservedTracks = {trackName:self.vanillaTracks[trackName] for trackName in self.constraints['preserve']}
preservedDataIndexes = [track['data_index'] for trackName,track in preservedTracks.items()]
musicData = [None]*(max(preservedDataIndexes)+1)
# fill preserved spots
for track in self.constraints['preserve']:
idx = self.vanillaTracks[track]['data_index']
nspc = self._nspc_path(self.vanillaTracks[track]['nspc_path'])
if nspc not in musicData:
musicData[idx] = nspc
# print("stored " + nspc + " at "+ str(idx))
# then fill data in remaining spots
idx = 0
for track in trackList:
previdx = idx
if track not in self.constraints['preserve']:
nspc = self._nspc_path(self.allTracks[track]['nspc_path'])
if nspc not in musicData:
for i in range(idx, len(musicData)):
# print("at " + str(i) + ": "+str(musicData[i]))
if musicData[i] is None:
musicData[i] = nspc
idx = i+1
break
if idx == previdx:
idx += 1
musicData.append(nspc)
# print("stored " + nspc + " at "+ str(idx))
return musicData
# get addresses to store each data file to. raise RuntimeError if not possible
# pretty dumb algorithm for now, just store data wherever possible,
# prioritizing first areas in usableSpace
# store data from end of usable space to make room for other data (for hacks for instance)
def _getMusicDataAddresses(self, musicData):
usableSpace = self.constraints['usable_space_ranges_pc']
musicDataAddresses = {}
for dataFile in musicData:
if dataFile is None:
continue
sz = os.path.getsize(dataFile)
blocks = self.nspcInfo[dataFile]['block_headers_offsets']
for r in usableSpace:
# find a suitable address so header words are not split across banks (header is 2 words)
addr = r['end'] - sz
def isCrossBank(off):
nonlocal addr
endBankOffset = pc_to_snes(addr+off+4) & 0x7fff
return endBankOffset == 1 or endBankOffset == 3
while addr >= r['start'] and any(isCrossBank(off) for off in blocks):
addr -= 1
if addr >= r['start']:
musicDataAddresses[dataFile] = addr
r['end'] = addr
break
if dataFile not in musicDataAddresses:
raise RuntimeError("Cannot find enough space to store music data file "+dataFile)
return musicDataAddresses
def _writeMusicData(self, musicDataAddresses):
for dataFile, addr in musicDataAddresses.items():
self.rom.seek(addr)
with open(dataFile, 'rb') as f:
self.rom.write(f.read())
def _writeMusicDataTable(self, musicData, musicDataAddresses):
self.rom.seek(self.musicDataTableAddress)
for dataFile in musicData:
addr = pc_to_snes(musicDataAddresses[dataFile]) if dataFile in musicDataAddresses else 0
self.rom.writeLong(addr)
def _getDataId(self, musicData, track):
return (musicData.index(self._nspc_path(self.allTracks[track]['nspc_path']))+1)*3
def _getTrackId(self, track):
return self.allTracks[track]['track_index'] + 5
def _updateReferences(self, trackList, musicData, replacedTracks):
trackAddresses = {}
def addAddresses(track, vanillaTrackData, prio=False):
nonlocal trackAddresses
addrs = []
prioAddrs = []
if 'pc_addresses' in vanillaTrackData:
addrs += vanillaTrackData['pc_addresses']
if self.area and 'pc_addresses_area' in vanillaTrackData:
prioAddrs += vanillaTrackData['pc_addresses_area']
if self.boss and 'pc_addresses_boss' in vanillaTrackData:
prioAddrs += vanillaTrackData['pc_addresses_boss']
if track not in trackAddresses:
trackAddresses[track] = []
# if prioAddrs are somewhere else, remove if necessary
prioSet = set(prioAddrs)
for t,tAddrs in trackAddresses.items():
trackAddresses[t] = list(set(tAddrs) - prioSet)
# if some of addrs are somewhere else, remove them from here
for t,tAddrs in trackAddresses.items():
addrs = list(set(addrs) - set(tAddrs))
trackAddresses[track] += prioAddrs + addrs
for track in trackList:
if track in replacedTracks.values():
for van,rep in replacedTracks.items():
if rep == track:
addAddresses(track, self.vanillaTracks[van])
else:
addAddresses(track, self.vanillaTracks[track])
for track in trackList:
dataId = self._getDataId(musicData, track)
trackId = self._getTrackId(track)
for addr in trackAddresses[track]:
self.rom.seek(addr)
self.rom.writeByte(dataId)
self.rom.writeByte(trackId)
self._writeSpecialReferences(replacedTracks, musicData)
# write special (boss) data
def _writeSpecialReferences(self, replacedTracks, musicData, static=True, dynamic=True):
for track,replacement in replacedTracks.items():
# static patches are needed only when replacing tracks
if track != replacement:
staticPatches = self.vanillaTracks[track].get("static_patches", None)
else:
staticPatches = None
# dynamic patches are similar to pc_addresses*, and must be written also
# when track is vanilla, as music data table is changed
dynamicPatches = self.vanillaTracks[track].get("dynamic_patches", None)
if static and staticPatches:
for addr,bytez in staticPatches.items():
self.rom.seek(int(addr))
for b in bytez:
self.rom.writeByte(b)
if dynamic and dynamicPatches:
dataId = self._getDataId(musicData, replacement)
trackId = self._getTrackId(replacement)
dataIdAddrs = dynamicPatches.get("data_id", [])
trackIdAddrs = dynamicPatches.get("track_id", [])
for addr in dataIdAddrs:
self.rom.writeByte(dataId, addr)
for addr in trackIdAddrs:
self.rom.writeByte(trackId, addr)
def _dump(self, output, trackList, musicData, musicDataAddresses):
music={}
no=0
for md in musicData:
if md is None:
music["NoData_%d" % no] = None
no += 1
else:
tracks = []
h,t=os.path.split(md)
md=os.path.join(os.path.split(h)[1], t)
for track,trackData in self.allTracks.items():
if trackData['nspc_path'] == md:
tracks.append(track)
music[md] = tracks
musicSnesAddresses = {}
for nspc, addr in musicDataAddresses.items():
h,t=os.path.split(nspc)
nspc=os.path.join(os.path.split(h)[1], t)
musicSnesAddresses[nspc] = "$%06x" % pc_to_snes(addr)
dump = {
"track_list": sorted(trackList),
"music_data": music,
"music_data_addresses": musicSnesAddresses
}
with open(output, 'w') as f:
json.dump(dump, f, indent=4)