From 920cffda2d79577e96733e5c18bacdefba795835 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Sat, 31 Aug 2024 06:15:00 -0500 Subject: [PATCH 01/13] KDL3: Version 2.0.0 (#3323) * initial work on procedure patch * more flexibility load default procedure for version 5 patches add args for procedure add default extension for tokens and bsdiff allow specifying additional required extensions for generation * pushing current changes to go fix tloz bug * move tokens into a separate inheritable class * forgot the commit to remove token from ProcedurePatch * further cleaning from bad commit * start on docstrings * further work on docstrings and typing * improve docstrings * fix incorrect docstring * cleanup * clean defaults and docstring * define interface that has only the bare minimum required for `Patch.create_rom_file` * change to dictionary.get * remove unnecessary if statement * update to explicitly check for procedure, restore compatible version and manual override * Update Files.py * remove struct uses * Update Rom.py * convert KDL3 to APPP * change class variables to instance variables * Update worlds/Files.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Update worlds/Files.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * move required_extensions to tuple * fix missing tuple ellipsis * fix classvar mixup * rename tokens to _tokens. use hasattr * type hint cleanup * Update Files.py * initial base for local items, need to finish * coo not clean * handle local items for real, appp cleanup * actually make bosses send their locations * fix cloudy park 4 rule, zero deathlink message * remove redundant door_shuffle bool when generic ER gets in, this whole function gets rewritten. So just clean it a little now. * properly fix deathlink messages, fix fill error * update docs * add prefill items * fix kine fill error * Update Rom.py * Update Files.py * mypy and softlock fix * Update Gifting.py * mypy phase 1 * fix rare async client bug * Update __init__.py * typing cleanup * fix stone softlock because of the way Kine's Stone works, you can't clear the stone blocks before clearing the burning blocks, so we have to bring Burning from outside * Update Rom.py * Add option groups * Rename to lowercase * finish rename * whoops broke the world * fix animal duplication bug * overhaul filler generation * add Miku flavor * Update gifting.py * fix issues related to max_hs increase * Update test_locations.py * fix boss shuffle not working if level shuffle is disabled * fix bleeding default levels * Update options.py * thought this would print seed * yay bad merges * forgot options too * yeah lets just break generation while at it * this is probably a problem * cap required heart stars * Revert "cap required heart stars" This reverts commit 759efd3e2b14ec2855082de041ac989cb9c5d500. * fix duplication removal placement, deprecated test option * forgot that we need to account for what we place * move location ids * rewrite trap handling * further stage renumber fixes * forgot one more * basic UT support * fix local heart star checks * fix pattern --------- Co-authored-by: beauxq Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- worlds/kdl3/Locations.py | 940 ------------------ worlds/kdl3/Rom.py | 577 ----------- worlds/kdl3/Room.py | 95 -- worlds/kdl3/__init__.py | 185 ++-- worlds/kdl3/{Aesthetics.py => aesthetics.py} | 35 +- worlds/kdl3/{Client.py => client.py} | 70 +- .../kdl3/{ClientAddrs.py => client_addrs.py} | 0 .../kdl3/{Compression.py => compression.py} | 0 worlds/kdl3/data/kdl3_basepatch.bsdiff4 | Bin 2411 -> 2646 bytes worlds/kdl3/{Gifting.py => gifting.py} | 19 +- worlds/kdl3/{Items.py => items.py} | 8 +- worlds/kdl3/locations.py | 940 ++++++++++++++++++ worlds/kdl3/{Names => names}/__init__.py | 0 .../animal_friend_spawns.py} | 11 + .../enemy_abilities.py} | 2 +- .../location_name.py} | 0 worlds/kdl3/{Options.py => options.py} | 66 +- worlds/kdl3/{Presets.py => presets.py} | 1 + worlds/kdl3/{Regions.py => regions.py} | 143 +-- worlds/kdl3/rom.py | 602 +++++++++++ worlds/kdl3/room.py | 133 +++ worlds/kdl3/{Rules.py => rules.py} | 145 +-- worlds/kdl3/{data => src}/APPauseIcons.dat | Bin worlds/kdl3/src/kdl3_basepatch.asm | 180 +++- worlds/kdl3/test/__init__.py | 2 + worlds/kdl3/test/test_goal.py | 14 +- worlds/kdl3/test/test_locations.py | 50 +- worlds/kdl3/test/test_shuffles.py | 258 +++-- 28 files changed, 2420 insertions(+), 2056 deletions(-) delete mode 100644 worlds/kdl3/Locations.py delete mode 100644 worlds/kdl3/Rom.py delete mode 100644 worlds/kdl3/Room.py rename worlds/kdl3/{Aesthetics.py => aesthetics.py} (91%) rename worlds/kdl3/{Client.py => client.py} (90%) rename worlds/kdl3/{ClientAddrs.py => client_addrs.py} (100%) rename worlds/kdl3/{Compression.py => compression.py} (100%) rename worlds/kdl3/{Gifting.py => gifting.py} (90%) rename worlds/kdl3/{Items.py => items.py} (95%) create mode 100644 worlds/kdl3/locations.py rename worlds/kdl3/{Names => names}/__init__.py (100%) rename worlds/kdl3/{Names/AnimalFriendSpawns.py => names/animal_friend_spawns.py} (95%) rename worlds/kdl3/{Names/EnemyAbilities.py => names/enemy_abilities.py} (99%) rename worlds/kdl3/{Names/LocationName.py => names/location_name.py} (100%) rename worlds/kdl3/{Options.py => options.py} (82%) rename worlds/kdl3/{Presets.py => presets.py} (98%) rename worlds/kdl3/{Regions.py => regions.py} (66%) create mode 100644 worlds/kdl3/rom.py create mode 100644 worlds/kdl3/room.py rename worlds/kdl3/{Rules.py => rules.py} (70%) rename worlds/kdl3/{data => src}/APPauseIcons.dat (100%) diff --git a/worlds/kdl3/Locations.py b/worlds/kdl3/Locations.py deleted file mode 100644 index 4d039a1349..0000000000 --- a/worlds/kdl3/Locations.py +++ /dev/null @@ -1,940 +0,0 @@ -import typing -from BaseClasses import Location, Region -from .Names import LocationName - -if typing.TYPE_CHECKING: - from .Room import KDL3Room - - -class KDL3Location(Location): - game: str = "Kirby's Dream Land 3" - room: typing.Optional["KDL3Room"] = None - - def __init__(self, player: int, name: str, address: typing.Optional[int], parent: typing.Union[Region, None]): - super().__init__(player, name, address, parent) - if not address: - self.show_in_spoiler = False - - -stage_locations = { - 0x770001: LocationName.grass_land_1, - 0x770002: LocationName.grass_land_2, - 0x770003: LocationName.grass_land_3, - 0x770004: LocationName.grass_land_4, - 0x770005: LocationName.grass_land_5, - 0x770006: LocationName.grass_land_6, - 0x770007: LocationName.ripple_field_1, - 0x770008: LocationName.ripple_field_2, - 0x770009: LocationName.ripple_field_3, - 0x77000A: LocationName.ripple_field_4, - 0x77000B: LocationName.ripple_field_5, - 0x77000C: LocationName.ripple_field_6, - 0x77000D: LocationName.sand_canyon_1, - 0x77000E: LocationName.sand_canyon_2, - 0x77000F: LocationName.sand_canyon_3, - 0x770010: LocationName.sand_canyon_4, - 0x770011: LocationName.sand_canyon_5, - 0x770012: LocationName.sand_canyon_6, - 0x770013: LocationName.cloudy_park_1, - 0x770014: LocationName.cloudy_park_2, - 0x770015: LocationName.cloudy_park_3, - 0x770016: LocationName.cloudy_park_4, - 0x770017: LocationName.cloudy_park_5, - 0x770018: LocationName.cloudy_park_6, - 0x770019: LocationName.iceberg_1, - 0x77001A: LocationName.iceberg_2, - 0x77001B: LocationName.iceberg_3, - 0x77001C: LocationName.iceberg_4, - 0x77001D: LocationName.iceberg_5, - 0x77001E: LocationName.iceberg_6, -} - -heart_star_locations = { - 0x770101: LocationName.grass_land_tulip, - 0x770102: LocationName.grass_land_muchi, - 0x770103: LocationName.grass_land_pitcherman, - 0x770104: LocationName.grass_land_chao, - 0x770105: LocationName.grass_land_mine, - 0x770106: LocationName.grass_land_pierre, - 0x770107: LocationName.ripple_field_kamuribana, - 0x770108: LocationName.ripple_field_bakasa, - 0x770109: LocationName.ripple_field_elieel, - 0x77010A: LocationName.ripple_field_toad, - 0x77010B: LocationName.ripple_field_mama_pitch, - 0x77010C: LocationName.ripple_field_hb002, - 0x77010D: LocationName.sand_canyon_mushrooms, - 0x77010E: LocationName.sand_canyon_auntie, - 0x77010F: LocationName.sand_canyon_caramello, - 0x770110: LocationName.sand_canyon_hikari, - 0x770111: LocationName.sand_canyon_nyupun, - 0x770112: LocationName.sand_canyon_rob, - 0x770113: LocationName.cloudy_park_hibanamodoki, - 0x770114: LocationName.cloudy_park_piyokeko, - 0x770115: LocationName.cloudy_park_mrball, - 0x770116: LocationName.cloudy_park_mikarin, - 0x770117: LocationName.cloudy_park_pick, - 0x770118: LocationName.cloudy_park_hb007, - 0x770119: LocationName.iceberg_kogoesou, - 0x77011A: LocationName.iceberg_samus, - 0x77011B: LocationName.iceberg_kawasaki, - 0x77011C: LocationName.iceberg_name, - 0x77011D: LocationName.iceberg_shiro, - 0x77011E: LocationName.iceberg_angel, -} - -boss_locations = { - 0x770200: LocationName.grass_land_whispy, - 0x770201: LocationName.ripple_field_acro, - 0x770202: LocationName.sand_canyon_poncon, - 0x770203: LocationName.cloudy_park_ado, - 0x770204: LocationName.iceberg_dedede, -} - -consumable_locations = { - 0x770300: LocationName.grass_land_1_u1, - 0x770301: LocationName.grass_land_1_m1, - 0x770302: LocationName.grass_land_2_u1, - 0x770303: LocationName.grass_land_3_u1, - 0x770304: LocationName.grass_land_3_m1, - 0x770305: LocationName.grass_land_4_m1, - 0x770306: LocationName.grass_land_4_u1, - 0x770307: LocationName.grass_land_4_m2, - 0x770308: LocationName.grass_land_4_m3, - 0x770309: LocationName.grass_land_6_u1, - 0x77030A: LocationName.grass_land_6_u2, - 0x77030B: LocationName.ripple_field_2_u1, - 0x77030C: LocationName.ripple_field_2_m1, - 0x77030D: LocationName.ripple_field_3_m1, - 0x77030E: LocationName.ripple_field_3_u1, - 0x77030F: LocationName.ripple_field_4_m2, - 0x770310: LocationName.ripple_field_4_u1, - 0x770311: LocationName.ripple_field_4_m1, - 0x770312: LocationName.ripple_field_5_u1, - 0x770313: LocationName.ripple_field_5_m2, - 0x770314: LocationName.ripple_field_5_m1, - 0x770315: LocationName.sand_canyon_1_u1, - 0x770316: LocationName.sand_canyon_2_u1, - 0x770317: LocationName.sand_canyon_2_m1, - 0x770318: LocationName.sand_canyon_4_m1, - 0x770319: LocationName.sand_canyon_4_u1, - 0x77031A: LocationName.sand_canyon_4_m2, - 0x77031B: LocationName.sand_canyon_5_u1, - 0x77031C: LocationName.sand_canyon_5_u3, - 0x77031D: LocationName.sand_canyon_5_m1, - 0x77031E: LocationName.sand_canyon_5_u4, - 0x77031F: LocationName.sand_canyon_5_u2, - 0x770320: LocationName.cloudy_park_1_m1, - 0x770321: LocationName.cloudy_park_1_u1, - 0x770322: LocationName.cloudy_park_4_u1, - 0x770323: LocationName.cloudy_park_4_m1, - 0x770324: LocationName.cloudy_park_5_m1, - 0x770325: LocationName.cloudy_park_6_u1, - 0x770326: LocationName.iceberg_3_m1, - 0x770327: LocationName.iceberg_5_u1, - 0x770328: LocationName.iceberg_5_u2, - 0x770329: LocationName.iceberg_5_u3, - 0x77032A: LocationName.iceberg_6_m1, - 0x77032B: LocationName.iceberg_6_u1, -} - -level_consumables = { - 1: [0, 1], - 2: [2], - 3: [3, 4], - 4: [5, 6, 7, 8], - 6: [9, 10], - 8: [11, 12], - 9: [13, 14], - 10: [15, 16, 17], - 11: [18, 19, 20], - 13: [21], - 14: [22, 23], - 16: [24, 25, 26], - 17: [27, 28, 29, 30, 31], - 19: [32, 33], - 22: [34, 35], - 23: [36], - 24: [37], - 27: [38], - 29: [39, 40, 41], - 30: [42, 43], -} - -star_locations = { - 0x770401: LocationName.grass_land_1_s1, - 0x770402: LocationName.grass_land_1_s2, - 0x770403: LocationName.grass_land_1_s3, - 0x770404: LocationName.grass_land_1_s4, - 0x770405: LocationName.grass_land_1_s5, - 0x770406: LocationName.grass_land_1_s6, - 0x770407: LocationName.grass_land_1_s7, - 0x770408: LocationName.grass_land_1_s8, - 0x770409: LocationName.grass_land_1_s9, - 0x77040a: LocationName.grass_land_1_s10, - 0x77040b: LocationName.grass_land_1_s11, - 0x77040c: LocationName.grass_land_1_s12, - 0x77040d: LocationName.grass_land_1_s13, - 0x77040e: LocationName.grass_land_1_s14, - 0x77040f: LocationName.grass_land_1_s15, - 0x770410: LocationName.grass_land_1_s16, - 0x770411: LocationName.grass_land_1_s17, - 0x770412: LocationName.grass_land_1_s18, - 0x770413: LocationName.grass_land_1_s19, - 0x770414: LocationName.grass_land_1_s20, - 0x770415: LocationName.grass_land_1_s21, - 0x770416: LocationName.grass_land_1_s22, - 0x770417: LocationName.grass_land_1_s23, - 0x770418: LocationName.grass_land_2_s1, - 0x770419: LocationName.grass_land_2_s2, - 0x77041a: LocationName.grass_land_2_s3, - 0x77041b: LocationName.grass_land_2_s4, - 0x77041c: LocationName.grass_land_2_s5, - 0x77041d: LocationName.grass_land_2_s6, - 0x77041e: LocationName.grass_land_2_s7, - 0x77041f: LocationName.grass_land_2_s8, - 0x770420: LocationName.grass_land_2_s9, - 0x770421: LocationName.grass_land_2_s10, - 0x770422: LocationName.grass_land_2_s11, - 0x770423: LocationName.grass_land_2_s12, - 0x770424: LocationName.grass_land_2_s13, - 0x770425: LocationName.grass_land_2_s14, - 0x770426: LocationName.grass_land_2_s15, - 0x770427: LocationName.grass_land_2_s16, - 0x770428: LocationName.grass_land_2_s17, - 0x770429: LocationName.grass_land_2_s18, - 0x77042a: LocationName.grass_land_2_s19, - 0x77042b: LocationName.grass_land_2_s20, - 0x77042c: LocationName.grass_land_2_s21, - 0x77042d: LocationName.grass_land_3_s1, - 0x77042e: LocationName.grass_land_3_s2, - 0x77042f: LocationName.grass_land_3_s3, - 0x770430: LocationName.grass_land_3_s4, - 0x770431: LocationName.grass_land_3_s5, - 0x770432: LocationName.grass_land_3_s6, - 0x770433: LocationName.grass_land_3_s7, - 0x770434: LocationName.grass_land_3_s8, - 0x770435: LocationName.grass_land_3_s9, - 0x770436: LocationName.grass_land_3_s10, - 0x770437: LocationName.grass_land_3_s11, - 0x770438: LocationName.grass_land_3_s12, - 0x770439: LocationName.grass_land_3_s13, - 0x77043a: LocationName.grass_land_3_s14, - 0x77043b: LocationName.grass_land_3_s15, - 0x77043c: LocationName.grass_land_3_s16, - 0x77043d: LocationName.grass_land_3_s17, - 0x77043e: LocationName.grass_land_3_s18, - 0x77043f: LocationName.grass_land_3_s19, - 0x770440: LocationName.grass_land_3_s20, - 0x770441: LocationName.grass_land_3_s21, - 0x770442: LocationName.grass_land_3_s22, - 0x770443: LocationName.grass_land_3_s23, - 0x770444: LocationName.grass_land_3_s24, - 0x770445: LocationName.grass_land_3_s25, - 0x770446: LocationName.grass_land_3_s26, - 0x770447: LocationName.grass_land_3_s27, - 0x770448: LocationName.grass_land_3_s28, - 0x770449: LocationName.grass_land_3_s29, - 0x77044a: LocationName.grass_land_3_s30, - 0x77044b: LocationName.grass_land_3_s31, - 0x77044c: LocationName.grass_land_4_s1, - 0x77044d: LocationName.grass_land_4_s2, - 0x77044e: LocationName.grass_land_4_s3, - 0x77044f: LocationName.grass_land_4_s4, - 0x770450: LocationName.grass_land_4_s5, - 0x770451: LocationName.grass_land_4_s6, - 0x770452: LocationName.grass_land_4_s7, - 0x770453: LocationName.grass_land_4_s8, - 0x770454: LocationName.grass_land_4_s9, - 0x770455: LocationName.grass_land_4_s10, - 0x770456: LocationName.grass_land_4_s11, - 0x770457: LocationName.grass_land_4_s12, - 0x770458: LocationName.grass_land_4_s13, - 0x770459: LocationName.grass_land_4_s14, - 0x77045a: LocationName.grass_land_4_s15, - 0x77045b: LocationName.grass_land_4_s16, - 0x77045c: LocationName.grass_land_4_s17, - 0x77045d: LocationName.grass_land_4_s18, - 0x77045e: LocationName.grass_land_4_s19, - 0x77045f: LocationName.grass_land_4_s20, - 0x770460: LocationName.grass_land_4_s21, - 0x770461: LocationName.grass_land_4_s22, - 0x770462: LocationName.grass_land_4_s23, - 0x770463: LocationName.grass_land_4_s24, - 0x770464: LocationName.grass_land_4_s25, - 0x770465: LocationName.grass_land_4_s26, - 0x770466: LocationName.grass_land_4_s27, - 0x770467: LocationName.grass_land_4_s28, - 0x770468: LocationName.grass_land_4_s29, - 0x770469: LocationName.grass_land_4_s30, - 0x77046a: LocationName.grass_land_4_s31, - 0x77046b: LocationName.grass_land_4_s32, - 0x77046c: LocationName.grass_land_4_s33, - 0x77046d: LocationName.grass_land_4_s34, - 0x77046e: LocationName.grass_land_4_s35, - 0x77046f: LocationName.grass_land_4_s36, - 0x770470: LocationName.grass_land_4_s37, - 0x770471: LocationName.grass_land_5_s1, - 0x770472: LocationName.grass_land_5_s2, - 0x770473: LocationName.grass_land_5_s3, - 0x770474: LocationName.grass_land_5_s4, - 0x770475: LocationName.grass_land_5_s5, - 0x770476: LocationName.grass_land_5_s6, - 0x770477: LocationName.grass_land_5_s7, - 0x770478: LocationName.grass_land_5_s8, - 0x770479: LocationName.grass_land_5_s9, - 0x77047a: LocationName.grass_land_5_s10, - 0x77047b: LocationName.grass_land_5_s11, - 0x77047c: LocationName.grass_land_5_s12, - 0x77047d: LocationName.grass_land_5_s13, - 0x77047e: LocationName.grass_land_5_s14, - 0x77047f: LocationName.grass_land_5_s15, - 0x770480: LocationName.grass_land_5_s16, - 0x770481: LocationName.grass_land_5_s17, - 0x770482: LocationName.grass_land_5_s18, - 0x770483: LocationName.grass_land_5_s19, - 0x770484: LocationName.grass_land_5_s20, - 0x770485: LocationName.grass_land_5_s21, - 0x770486: LocationName.grass_land_5_s22, - 0x770487: LocationName.grass_land_5_s23, - 0x770488: LocationName.grass_land_5_s24, - 0x770489: LocationName.grass_land_5_s25, - 0x77048a: LocationName.grass_land_5_s26, - 0x77048b: LocationName.grass_land_5_s27, - 0x77048c: LocationName.grass_land_5_s28, - 0x77048d: LocationName.grass_land_5_s29, - 0x77048e: LocationName.grass_land_6_s1, - 0x77048f: LocationName.grass_land_6_s2, - 0x770490: LocationName.grass_land_6_s3, - 0x770491: LocationName.grass_land_6_s4, - 0x770492: LocationName.grass_land_6_s5, - 0x770493: LocationName.grass_land_6_s6, - 0x770494: LocationName.grass_land_6_s7, - 0x770495: LocationName.grass_land_6_s8, - 0x770496: LocationName.grass_land_6_s9, - 0x770497: LocationName.grass_land_6_s10, - 0x770498: LocationName.grass_land_6_s11, - 0x770499: LocationName.grass_land_6_s12, - 0x77049a: LocationName.grass_land_6_s13, - 0x77049b: LocationName.grass_land_6_s14, - 0x77049c: LocationName.grass_land_6_s15, - 0x77049d: LocationName.grass_land_6_s16, - 0x77049e: LocationName.grass_land_6_s17, - 0x77049f: LocationName.grass_land_6_s18, - 0x7704a0: LocationName.grass_land_6_s19, - 0x7704a1: LocationName.grass_land_6_s20, - 0x7704a2: LocationName.grass_land_6_s21, - 0x7704a3: LocationName.grass_land_6_s22, - 0x7704a4: LocationName.grass_land_6_s23, - 0x7704a5: LocationName.grass_land_6_s24, - 0x7704a6: LocationName.grass_land_6_s25, - 0x7704a7: LocationName.grass_land_6_s26, - 0x7704a8: LocationName.grass_land_6_s27, - 0x7704a9: LocationName.grass_land_6_s28, - 0x7704aa: LocationName.grass_land_6_s29, - 0x7704ab: LocationName.ripple_field_1_s1, - 0x7704ac: LocationName.ripple_field_1_s2, - 0x7704ad: LocationName.ripple_field_1_s3, - 0x7704ae: LocationName.ripple_field_1_s4, - 0x7704af: LocationName.ripple_field_1_s5, - 0x7704b0: LocationName.ripple_field_1_s6, - 0x7704b1: LocationName.ripple_field_1_s7, - 0x7704b2: LocationName.ripple_field_1_s8, - 0x7704b3: LocationName.ripple_field_1_s9, - 0x7704b4: LocationName.ripple_field_1_s10, - 0x7704b5: LocationName.ripple_field_1_s11, - 0x7704b6: LocationName.ripple_field_1_s12, - 0x7704b7: LocationName.ripple_field_1_s13, - 0x7704b8: LocationName.ripple_field_1_s14, - 0x7704b9: LocationName.ripple_field_1_s15, - 0x7704ba: LocationName.ripple_field_1_s16, - 0x7704bb: LocationName.ripple_field_1_s17, - 0x7704bc: LocationName.ripple_field_1_s18, - 0x7704bd: LocationName.ripple_field_1_s19, - 0x7704be: LocationName.ripple_field_2_s1, - 0x7704bf: LocationName.ripple_field_2_s2, - 0x7704c0: LocationName.ripple_field_2_s3, - 0x7704c1: LocationName.ripple_field_2_s4, - 0x7704c2: LocationName.ripple_field_2_s5, - 0x7704c3: LocationName.ripple_field_2_s6, - 0x7704c4: LocationName.ripple_field_2_s7, - 0x7704c5: LocationName.ripple_field_2_s8, - 0x7704c6: LocationName.ripple_field_2_s9, - 0x7704c7: LocationName.ripple_field_2_s10, - 0x7704c8: LocationName.ripple_field_2_s11, - 0x7704c9: LocationName.ripple_field_2_s12, - 0x7704ca: LocationName.ripple_field_2_s13, - 0x7704cb: LocationName.ripple_field_2_s14, - 0x7704cc: LocationName.ripple_field_2_s15, - 0x7704cd: LocationName.ripple_field_2_s16, - 0x7704ce: LocationName.ripple_field_2_s17, - 0x7704cf: LocationName.ripple_field_3_s1, - 0x7704d0: LocationName.ripple_field_3_s2, - 0x7704d1: LocationName.ripple_field_3_s3, - 0x7704d2: LocationName.ripple_field_3_s4, - 0x7704d3: LocationName.ripple_field_3_s5, - 0x7704d4: LocationName.ripple_field_3_s6, - 0x7704d5: LocationName.ripple_field_3_s7, - 0x7704d6: LocationName.ripple_field_3_s8, - 0x7704d7: LocationName.ripple_field_3_s9, - 0x7704d8: LocationName.ripple_field_3_s10, - 0x7704d9: LocationName.ripple_field_3_s11, - 0x7704da: LocationName.ripple_field_3_s12, - 0x7704db: LocationName.ripple_field_3_s13, - 0x7704dc: LocationName.ripple_field_3_s14, - 0x7704dd: LocationName.ripple_field_3_s15, - 0x7704de: LocationName.ripple_field_3_s16, - 0x7704df: LocationName.ripple_field_3_s17, - 0x7704e0: LocationName.ripple_field_3_s18, - 0x7704e1: LocationName.ripple_field_3_s19, - 0x7704e2: LocationName.ripple_field_3_s20, - 0x7704e3: LocationName.ripple_field_3_s21, - 0x7704e4: LocationName.ripple_field_4_s1, - 0x7704e5: LocationName.ripple_field_4_s2, - 0x7704e6: LocationName.ripple_field_4_s3, - 0x7704e7: LocationName.ripple_field_4_s4, - 0x7704e8: LocationName.ripple_field_4_s5, - 0x7704e9: LocationName.ripple_field_4_s6, - 0x7704ea: LocationName.ripple_field_4_s7, - 0x7704eb: LocationName.ripple_field_4_s8, - 0x7704ec: LocationName.ripple_field_4_s9, - 0x7704ed: LocationName.ripple_field_4_s10, - 0x7704ee: LocationName.ripple_field_4_s11, - 0x7704ef: LocationName.ripple_field_4_s12, - 0x7704f0: LocationName.ripple_field_4_s13, - 0x7704f1: LocationName.ripple_field_4_s14, - 0x7704f2: LocationName.ripple_field_4_s15, - 0x7704f3: LocationName.ripple_field_4_s16, - 0x7704f4: LocationName.ripple_field_4_s17, - 0x7704f5: LocationName.ripple_field_4_s18, - 0x7704f6: LocationName.ripple_field_4_s19, - 0x7704f7: LocationName.ripple_field_4_s20, - 0x7704f8: LocationName.ripple_field_4_s21, - 0x7704f9: LocationName.ripple_field_4_s22, - 0x7704fa: LocationName.ripple_field_4_s23, - 0x7704fb: LocationName.ripple_field_4_s24, - 0x7704fc: LocationName.ripple_field_4_s25, - 0x7704fd: LocationName.ripple_field_4_s26, - 0x7704fe: LocationName.ripple_field_4_s27, - 0x7704ff: LocationName.ripple_field_4_s28, - 0x770500: LocationName.ripple_field_4_s29, - 0x770501: LocationName.ripple_field_4_s30, - 0x770502: LocationName.ripple_field_4_s31, - 0x770503: LocationName.ripple_field_4_s32, - 0x770504: LocationName.ripple_field_4_s33, - 0x770505: LocationName.ripple_field_4_s34, - 0x770506: LocationName.ripple_field_4_s35, - 0x770507: LocationName.ripple_field_4_s36, - 0x770508: LocationName.ripple_field_4_s37, - 0x770509: LocationName.ripple_field_4_s38, - 0x77050a: LocationName.ripple_field_4_s39, - 0x77050b: LocationName.ripple_field_4_s40, - 0x77050c: LocationName.ripple_field_4_s41, - 0x77050d: LocationName.ripple_field_4_s42, - 0x77050e: LocationName.ripple_field_4_s43, - 0x77050f: LocationName.ripple_field_4_s44, - 0x770510: LocationName.ripple_field_4_s45, - 0x770511: LocationName.ripple_field_4_s46, - 0x770512: LocationName.ripple_field_4_s47, - 0x770513: LocationName.ripple_field_4_s48, - 0x770514: LocationName.ripple_field_4_s49, - 0x770515: LocationName.ripple_field_4_s50, - 0x770516: LocationName.ripple_field_4_s51, - 0x770517: LocationName.ripple_field_5_s1, - 0x770518: LocationName.ripple_field_5_s2, - 0x770519: LocationName.ripple_field_5_s3, - 0x77051a: LocationName.ripple_field_5_s4, - 0x77051b: LocationName.ripple_field_5_s5, - 0x77051c: LocationName.ripple_field_5_s6, - 0x77051d: LocationName.ripple_field_5_s7, - 0x77051e: LocationName.ripple_field_5_s8, - 0x77051f: LocationName.ripple_field_5_s9, - 0x770520: LocationName.ripple_field_5_s10, - 0x770521: LocationName.ripple_field_5_s11, - 0x770522: LocationName.ripple_field_5_s12, - 0x770523: LocationName.ripple_field_5_s13, - 0x770524: LocationName.ripple_field_5_s14, - 0x770525: LocationName.ripple_field_5_s15, - 0x770526: LocationName.ripple_field_5_s16, - 0x770527: LocationName.ripple_field_5_s17, - 0x770528: LocationName.ripple_field_5_s18, - 0x770529: LocationName.ripple_field_5_s19, - 0x77052a: LocationName.ripple_field_5_s20, - 0x77052b: LocationName.ripple_field_5_s21, - 0x77052c: LocationName.ripple_field_5_s22, - 0x77052d: LocationName.ripple_field_5_s23, - 0x77052e: LocationName.ripple_field_5_s24, - 0x77052f: LocationName.ripple_field_5_s25, - 0x770530: LocationName.ripple_field_5_s26, - 0x770531: LocationName.ripple_field_5_s27, - 0x770532: LocationName.ripple_field_5_s28, - 0x770533: LocationName.ripple_field_5_s29, - 0x770534: LocationName.ripple_field_5_s30, - 0x770535: LocationName.ripple_field_5_s31, - 0x770536: LocationName.ripple_field_5_s32, - 0x770537: LocationName.ripple_field_5_s33, - 0x770538: LocationName.ripple_field_5_s34, - 0x770539: LocationName.ripple_field_5_s35, - 0x77053a: LocationName.ripple_field_5_s36, - 0x77053b: LocationName.ripple_field_5_s37, - 0x77053c: LocationName.ripple_field_5_s38, - 0x77053d: LocationName.ripple_field_5_s39, - 0x77053e: LocationName.ripple_field_5_s40, - 0x77053f: LocationName.ripple_field_5_s41, - 0x770540: LocationName.ripple_field_5_s42, - 0x770541: LocationName.ripple_field_5_s43, - 0x770542: LocationName.ripple_field_5_s44, - 0x770543: LocationName.ripple_field_5_s45, - 0x770544: LocationName.ripple_field_5_s46, - 0x770545: LocationName.ripple_field_5_s47, - 0x770546: LocationName.ripple_field_5_s48, - 0x770547: LocationName.ripple_field_5_s49, - 0x770548: LocationName.ripple_field_5_s50, - 0x770549: LocationName.ripple_field_5_s51, - 0x77054a: LocationName.ripple_field_6_s1, - 0x77054b: LocationName.ripple_field_6_s2, - 0x77054c: LocationName.ripple_field_6_s3, - 0x77054d: LocationName.ripple_field_6_s4, - 0x77054e: LocationName.ripple_field_6_s5, - 0x77054f: LocationName.ripple_field_6_s6, - 0x770550: LocationName.ripple_field_6_s7, - 0x770551: LocationName.ripple_field_6_s8, - 0x770552: LocationName.ripple_field_6_s9, - 0x770553: LocationName.ripple_field_6_s10, - 0x770554: LocationName.ripple_field_6_s11, - 0x770555: LocationName.ripple_field_6_s12, - 0x770556: LocationName.ripple_field_6_s13, - 0x770557: LocationName.ripple_field_6_s14, - 0x770558: LocationName.ripple_field_6_s15, - 0x770559: LocationName.ripple_field_6_s16, - 0x77055a: LocationName.ripple_field_6_s17, - 0x77055b: LocationName.ripple_field_6_s18, - 0x77055c: LocationName.ripple_field_6_s19, - 0x77055d: LocationName.ripple_field_6_s20, - 0x77055e: LocationName.ripple_field_6_s21, - 0x77055f: LocationName.ripple_field_6_s22, - 0x770560: LocationName.ripple_field_6_s23, - 0x770561: LocationName.sand_canyon_1_s1, - 0x770562: LocationName.sand_canyon_1_s2, - 0x770563: LocationName.sand_canyon_1_s3, - 0x770564: LocationName.sand_canyon_1_s4, - 0x770565: LocationName.sand_canyon_1_s5, - 0x770566: LocationName.sand_canyon_1_s6, - 0x770567: LocationName.sand_canyon_1_s7, - 0x770568: LocationName.sand_canyon_1_s8, - 0x770569: LocationName.sand_canyon_1_s9, - 0x77056a: LocationName.sand_canyon_1_s10, - 0x77056b: LocationName.sand_canyon_1_s11, - 0x77056c: LocationName.sand_canyon_1_s12, - 0x77056d: LocationName.sand_canyon_1_s13, - 0x77056e: LocationName.sand_canyon_1_s14, - 0x77056f: LocationName.sand_canyon_1_s15, - 0x770570: LocationName.sand_canyon_1_s16, - 0x770571: LocationName.sand_canyon_1_s17, - 0x770572: LocationName.sand_canyon_1_s18, - 0x770573: LocationName.sand_canyon_1_s19, - 0x770574: LocationName.sand_canyon_1_s20, - 0x770575: LocationName.sand_canyon_1_s21, - 0x770576: LocationName.sand_canyon_1_s22, - 0x770577: LocationName.sand_canyon_2_s1, - 0x770578: LocationName.sand_canyon_2_s2, - 0x770579: LocationName.sand_canyon_2_s3, - 0x77057a: LocationName.sand_canyon_2_s4, - 0x77057b: LocationName.sand_canyon_2_s5, - 0x77057c: LocationName.sand_canyon_2_s6, - 0x77057d: LocationName.sand_canyon_2_s7, - 0x77057e: LocationName.sand_canyon_2_s8, - 0x77057f: LocationName.sand_canyon_2_s9, - 0x770580: LocationName.sand_canyon_2_s10, - 0x770581: LocationName.sand_canyon_2_s11, - 0x770582: LocationName.sand_canyon_2_s12, - 0x770583: LocationName.sand_canyon_2_s13, - 0x770584: LocationName.sand_canyon_2_s14, - 0x770585: LocationName.sand_canyon_2_s15, - 0x770586: LocationName.sand_canyon_2_s16, - 0x770587: LocationName.sand_canyon_2_s17, - 0x770588: LocationName.sand_canyon_2_s18, - 0x770589: LocationName.sand_canyon_2_s19, - 0x77058a: LocationName.sand_canyon_2_s20, - 0x77058b: LocationName.sand_canyon_2_s21, - 0x77058c: LocationName.sand_canyon_2_s22, - 0x77058d: LocationName.sand_canyon_2_s23, - 0x77058e: LocationName.sand_canyon_2_s24, - 0x77058f: LocationName.sand_canyon_2_s25, - 0x770590: LocationName.sand_canyon_2_s26, - 0x770591: LocationName.sand_canyon_2_s27, - 0x770592: LocationName.sand_canyon_2_s28, - 0x770593: LocationName.sand_canyon_2_s29, - 0x770594: LocationName.sand_canyon_2_s30, - 0x770595: LocationName.sand_canyon_2_s31, - 0x770596: LocationName.sand_canyon_2_s32, - 0x770597: LocationName.sand_canyon_2_s33, - 0x770598: LocationName.sand_canyon_2_s34, - 0x770599: LocationName.sand_canyon_2_s35, - 0x77059a: LocationName.sand_canyon_2_s36, - 0x77059b: LocationName.sand_canyon_2_s37, - 0x77059c: LocationName.sand_canyon_2_s38, - 0x77059d: LocationName.sand_canyon_2_s39, - 0x77059e: LocationName.sand_canyon_2_s40, - 0x77059f: LocationName.sand_canyon_2_s41, - 0x7705a0: LocationName.sand_canyon_2_s42, - 0x7705a1: LocationName.sand_canyon_2_s43, - 0x7705a2: LocationName.sand_canyon_2_s44, - 0x7705a3: LocationName.sand_canyon_2_s45, - 0x7705a4: LocationName.sand_canyon_2_s46, - 0x7705a5: LocationName.sand_canyon_2_s47, - 0x7705a6: LocationName.sand_canyon_2_s48, - 0x7705a7: LocationName.sand_canyon_3_s1, - 0x7705a8: LocationName.sand_canyon_3_s2, - 0x7705a9: LocationName.sand_canyon_3_s3, - 0x7705aa: LocationName.sand_canyon_3_s4, - 0x7705ab: LocationName.sand_canyon_3_s5, - 0x7705ac: LocationName.sand_canyon_3_s6, - 0x7705ad: LocationName.sand_canyon_3_s7, - 0x7705ae: LocationName.sand_canyon_3_s8, - 0x7705af: LocationName.sand_canyon_3_s9, - 0x7705b0: LocationName.sand_canyon_3_s10, - 0x7705b1: LocationName.sand_canyon_4_s1, - 0x7705b2: LocationName.sand_canyon_4_s2, - 0x7705b3: LocationName.sand_canyon_4_s3, - 0x7705b4: LocationName.sand_canyon_4_s4, - 0x7705b5: LocationName.sand_canyon_4_s5, - 0x7705b6: LocationName.sand_canyon_4_s6, - 0x7705b7: LocationName.sand_canyon_4_s7, - 0x7705b8: LocationName.sand_canyon_4_s8, - 0x7705b9: LocationName.sand_canyon_4_s9, - 0x7705ba: LocationName.sand_canyon_4_s10, - 0x7705bb: LocationName.sand_canyon_4_s11, - 0x7705bc: LocationName.sand_canyon_4_s12, - 0x7705bd: LocationName.sand_canyon_4_s13, - 0x7705be: LocationName.sand_canyon_4_s14, - 0x7705bf: LocationName.sand_canyon_4_s15, - 0x7705c0: LocationName.sand_canyon_4_s16, - 0x7705c1: LocationName.sand_canyon_4_s17, - 0x7705c2: LocationName.sand_canyon_4_s18, - 0x7705c3: LocationName.sand_canyon_4_s19, - 0x7705c4: LocationName.sand_canyon_4_s20, - 0x7705c5: LocationName.sand_canyon_4_s21, - 0x7705c6: LocationName.sand_canyon_4_s22, - 0x7705c7: LocationName.sand_canyon_4_s23, - 0x7705c8: LocationName.sand_canyon_5_s1, - 0x7705c9: LocationName.sand_canyon_5_s2, - 0x7705ca: LocationName.sand_canyon_5_s3, - 0x7705cb: LocationName.sand_canyon_5_s4, - 0x7705cc: LocationName.sand_canyon_5_s5, - 0x7705cd: LocationName.sand_canyon_5_s6, - 0x7705ce: LocationName.sand_canyon_5_s7, - 0x7705cf: LocationName.sand_canyon_5_s8, - 0x7705d0: LocationName.sand_canyon_5_s9, - 0x7705d1: LocationName.sand_canyon_5_s10, - 0x7705d2: LocationName.sand_canyon_5_s11, - 0x7705d3: LocationName.sand_canyon_5_s12, - 0x7705d4: LocationName.sand_canyon_5_s13, - 0x7705d5: LocationName.sand_canyon_5_s14, - 0x7705d6: LocationName.sand_canyon_5_s15, - 0x7705d7: LocationName.sand_canyon_5_s16, - 0x7705d8: LocationName.sand_canyon_5_s17, - 0x7705d9: LocationName.sand_canyon_5_s18, - 0x7705da: LocationName.sand_canyon_5_s19, - 0x7705db: LocationName.sand_canyon_5_s20, - 0x7705dc: LocationName.sand_canyon_5_s21, - 0x7705dd: LocationName.sand_canyon_5_s22, - 0x7705de: LocationName.sand_canyon_5_s23, - 0x7705df: LocationName.sand_canyon_5_s24, - 0x7705e0: LocationName.sand_canyon_5_s25, - 0x7705e1: LocationName.sand_canyon_5_s26, - 0x7705e2: LocationName.sand_canyon_5_s27, - 0x7705e3: LocationName.sand_canyon_5_s28, - 0x7705e4: LocationName.sand_canyon_5_s29, - 0x7705e5: LocationName.sand_canyon_5_s30, - 0x7705e6: LocationName.sand_canyon_5_s31, - 0x7705e7: LocationName.sand_canyon_5_s32, - 0x7705e8: LocationName.sand_canyon_5_s33, - 0x7705e9: LocationName.sand_canyon_5_s34, - 0x7705ea: LocationName.sand_canyon_5_s35, - 0x7705eb: LocationName.sand_canyon_5_s36, - 0x7705ec: LocationName.sand_canyon_5_s37, - 0x7705ed: LocationName.sand_canyon_5_s38, - 0x7705ee: LocationName.sand_canyon_5_s39, - 0x7705ef: LocationName.sand_canyon_5_s40, - 0x7705f0: LocationName.cloudy_park_1_s1, - 0x7705f1: LocationName.cloudy_park_1_s2, - 0x7705f2: LocationName.cloudy_park_1_s3, - 0x7705f3: LocationName.cloudy_park_1_s4, - 0x7705f4: LocationName.cloudy_park_1_s5, - 0x7705f5: LocationName.cloudy_park_1_s6, - 0x7705f6: LocationName.cloudy_park_1_s7, - 0x7705f7: LocationName.cloudy_park_1_s8, - 0x7705f8: LocationName.cloudy_park_1_s9, - 0x7705f9: LocationName.cloudy_park_1_s10, - 0x7705fa: LocationName.cloudy_park_1_s11, - 0x7705fb: LocationName.cloudy_park_1_s12, - 0x7705fc: LocationName.cloudy_park_1_s13, - 0x7705fd: LocationName.cloudy_park_1_s14, - 0x7705fe: LocationName.cloudy_park_1_s15, - 0x7705ff: LocationName.cloudy_park_1_s16, - 0x770600: LocationName.cloudy_park_1_s17, - 0x770601: LocationName.cloudy_park_1_s18, - 0x770602: LocationName.cloudy_park_1_s19, - 0x770603: LocationName.cloudy_park_1_s20, - 0x770604: LocationName.cloudy_park_1_s21, - 0x770605: LocationName.cloudy_park_1_s22, - 0x770606: LocationName.cloudy_park_1_s23, - 0x770607: LocationName.cloudy_park_2_s1, - 0x770608: LocationName.cloudy_park_2_s2, - 0x770609: LocationName.cloudy_park_2_s3, - 0x77060a: LocationName.cloudy_park_2_s4, - 0x77060b: LocationName.cloudy_park_2_s5, - 0x77060c: LocationName.cloudy_park_2_s6, - 0x77060d: LocationName.cloudy_park_2_s7, - 0x77060e: LocationName.cloudy_park_2_s8, - 0x77060f: LocationName.cloudy_park_2_s9, - 0x770610: LocationName.cloudy_park_2_s10, - 0x770611: LocationName.cloudy_park_2_s11, - 0x770612: LocationName.cloudy_park_2_s12, - 0x770613: LocationName.cloudy_park_2_s13, - 0x770614: LocationName.cloudy_park_2_s14, - 0x770615: LocationName.cloudy_park_2_s15, - 0x770616: LocationName.cloudy_park_2_s16, - 0x770617: LocationName.cloudy_park_2_s17, - 0x770618: LocationName.cloudy_park_2_s18, - 0x770619: LocationName.cloudy_park_2_s19, - 0x77061a: LocationName.cloudy_park_2_s20, - 0x77061b: LocationName.cloudy_park_2_s21, - 0x77061c: LocationName.cloudy_park_2_s22, - 0x77061d: LocationName.cloudy_park_2_s23, - 0x77061e: LocationName.cloudy_park_2_s24, - 0x77061f: LocationName.cloudy_park_2_s25, - 0x770620: LocationName.cloudy_park_2_s26, - 0x770621: LocationName.cloudy_park_2_s27, - 0x770622: LocationName.cloudy_park_2_s28, - 0x770623: LocationName.cloudy_park_2_s29, - 0x770624: LocationName.cloudy_park_2_s30, - 0x770625: LocationName.cloudy_park_2_s31, - 0x770626: LocationName.cloudy_park_2_s32, - 0x770627: LocationName.cloudy_park_2_s33, - 0x770628: LocationName.cloudy_park_2_s34, - 0x770629: LocationName.cloudy_park_2_s35, - 0x77062a: LocationName.cloudy_park_2_s36, - 0x77062b: LocationName.cloudy_park_2_s37, - 0x77062c: LocationName.cloudy_park_2_s38, - 0x77062d: LocationName.cloudy_park_2_s39, - 0x77062e: LocationName.cloudy_park_2_s40, - 0x77062f: LocationName.cloudy_park_2_s41, - 0x770630: LocationName.cloudy_park_2_s42, - 0x770631: LocationName.cloudy_park_2_s43, - 0x770632: LocationName.cloudy_park_2_s44, - 0x770633: LocationName.cloudy_park_2_s45, - 0x770634: LocationName.cloudy_park_2_s46, - 0x770635: LocationName.cloudy_park_2_s47, - 0x770636: LocationName.cloudy_park_2_s48, - 0x770637: LocationName.cloudy_park_2_s49, - 0x770638: LocationName.cloudy_park_2_s50, - 0x770639: LocationName.cloudy_park_2_s51, - 0x77063a: LocationName.cloudy_park_2_s52, - 0x77063b: LocationName.cloudy_park_2_s53, - 0x77063c: LocationName.cloudy_park_2_s54, - 0x77063d: LocationName.cloudy_park_3_s1, - 0x77063e: LocationName.cloudy_park_3_s2, - 0x77063f: LocationName.cloudy_park_3_s3, - 0x770640: LocationName.cloudy_park_3_s4, - 0x770641: LocationName.cloudy_park_3_s5, - 0x770642: LocationName.cloudy_park_3_s6, - 0x770643: LocationName.cloudy_park_3_s7, - 0x770644: LocationName.cloudy_park_3_s8, - 0x770645: LocationName.cloudy_park_3_s9, - 0x770646: LocationName.cloudy_park_3_s10, - 0x770647: LocationName.cloudy_park_3_s11, - 0x770648: LocationName.cloudy_park_3_s12, - 0x770649: LocationName.cloudy_park_3_s13, - 0x77064a: LocationName.cloudy_park_3_s14, - 0x77064b: LocationName.cloudy_park_3_s15, - 0x77064c: LocationName.cloudy_park_3_s16, - 0x77064d: LocationName.cloudy_park_3_s17, - 0x77064e: LocationName.cloudy_park_3_s18, - 0x77064f: LocationName.cloudy_park_3_s19, - 0x770650: LocationName.cloudy_park_3_s20, - 0x770651: LocationName.cloudy_park_3_s21, - 0x770652: LocationName.cloudy_park_3_s22, - 0x770653: LocationName.cloudy_park_4_s1, - 0x770654: LocationName.cloudy_park_4_s2, - 0x770655: LocationName.cloudy_park_4_s3, - 0x770656: LocationName.cloudy_park_4_s4, - 0x770657: LocationName.cloudy_park_4_s5, - 0x770658: LocationName.cloudy_park_4_s6, - 0x770659: LocationName.cloudy_park_4_s7, - 0x77065a: LocationName.cloudy_park_4_s8, - 0x77065b: LocationName.cloudy_park_4_s9, - 0x77065c: LocationName.cloudy_park_4_s10, - 0x77065d: LocationName.cloudy_park_4_s11, - 0x77065e: LocationName.cloudy_park_4_s12, - 0x77065f: LocationName.cloudy_park_4_s13, - 0x770660: LocationName.cloudy_park_4_s14, - 0x770661: LocationName.cloudy_park_4_s15, - 0x770662: LocationName.cloudy_park_4_s16, - 0x770663: LocationName.cloudy_park_4_s17, - 0x770664: LocationName.cloudy_park_4_s18, - 0x770665: LocationName.cloudy_park_4_s19, - 0x770666: LocationName.cloudy_park_4_s20, - 0x770667: LocationName.cloudy_park_4_s21, - 0x770668: LocationName.cloudy_park_4_s22, - 0x770669: LocationName.cloudy_park_4_s23, - 0x77066a: LocationName.cloudy_park_4_s24, - 0x77066b: LocationName.cloudy_park_4_s25, - 0x77066c: LocationName.cloudy_park_4_s26, - 0x77066d: LocationName.cloudy_park_4_s27, - 0x77066e: LocationName.cloudy_park_4_s28, - 0x77066f: LocationName.cloudy_park_4_s29, - 0x770670: LocationName.cloudy_park_4_s30, - 0x770671: LocationName.cloudy_park_4_s31, - 0x770672: LocationName.cloudy_park_4_s32, - 0x770673: LocationName.cloudy_park_4_s33, - 0x770674: LocationName.cloudy_park_4_s34, - 0x770675: LocationName.cloudy_park_4_s35, - 0x770676: LocationName.cloudy_park_4_s36, - 0x770677: LocationName.cloudy_park_4_s37, - 0x770678: LocationName.cloudy_park_4_s38, - 0x770679: LocationName.cloudy_park_4_s39, - 0x77067a: LocationName.cloudy_park_4_s40, - 0x77067b: LocationName.cloudy_park_4_s41, - 0x77067c: LocationName.cloudy_park_4_s42, - 0x77067d: LocationName.cloudy_park_4_s43, - 0x77067e: LocationName.cloudy_park_4_s44, - 0x77067f: LocationName.cloudy_park_4_s45, - 0x770680: LocationName.cloudy_park_4_s46, - 0x770681: LocationName.cloudy_park_4_s47, - 0x770682: LocationName.cloudy_park_4_s48, - 0x770683: LocationName.cloudy_park_4_s49, - 0x770684: LocationName.cloudy_park_4_s50, - 0x770685: LocationName.cloudy_park_5_s1, - 0x770686: LocationName.cloudy_park_5_s2, - 0x770687: LocationName.cloudy_park_5_s3, - 0x770688: LocationName.cloudy_park_5_s4, - 0x770689: LocationName.cloudy_park_5_s5, - 0x77068a: LocationName.cloudy_park_5_s6, - 0x77068b: LocationName.cloudy_park_6_s1, - 0x77068c: LocationName.cloudy_park_6_s2, - 0x77068d: LocationName.cloudy_park_6_s3, - 0x77068e: LocationName.cloudy_park_6_s4, - 0x77068f: LocationName.cloudy_park_6_s5, - 0x770690: LocationName.cloudy_park_6_s6, - 0x770691: LocationName.cloudy_park_6_s7, - 0x770692: LocationName.cloudy_park_6_s8, - 0x770693: LocationName.cloudy_park_6_s9, - 0x770694: LocationName.cloudy_park_6_s10, - 0x770695: LocationName.cloudy_park_6_s11, - 0x770696: LocationName.cloudy_park_6_s12, - 0x770697: LocationName.cloudy_park_6_s13, - 0x770698: LocationName.cloudy_park_6_s14, - 0x770699: LocationName.cloudy_park_6_s15, - 0x77069a: LocationName.cloudy_park_6_s16, - 0x77069b: LocationName.cloudy_park_6_s17, - 0x77069c: LocationName.cloudy_park_6_s18, - 0x77069d: LocationName.cloudy_park_6_s19, - 0x77069e: LocationName.cloudy_park_6_s20, - 0x77069f: LocationName.cloudy_park_6_s21, - 0x7706a0: LocationName.cloudy_park_6_s22, - 0x7706a1: LocationName.cloudy_park_6_s23, - 0x7706a2: LocationName.cloudy_park_6_s24, - 0x7706a3: LocationName.cloudy_park_6_s25, - 0x7706a4: LocationName.cloudy_park_6_s26, - 0x7706a5: LocationName.cloudy_park_6_s27, - 0x7706a6: LocationName.cloudy_park_6_s28, - 0x7706a7: LocationName.cloudy_park_6_s29, - 0x7706a8: LocationName.cloudy_park_6_s30, - 0x7706a9: LocationName.cloudy_park_6_s31, - 0x7706aa: LocationName.cloudy_park_6_s32, - 0x7706ab: LocationName.cloudy_park_6_s33, - 0x7706ac: LocationName.iceberg_1_s1, - 0x7706ad: LocationName.iceberg_1_s2, - 0x7706ae: LocationName.iceberg_1_s3, - 0x7706af: LocationName.iceberg_1_s4, - 0x7706b0: LocationName.iceberg_1_s5, - 0x7706b1: LocationName.iceberg_1_s6, - 0x7706b2: LocationName.iceberg_2_s1, - 0x7706b3: LocationName.iceberg_2_s2, - 0x7706b4: LocationName.iceberg_2_s3, - 0x7706b5: LocationName.iceberg_2_s4, - 0x7706b6: LocationName.iceberg_2_s5, - 0x7706b7: LocationName.iceberg_2_s6, - 0x7706b8: LocationName.iceberg_2_s7, - 0x7706b9: LocationName.iceberg_2_s8, - 0x7706ba: LocationName.iceberg_2_s9, - 0x7706bb: LocationName.iceberg_2_s10, - 0x7706bc: LocationName.iceberg_2_s11, - 0x7706bd: LocationName.iceberg_2_s12, - 0x7706be: LocationName.iceberg_2_s13, - 0x7706bf: LocationName.iceberg_2_s14, - 0x7706c0: LocationName.iceberg_2_s15, - 0x7706c1: LocationName.iceberg_2_s16, - 0x7706c2: LocationName.iceberg_2_s17, - 0x7706c3: LocationName.iceberg_2_s18, - 0x7706c4: LocationName.iceberg_2_s19, - 0x7706c5: LocationName.iceberg_3_s1, - 0x7706c6: LocationName.iceberg_3_s2, - 0x7706c7: LocationName.iceberg_3_s3, - 0x7706c8: LocationName.iceberg_3_s4, - 0x7706c9: LocationName.iceberg_3_s5, - 0x7706ca: LocationName.iceberg_3_s6, - 0x7706cb: LocationName.iceberg_3_s7, - 0x7706cc: LocationName.iceberg_3_s8, - 0x7706cd: LocationName.iceberg_3_s9, - 0x7706ce: LocationName.iceberg_3_s10, - 0x7706cf: LocationName.iceberg_3_s11, - 0x7706d0: LocationName.iceberg_3_s12, - 0x7706d1: LocationName.iceberg_3_s13, - 0x7706d2: LocationName.iceberg_3_s14, - 0x7706d3: LocationName.iceberg_3_s15, - 0x7706d4: LocationName.iceberg_3_s16, - 0x7706d5: LocationName.iceberg_3_s17, - 0x7706d6: LocationName.iceberg_3_s18, - 0x7706d7: LocationName.iceberg_3_s19, - 0x7706d8: LocationName.iceberg_3_s20, - 0x7706d9: LocationName.iceberg_3_s21, - 0x7706da: LocationName.iceberg_4_s1, - 0x7706db: LocationName.iceberg_4_s2, - 0x7706dc: LocationName.iceberg_4_s3, - 0x7706dd: LocationName.iceberg_5_s1, - 0x7706de: LocationName.iceberg_5_s2, - 0x7706df: LocationName.iceberg_5_s3, - 0x7706e0: LocationName.iceberg_5_s4, - 0x7706e1: LocationName.iceberg_5_s5, - 0x7706e2: LocationName.iceberg_5_s6, - 0x7706e3: LocationName.iceberg_5_s7, - 0x7706e4: LocationName.iceberg_5_s8, - 0x7706e5: LocationName.iceberg_5_s9, - 0x7706e6: LocationName.iceberg_5_s10, - 0x7706e7: LocationName.iceberg_5_s11, - 0x7706e8: LocationName.iceberg_5_s12, - 0x7706e9: LocationName.iceberg_5_s13, - 0x7706ea: LocationName.iceberg_5_s14, - 0x7706eb: LocationName.iceberg_5_s15, - 0x7706ec: LocationName.iceberg_5_s16, - 0x7706ed: LocationName.iceberg_5_s17, - 0x7706ee: LocationName.iceberg_5_s18, - 0x7706ef: LocationName.iceberg_5_s19, - 0x7706f0: LocationName.iceberg_5_s20, - 0x7706f1: LocationName.iceberg_5_s21, - 0x7706f2: LocationName.iceberg_5_s22, - 0x7706f3: LocationName.iceberg_5_s23, - 0x7706f4: LocationName.iceberg_5_s24, - 0x7706f5: LocationName.iceberg_5_s25, - 0x7706f6: LocationName.iceberg_5_s26, - 0x7706f7: LocationName.iceberg_5_s27, - 0x7706f8: LocationName.iceberg_5_s28, - 0x7706f9: LocationName.iceberg_5_s29, - 0x7706fa: LocationName.iceberg_5_s30, - 0x7706fb: LocationName.iceberg_5_s31, - 0x7706fc: LocationName.iceberg_5_s32, - 0x7706fd: LocationName.iceberg_5_s33, - 0x7706fe: LocationName.iceberg_5_s34, - 0x7706ff: LocationName.iceberg_6_s1, - -} - -location_table = { - **stage_locations, - **heart_star_locations, - **boss_locations, - **consumable_locations, - **star_locations -} diff --git a/worlds/kdl3/Rom.py b/worlds/kdl3/Rom.py deleted file mode 100644 index 5a846ab8be..0000000000 --- a/worlds/kdl3/Rom.py +++ /dev/null @@ -1,577 +0,0 @@ -import typing -from pkgutil import get_data - -import Utils -from typing import Optional, TYPE_CHECKING -import hashlib -import os -import struct - -import settings -from worlds.Files import APDeltaPatch -from .Aesthetics import get_palette_bytes, kirby_target_palettes, get_kirby_palette, gooey_target_palettes, \ - get_gooey_palette -from .Compression import hal_decompress -import bsdiff4 - -if TYPE_CHECKING: - from . import KDL3World - -KDL3UHASH = "201e7658f6194458a3869dde36bf8ec2" -KDL3JHASH = "b2f2d004ea640c3db66df958fce122b2" - -level_pointers = { - 0x770001: 0x0084, - 0x770002: 0x009C, - 0x770003: 0x00B8, - 0x770004: 0x00D8, - 0x770005: 0x0104, - 0x770006: 0x0124, - 0x770007: 0x014C, - 0x770008: 0x0170, - 0x770009: 0x0190, - 0x77000A: 0x01B0, - 0x77000B: 0x01E8, - 0x77000C: 0x0218, - 0x77000D: 0x024C, - 0x77000E: 0x0270, - 0x77000F: 0x02A0, - 0x770010: 0x02C4, - 0x770011: 0x02EC, - 0x770012: 0x0314, - 0x770013: 0x03CC, - 0x770014: 0x0404, - 0x770015: 0x042C, - 0x770016: 0x044C, - 0x770017: 0x0478, - 0x770018: 0x049C, - 0x770019: 0x04E4, - 0x77001A: 0x0504, - 0x77001B: 0x0530, - 0x77001C: 0x0554, - 0x77001D: 0x05A8, - 0x77001E: 0x0640, - 0x770200: 0x0148, - 0x770201: 0x0248, - 0x770202: 0x03C8, - 0x770203: 0x04E0, - 0x770204: 0x06A4, - 0x770205: 0x06A8, -} - -bb_bosses = { - 0x770200: 0xED85F1, - 0x770201: 0xF01360, - 0x770202: 0xEDA3DF, - 0x770203: 0xEDC2B9, - 0x770204: 0xED7C3F, - 0x770205: 0xEC29D2, -} - -level_sprites = { - 0x19B2C6: 1827, - 0x1A195C: 1584, - 0x19F6F3: 1679, - 0x19DC8B: 1717, - 0x197900: 1872 -} - -stage_tiles = { - 0: [ - 0, 1, 2, - 16, 17, 18, - 32, 33, 34, - 48, 49, 50 - ], - 1: [ - 3, 4, 5, - 19, 20, 21, - 35, 36, 37, - 51, 52, 53 - ], - 2: [ - 6, 7, 8, - 22, 23, 24, - 38, 39, 40, - 54, 55, 56 - ], - 3: [ - 9, 10, 11, - 25, 26, 27, - 41, 42, 43, - 57, 58, 59, - ], - 4: [ - 12, 13, 64, - 28, 29, 65, - 44, 45, 66, - 60, 61, 67 - ], - 5: [ - 14, 15, 68, - 30, 31, 69, - 46, 47, 70, - 62, 63, 71 - ] -} - -heart_star_address = 0x2D0000 -heart_star_size = 456 -consumable_address = 0x2F91DD -consumable_size = 698 - -stage_palettes = [0x60964, 0x60B64, 0x60D64, 0x60F64, 0x61164] - -music_choices = [ - 2, # Boss 1 - 3, # Boss 2 (Unused) - 4, # Boss 3 (Miniboss) - 7, # Dedede - 9, # Event 2 (used once) - 10, # Field 1 - 11, # Field 2 - 12, # Field 3 - 13, # Field 4 - 14, # Field 5 - 15, # Field 6 - 16, # Field 7 - 17, # Field 8 - 18, # Field 9 - 19, # Field 10 - 20, # Field 11 - 21, # Field 12 (Gourmet Race) - 23, # Dark Matter in the Hyper Zone - 24, # Zero - 25, # Level 1 - 26, # Level 2 - 27, # Level 4 - 28, # Level 3 - 29, # Heart Star Failed - 30, # Level 5 - 31, # Minigame - 38, # Animal Friend 1 - 39, # Animal Friend 2 - 40, # Animal Friend 3 -] -# extra room pointers we don't want to track other than for music -room_pointers = [ - 3079990, # Zero - 2983409, # BB Whispy - 3150688, # BB Acro - 2991071, # BB PonCon - 2998969, # BB Ado - 2980927, # BB Dedede - 2894290 # BB Zero -] - -enemy_remap = { - "Waddle Dee": 0, - "Bronto Burt": 2, - "Rocky": 3, - "Bobo": 5, - "Chilly": 6, - "Poppy Bros Jr.": 7, - "Sparky": 8, - "Polof": 9, - "Broom Hatter": 11, - "Cappy": 12, - "Bouncy": 13, - "Nruff": 15, - "Glunk": 16, - "Togezo": 18, - "Kabu": 19, - "Mony": 20, - "Blipper": 21, - "Squishy": 22, - "Gabon": 24, - "Oro": 25, - "Galbo": 26, - "Sir Kibble": 27, - "Nidoo": 28, - "Kany": 29, - "Sasuke": 30, - "Yaban": 32, - "Boten": 33, - "Coconut": 34, - "Doka": 35, - "Icicle": 36, - "Pteran": 39, - "Loud": 40, - "Como": 41, - "Klinko": 42, - "Babut": 43, - "Wappa": 44, - "Mariel": 45, - "Tick": 48, - "Apolo": 49, - "Popon Ball": 50, - "KeKe": 51, - "Magoo": 53, - "Raft Waddle Dee": 57, - "Madoo": 58, - "Corori": 60, - "Kapar": 67, - "Batamon": 68, - "Peran": 72, - "Bobin": 73, - "Mopoo": 74, - "Gansan": 75, - "Bukiset (Burning)": 76, - "Bukiset (Stone)": 77, - "Bukiset (Ice)": 78, - "Bukiset (Needle)": 79, - "Bukiset (Clean)": 80, - "Bukiset (Parasol)": 81, - "Bukiset (Spark)": 82, - "Bukiset (Cutter)": 83, - "Waddle Dee Drawing": 84, - "Bronto Burt Drawing": 85, - "Bouncy Drawing": 86, - "Kabu (Dekabu)": 87, - "Wapod": 88, - "Propeller": 89, - "Dogon": 90, - "Joe": 91 -} - -miniboss_remap = { - "Captain Stitch": 0, - "Yuki": 1, - "Blocky": 2, - "Jumper Shoot": 3, - "Boboo": 4, - "Haboki": 5 -} - -ability_remap = { - "No Ability": 0, - "Burning Ability": 1, - "Stone Ability": 2, - "Ice Ability": 3, - "Needle Ability": 4, - "Clean Ability": 5, - "Parasol Ability": 6, - "Spark Ability": 7, - "Cutter Ability": 8, -} - - -class RomData: - def __init__(self, file: str, name: typing.Optional[str] = None): - self.file = bytearray() - self.read_from_file(file) - self.name = name - - def read_byte(self, offset: int): - return self.file[offset] - - def read_bytes(self, offset: int, length: int): - return self.file[offset:offset + length] - - def write_byte(self, offset: int, value: int): - self.file[offset] = value - - def write_bytes(self, offset: int, values: typing.Sequence) -> None: - self.file[offset:offset + len(values)] = values - - def write_to_file(self, file: str): - with open(file, 'wb') as outfile: - outfile.write(self.file) - - def read_from_file(self, file: str): - with open(file, 'rb') as stream: - self.file = bytearray(stream.read()) - - def apply_patch(self, patch: bytes): - self.file = bytearray(bsdiff4.patch(bytes(self.file), patch)) - - def write_crc(self): - crc = (sum(self.file[:0x7FDC] + self.file[0x7FE0:]) + 0x01FE) & 0xFFFF - inv = crc ^ 0xFFFF - self.write_bytes(0x7FDC, [inv & 0xFF, (inv >> 8) & 0xFF, crc & 0xFF, (crc >> 8) & 0xFF]) - - -def handle_level_sprites(stages, sprites, palettes): - palette_by_level = list() - for palette in palettes: - palette_by_level.extend(palette[10:16]) - for i in range(5): - for j in range(6): - palettes[i][10 + j] = palette_by_level[stages[i][j] - 1] - palettes[i] = [x for palette in palettes[i] for x in palette] - tiles_by_level = list() - for spritesheet in sprites: - decompressed = hal_decompress(spritesheet) - tiles = [decompressed[i:i + 32] for i in range(0, 2304, 32)] - tiles_by_level.extend([[tiles[x] for x in stage_tiles[stage]] for stage in stage_tiles]) - for world in range(5): - levels = [stages[world][x] - 1 for x in range(6)] - world_tiles: typing.List[typing.Optional[bytes]] = [None for _ in range(72)] - for i in range(6): - for x in range(12): - world_tiles[stage_tiles[i][x]] = tiles_by_level[levels[i]][x] - sprites[world] = list() - for tile in world_tiles: - sprites[world].extend(tile) - # insert our fake compression - sprites[world][0:0] = [0xe3, 0xff] - sprites[world][1026:1026] = [0xe3, 0xff] - sprites[world][2052:2052] = [0xe0, 0xff] - sprites[world].append(0xff) - return sprites, palettes - - -def write_heart_star_sprites(rom: RomData): - compressed = rom.read_bytes(heart_star_address, heart_star_size) - decompressed = hal_decompress(compressed) - patch = get_data(__name__, os.path.join("data", "APHeartStar.bsdiff4")) - patched = bytearray(bsdiff4.patch(decompressed, patch)) - rom.write_bytes(0x1AF7DF, patched) - patched[0:0] = [0xE3, 0xFF] - patched.append(0xFF) - rom.write_bytes(0x1CD000, patched) - rom.write_bytes(0x3F0EBF, [0x00, 0xD0, 0x39]) - - -def write_consumable_sprites(rom: RomData, consumables: bool, stars: bool): - compressed = rom.read_bytes(consumable_address, consumable_size) - decompressed = hal_decompress(compressed) - patched = bytearray(decompressed) - if consumables: - patch = get_data(__name__, os.path.join("data", "APConsumable.bsdiff4")) - patched = bytearray(bsdiff4.patch(bytes(patched), patch)) - if stars: - patch = get_data(__name__, os.path.join("data", "APStars.bsdiff4")) - patched = bytearray(bsdiff4.patch(bytes(patched), patch)) - patched[0:0] = [0xE3, 0xFF] - patched.append(0xFF) - rom.write_bytes(0x1CD500, patched) - rom.write_bytes(0x3F0DAE, [0x00, 0xD5, 0x39]) - - -class KDL3DeltaPatch(APDeltaPatch): - hash = [KDL3UHASH, KDL3JHASH] - game = "Kirby's Dream Land 3" - patch_file_ending = ".apkdl3" - - @classmethod - def get_source_data(cls) -> bytes: - return get_base_rom_bytes() - - def patch(self, target: str): - super().patch(target) - rom = RomData(target) - target_language = rom.read_byte(0x3C020) - rom.write_byte(0x7FD9, target_language) - write_heart_star_sprites(rom) - if rom.read_bytes(0x3D014, 1)[0] > 0: - stages = [struct.unpack("HHHHHHH", rom.read_bytes(0x3D020 + x * 14, 14)) for x in range(5)] - palettes = [rom.read_bytes(full_pal, 512) for full_pal in stage_palettes] - palettes = [[palette[i:i + 32] for i in range(0, 512, 32)] for palette in palettes] - sprites = [rom.read_bytes(offset, level_sprites[offset]) for offset in level_sprites] - sprites, palettes = handle_level_sprites(stages, sprites, palettes) - for addr, palette in zip(stage_palettes, palettes): - rom.write_bytes(addr, palette) - for addr, level_sprite in zip([0x1CA000, 0x1CA920, 0x1CB230, 0x1CBB40, 0x1CC450], sprites): - rom.write_bytes(addr, level_sprite) - rom.write_bytes(0x460A, [0x00, 0xA0, 0x39, 0x20, 0xA9, 0x39, 0x30, 0xB2, 0x39, 0x40, 0xBB, 0x39, - 0x50, 0xC4, 0x39]) - write_consumable_sprites(rom, rom.read_byte(0x3D018) > 0, rom.read_byte(0x3D01A) > 0) - rom_name = rom.read_bytes(0x3C000, 21) - rom.write_bytes(0x7FC0, rom_name) - rom.write_crc() - rom.write_to_file(target) - - -def patch_rom(world: "KDL3World", rom: RomData): - rom.apply_patch(get_data(__name__, os.path.join("data", "kdl3_basepatch.bsdiff4"))) - tiles = get_data(__name__, os.path.join("data", "APPauseIcons.dat")) - rom.write_bytes(0x3F000, tiles) - - # Write open world patch - if world.options.open_world: - rom.write_bytes(0x143C7, [0xAD, 0xC1, 0x5A, 0xCD, 0xC1, 0x5A, ]) - # changes the stage flag function to compare $5AC1 to $5AC1, - # always running the "new stage" function - # This has further checks present for bosses already, so we just - # need to handle regular stages - # write check for boss to be unlocked - - if world.options.consumables: - # reroute maxim tomatoes to use the 1-UP function, then null out the function - rom.write_bytes(0x3002F, [0x37, 0x00]) - rom.write_bytes(0x30037, [0xA9, 0x26, 0x00, # LDA #$0026 - 0x22, 0x27, 0xD9, 0x00, # JSL $00D927 - 0xA4, 0xD2, # LDY $D2 - 0x6B, # RTL - 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, # NOP #10 - ]) - - # stars handling is built into the rom, so no changes there - - rooms = world.rooms - if world.options.music_shuffle > 0: - if world.options.music_shuffle == 1: - shuffled_music = music_choices.copy() - world.random.shuffle(shuffled_music) - music_map = dict(zip(music_choices, shuffled_music)) - # Avoid putting star twinkle in the pool - music_map[5] = world.random.choice(music_choices) - # Heart Star music doesn't work on regular stages - music_map[8] = world.random.choice(music_choices) - for room in rooms: - room.music = music_map[room.music] - for room in room_pointers: - old_music = rom.read_byte(room + 2) - rom.write_byte(room + 2, music_map[old_music]) - for i in range(5): - # level themes - old_music = rom.read_byte(0x133F2 + i) - rom.write_byte(0x133F2 + i, music_map[old_music]) - # Zero - rom.write_byte(0x9AE79, music_map[0x18]) - # Heart Star success and fail - rom.write_byte(0x4A388, music_map[0x08]) - rom.write_byte(0x4A38D, music_map[0x1D]) - elif world.options.music_shuffle == 2: - for room in rooms: - room.music = world.random.choice(music_choices) - for room in room_pointers: - rom.write_byte(room + 2, world.random.choice(music_choices)) - for i in range(5): - # level themes - rom.write_byte(0x133F2 + i, world.random.choice(music_choices)) - # Zero - rom.write_byte(0x9AE79, world.random.choice(music_choices)) - # Heart Star success and fail - rom.write_byte(0x4A388, world.random.choice(music_choices)) - rom.write_byte(0x4A38D, world.random.choice(music_choices)) - - for room in rooms: - room.patch(rom) - - if world.options.virtual_console in [1, 3]: - # Flash Reduction - rom.write_byte(0x9AE68, 0x10) - rom.write_bytes(0x9AE8E, [0x08, 0x00, 0x22, 0x5D, 0xF7, 0x00, 0xA2, 0x08, ]) - rom.write_byte(0x9AEA1, 0x08) - rom.write_byte(0x9AEC9, 0x01) - rom.write_bytes(0x9AED2, [0xA9, 0x1F]) - rom.write_byte(0x9AEE1, 0x08) - - if world.options.virtual_console in [2, 3]: - # Hyper Zone BB colors - rom.write_bytes(0x2C5E16, [0xEE, 0x1B, 0x18, 0x5B, 0xD3, 0x4A, 0xF4, 0x3B, ]) - rom.write_bytes(0x2C8217, [0xFF, 0x1E, ]) - - # boss requirements - rom.write_bytes(0x3D000, struct.pack("HHHHH", world.boss_requirements[0], world.boss_requirements[1], - world.boss_requirements[2], world.boss_requirements[3], - world.boss_requirements[4])) - rom.write_bytes(0x3D00A, struct.pack("H", world.required_heart_stars if world.options.goal_speed == 1 else 0xFFFF)) - rom.write_byte(0x3D00C, world.options.goal_speed.value) - rom.write_byte(0x3D00E, world.options.open_world.value) - rom.write_byte(0x3D010, world.options.death_link.value) - rom.write_byte(0x3D012, world.options.goal.value) - rom.write_byte(0x3D014, world.options.stage_shuffle.value) - rom.write_byte(0x3D016, world.options.ow_boss_requirement.value) - rom.write_byte(0x3D018, world.options.consumables.value) - rom.write_byte(0x3D01A, world.options.starsanity.value) - rom.write_byte(0x3D01C, world.options.gifting.value if world.multiworld.players > 1 else 0) - rom.write_byte(0x3D01E, world.options.strict_bosses.value) - # don't write gifting for solo game, since there's no one to send anything to - - for level in world.player_levels: - for i in range(len(world.player_levels[level])): - rom.write_bytes(0x3F002E + ((level - 1) * 14) + (i * 2), - struct.pack("H", level_pointers[world.player_levels[level][i]])) - rom.write_bytes(0x3D020 + (level - 1) * 14 + (i * 2), - struct.pack("H", world.player_levels[level][i] & 0x00FFFF)) - if (i == 0) or (i > 0 and i % 6 != 0): - rom.write_bytes(0x3D080 + (level - 1) * 12 + (i * 2), - struct.pack("H", (world.player_levels[level][i] & 0x00FFFF) % 6)) - - for i in range(6): - if world.boss_butch_bosses[i]: - rom.write_bytes(0x3F0000 + (level_pointers[0x770200 + i]), struct.pack("I", bb_bosses[0x770200 + i])) - - # copy ability shuffle - if world.options.copy_ability_randomization.value > 0: - for enemy in world.copy_abilities: - if enemy in miniboss_remap: - rom.write_bytes(0xB417E + (miniboss_remap[enemy] << 1), - struct.pack("H", ability_remap[world.copy_abilities[enemy]])) - else: - rom.write_bytes(0xB3CAC + (enemy_remap[enemy] << 1), - struct.pack("H", ability_remap[world.copy_abilities[enemy]])) - # following only needs done on non-door rando - # incredibly lucky this follows the same order (including 5E == star block) - rom.write_byte(0x2F77EA, 0x5E + (ability_remap[world.copy_abilities["Sparky"]] << 1)) - rom.write_byte(0x2F7811, 0x5E + (ability_remap[world.copy_abilities["Sparky"]] << 1)) - rom.write_byte(0x2F9BC4, 0x5E + (ability_remap[world.copy_abilities["Blocky"]] << 1)) - rom.write_byte(0x2F9BEB, 0x5E + (ability_remap[world.copy_abilities["Blocky"]] << 1)) - rom.write_byte(0x2FAC06, 0x5E + (ability_remap[world.copy_abilities["Jumper Shoot"]] << 1)) - rom.write_byte(0x2FAC2D, 0x5E + (ability_remap[world.copy_abilities["Jumper Shoot"]] << 1)) - rom.write_byte(0x2F9E7B, 0x5E + (ability_remap[world.copy_abilities["Yuki"]] << 1)) - rom.write_byte(0x2F9EA2, 0x5E + (ability_remap[world.copy_abilities["Yuki"]] << 1)) - rom.write_byte(0x2FA951, 0x5E + (ability_remap[world.copy_abilities["Sir Kibble"]] << 1)) - rom.write_byte(0x2FA978, 0x5E + (ability_remap[world.copy_abilities["Sir Kibble"]] << 1)) - rom.write_byte(0x2FA132, 0x5E + (ability_remap[world.copy_abilities["Haboki"]] << 1)) - rom.write_byte(0x2FA159, 0x5E + (ability_remap[world.copy_abilities["Haboki"]] << 1)) - rom.write_byte(0x2FA3E8, 0x5E + (ability_remap[world.copy_abilities["Boboo"]] << 1)) - rom.write_byte(0x2FA40F, 0x5E + (ability_remap[world.copy_abilities["Boboo"]] << 1)) - rom.write_byte(0x2F90E2, 0x5E + (ability_remap[world.copy_abilities["Captain Stitch"]] << 1)) - rom.write_byte(0x2F9109, 0x5E + (ability_remap[world.copy_abilities["Captain Stitch"]] << 1)) - - if world.options.copy_ability_randomization == 2: - for enemy in enemy_remap: - # we just won't include it for minibosses - rom.write_bytes(0xB3E40 + (enemy_remap[enemy] << 1), struct.pack("h", world.random.randint(-1, 2))) - - # write jumping goal - rom.write_bytes(0x94F8, struct.pack("H", world.options.jumping_target)) - rom.write_bytes(0x944E, struct.pack("H", world.options.jumping_target)) - - from Utils import __version__ - rom.name = bytearray( - f'KDL3{__version__.replace(".", "")[0:3]}_{world.player}_{world.multiworld.seed:11}\0', 'utf8')[:21] - rom.name.extend([0] * (21 - len(rom.name))) - rom.write_bytes(0x3C000, rom.name) - rom.write_byte(0x3C020, world.options.game_language.value) - - # handle palette - if world.options.kirby_flavor_preset.value != 0: - for addr in kirby_target_palettes: - target = kirby_target_palettes[addr] - palette = get_kirby_palette(world) - rom.write_bytes(addr, get_palette_bytes(palette, target[0], target[1], target[2])) - - if world.options.gooey_flavor_preset.value != 0: - for addr in gooey_target_palettes: - target = gooey_target_palettes[addr] - palette = get_gooey_palette(world) - rom.write_bytes(addr, get_palette_bytes(palette, target[0], target[1], target[2])) - - -def get_base_rom_bytes() -> bytes: - rom_file: str = get_base_rom_path() - base_rom_bytes: Optional[bytes] = getattr(get_base_rom_bytes, "base_rom_bytes", None) - if not base_rom_bytes: - base_rom_bytes = bytes(Utils.read_snes_rom(open(rom_file, "rb"))) - - basemd5 = hashlib.md5() - basemd5.update(base_rom_bytes) - if basemd5.hexdigest() not in {KDL3UHASH, KDL3JHASH}: - raise Exception("Supplied Base Rom does not match known MD5 for US or JP release. " - "Get the correct game and version, then dump it") - get_base_rom_bytes.base_rom_bytes = base_rom_bytes - return base_rom_bytes - - -def get_base_rom_path(file_name: str = "") -> str: - options: settings.Settings = settings.get_settings() - if not file_name: - file_name = options["kdl3_options"]["rom_file"] - if not os.path.exists(file_name): - file_name = Utils.user_path(file_name) - return file_name diff --git a/worlds/kdl3/Room.py b/worlds/kdl3/Room.py deleted file mode 100644 index 256955b924..0000000000 --- a/worlds/kdl3/Room.py +++ /dev/null @@ -1,95 +0,0 @@ -import struct -import typing -from BaseClasses import Region, ItemClassification - -if typing.TYPE_CHECKING: - from .Rom import RomData - -animal_map = { - "Rick Spawn": 0, - "Kine Spawn": 1, - "Coo Spawn": 2, - "Nago Spawn": 3, - "ChuChu Spawn": 4, - "Pitch Spawn": 5 -} - - -class KDL3Room(Region): - pointer: int = 0 - level: int = 0 - stage: int = 0 - room: int = 0 - music: int = 0 - default_exits: typing.List[typing.Dict[str, typing.Union[int, typing.List[str]]]] - animal_pointers: typing.List[int] - enemies: typing.List[str] - entity_load: typing.List[typing.List[int]] - consumables: typing.List[typing.Dict[str, typing.Union[int, str]]] - - def __init__(self, name, player, multiworld, hint, level, stage, room, pointer, music, default_exits, - animal_pointers, enemies, entity_load, consumables, consumable_pointer): - super().__init__(name, player, multiworld, hint) - self.level = level - self.stage = stage - self.room = room - self.pointer = pointer - self.music = music - self.default_exits = default_exits - self.animal_pointers = animal_pointers - self.enemies = enemies - self.entity_load = entity_load - self.consumables = consumables - self.consumable_pointer = consumable_pointer - - def patch(self, rom: "RomData"): - rom.write_byte(self.pointer + 2, self.music) - animals = [x.item.name for x in self.locations if "Animal" in x.name] - if len(animals) > 0: - for current_animal, address in zip(animals, self.animal_pointers): - rom.write_byte(self.pointer + address + 7, animal_map[current_animal]) - if self.multiworld.worlds[self.player].options.consumables: - load_len = len(self.entity_load) - for consumable in self.consumables: - location = next(x for x in self.locations if x.name == consumable["name"]) - assert location.item - is_progression = location.item.classification & ItemClassification.progression - if load_len == 8: - # edge case, there is exactly 1 room with 8 entities and only 1 consumable among them - if not (any(x in self.entity_load for x in [[0, 22], [1, 22]]) - and any(x in self.entity_load for x in [[2, 22], [3, 22]])): - replacement_target = self.entity_load.index( - next(x for x in self.entity_load if x in [[0, 22], [1, 22], [2, 22], [3, 22]])) - if is_progression: - vtype = 0 - else: - vtype = 2 - rom.write_byte(self.pointer + 88 + (replacement_target * 2), vtype) - self.entity_load[replacement_target] = [vtype, 22] - else: - if is_progression: - # we need to see if 1-ups are in our load list - if any(x not in self.entity_load for x in [[0, 22], [1, 22]]): - self.entity_load.append([0, 22]) - else: - if any(x not in self.entity_load for x in [[2, 22], [3, 22]]): - # edge case: if (1, 22) is in, we need to load (3, 22) instead - if [1, 22] in self.entity_load: - self.entity_load.append([3, 22]) - else: - self.entity_load.append([2, 22]) - if load_len < len(self.entity_load): - rom.write_bytes(self.pointer + 88 + (load_len * 2), bytes(self.entity_load[load_len])) - rom.write_bytes(self.pointer + 104 + (load_len * 2), - bytes(struct.pack("H", self.consumable_pointer))) - if is_progression: - if [1, 22] in self.entity_load: - vtype = 1 - else: - vtype = 0 - else: - if [3, 22] in self.entity_load: - vtype = 3 - else: - vtype = 2 - rom.write_byte(self.pointer + consumable["pointer"] + 7, vtype) diff --git a/worlds/kdl3/__init__.py b/worlds/kdl3/__init__.py index 8c9f3cc46a..12f56a0230 100644 --- a/worlds/kdl3/__init__.py +++ b/worlds/kdl3/__init__.py @@ -1,25 +1,25 @@ import logging -import typing -from BaseClasses import Tutorial, ItemClassification, MultiWorld +from BaseClasses import Tutorial, ItemClassification, MultiWorld, CollectionState, Item from Fill import fill_restrictive from Options import PerGameCommonOptions from worlds.AutoWorld import World, WebWorld -from .Items import item_table, item_names, copy_ability_table, animal_friend_table, filler_item_weights, KDL3Item, \ - trap_item_table, copy_ability_access_table, star_item_weights, total_filler_weights -from .Locations import location_table, KDL3Location, level_consumables, consumable_locations, star_locations -from .Names.AnimalFriendSpawns import animal_friend_spawns -from .Names.EnemyAbilities import vanilla_enemies, enemy_mapping, enemy_restrictive -from .Regions import create_levels, default_levels -from .Options import KDL3Options -from .Presets import kdl3_options_presets -from .Names import LocationName -from .Room import KDL3Room -from .Rules import set_rules -from .Rom import KDL3DeltaPatch, get_base_rom_path, RomData, patch_rom, KDL3JHASH, KDL3UHASH -from .Client import KDL3SNIClient +from .items import item_table, item_names, copy_ability_table, animal_friend_table, filler_item_weights, KDL3Item, \ + trap_item_table, copy_ability_access_table, star_item_weights, total_filler_weights, animal_friend_spawn_table,\ + lookup_item_to_id +from .locations import location_table, KDL3Location, level_consumables, consumable_locations, star_locations +from .names.animal_friend_spawns import animal_friend_spawns, problematic_sets +from .names.enemy_abilities import vanilla_enemies, enemy_mapping, enemy_restrictive +from .regions import create_levels, default_levels +from .options import KDL3Options, kdl3_option_groups +from .presets import kdl3_options_presets +from .names import location_name +from .room import KDL3Room +from .rules import set_rules +from .rom import KDL3ProcedurePatch, get_base_rom_path, patch_rom, KDL3JHASH, KDL3UHASH +from .client import KDL3SNIClient -from typing import Dict, TextIO, Optional, List +from typing import Dict, TextIO, Optional, List, Any, Mapping, ClassVar, Type import os import math import threading @@ -53,6 +53,7 @@ class KDL3WebWorld(WebWorld): ) ] options_presets = kdl3_options_presets + option_groups = kdl3_option_groups class KDL3World(World): @@ -61,35 +62,35 @@ class KDL3World(World): """ game = "Kirby's Dream Land 3" - options_dataclass: typing.ClassVar[typing.Type[PerGameCommonOptions]] = KDL3Options + options_dataclass: ClassVar[Type[PerGameCommonOptions]] = KDL3Options options: KDL3Options - item_name_to_id = {item: item_table[item].code for item in item_table} + item_name_to_id = lookup_item_to_id location_name_to_id = {location_table[location]: location for location in location_table} item_name_groups = item_names web = KDL3WebWorld() - settings: typing.ClassVar[KDL3Settings] + settings: ClassVar[KDL3Settings] def __init__(self, multiworld: MultiWorld, player: int): - self.rom_name = None + self.rom_name: bytes = bytes() self.rom_name_available_event = threading.Event() super().__init__(multiworld, player) self.copy_abilities: Dict[str, str] = vanilla_enemies.copy() self.required_heart_stars: int = 0 # we fill this during create_items - self.boss_requirements: Dict[int, int] = dict() + self.boss_requirements: List[int] = [] self.player_levels = default_levels.copy() self.stage_shuffle_enabled = False - self.boss_butch_bosses: List[Optional[bool]] = list() - self.rooms: Optional[List[KDL3Room]] = None - - @classmethod - def stage_assert_generate(cls, multiworld: MultiWorld) -> None: - rom_file: str = get_base_rom_path() - if not os.path.exists(rom_file): - raise FileNotFoundError(f"Could not find base ROM for {cls.game}: {rom_file}") + self.boss_butch_bosses: List[Optional[bool]] = [] + self.rooms: List[KDL3Room] = [] create_regions = create_levels - def create_item(self, name: str, force_non_progression=False) -> KDL3Item: + def generate_early(self) -> None: + if self.options.total_heart_stars != -1: + logger.warning(f"Kirby's Dream Land 3 ({self.player_name}): Use of \"total_heart_stars\" is deprecated. " + f"Please use \"max_heart_stars\" instead.") + self.options.max_heart_stars.value = self.options.total_heart_stars.value + + def create_item(self, name: str, force_non_progression: bool = False) -> KDL3Item: item = item_table[name] classification = ItemClassification.filler if item.progression and not force_non_progression: @@ -99,7 +100,7 @@ class KDL3World(World): classification = ItemClassification.trap return KDL3Item(name, classification, item.code, self.player) - def get_filler_item_name(self, include_stars=True) -> str: + def get_filler_item_name(self, include_stars: bool = True) -> str: if include_stars: return self.random.choices(list(total_filler_weights.keys()), weights=list(total_filler_weights.values()))[0] @@ -112,8 +113,8 @@ class KDL3World(World): self.options.slow_trap_weight.value, self.options.ability_trap_weight.value])[0] - def get_restrictive_copy_ability_placement(self, copy_ability: str, enemies_to_set: typing.List[str], - level: int, stage: int): + def get_restrictive_copy_ability_placement(self, copy_ability: str, enemies_to_set: List[str], + level: int, stage: int) -> Optional[str]: valid_rooms = [room for room in self.rooms if (room.level < level) or (room.level == level and room.stage < stage)] # leave out the stage in question to avoid edge valid_enemies = set() @@ -124,6 +125,10 @@ class KDL3World(World): return None # a valid enemy got placed by a more restrictive placement return self.random.choice(sorted([enemy for enemy in valid_enemies if enemy not in placed_enemies])) + def get_pre_fill_items(self) -> List[Item]: + return [self.create_item(item) + for item in [*copy_ability_access_table.keys(), *animal_friend_spawn_table.keys()]] + def pre_fill(self) -> None: if self.options.copy_ability_randomization: # randomize copy abilities @@ -207,10 +212,32 @@ class KDL3World(World): # If Kine is ever the last animal friend placed, he will cause fill errors on closed world animal_pool.sort() locations = [self.multiworld.get_location(spawn, self.player) for spawn in spawns] - items = [self.create_item(animal) for animal in animal_pool] - allstate = self.multiworld.get_all_state(False) + items: List[Item] = [self.create_item(animal) for animal in animal_pool] + allstate = CollectionState(self.multiworld) + for item in [*copy_ability_table, *animal_friend_table, *["Heart Star" for _ in range(99)]]: + self.collect(allstate, self.create_item(item)) self.random.shuffle(locations) fill_restrictive(self.multiworld, allstate, locations, items, True, True) + + # Need to ensure all of these are unique items, and replace them if they aren't + for spawns in problematic_sets: + placed = [self.get_location(spawn).item for spawn in spawns] + placed_names = set([item.name for item in placed]) + if len(placed_names) != len(placed): + # have a duplicate + animals = [] + for spawn in spawns: + spawn_location = self.get_location(spawn) + if spawn_location.item.name not in animals: + animals.append(spawn_location.item.name) + else: + new_animal = self.random.choice([x for x in ["Rick Spawn", "Coo Spawn", "Kine Spawn", + "ChuChu Spawn", "Nago Spawn", "Pitch Spawn"] + if x not in placed_names and x not in animals]) + spawn_location.item = None + spawn_location.place_locked_item(self.create_item(new_animal)) + animals.append(new_animal) + # logically, this should be sound pre-ER. May need to adjust around it with ER in the future else: animal_friends = animal_friend_spawns.copy() for animal in animal_friends: @@ -225,21 +252,20 @@ class KDL3World(World): remaining_items = len(location_table) - len(itempool) if not self.options.consumables: remaining_items -= len(consumable_locations) - remaining_items -= len(star_locations) - if self.options.starsanity: - # star fill, keep consumable pool locked to consumable and fill 767 stars specifically - star_items = list(star_item_weights.keys()) - star_weights = list(star_item_weights.values()) - itempool.extend([self.create_item(item) for item in self.random.choices(star_items, weights=star_weights, - k=767)]) - total_heart_stars = self.options.total_heart_stars + if not self.options.starsanity: + remaining_items -= len(star_locations) + max_heart_stars = self.options.max_heart_stars.value + if max_heart_stars > remaining_items: + max_heart_stars = remaining_items # ensure at least 1 heart star required per world - required_heart_stars = max(int(total_heart_stars * required_percentage), 5) - filler_items = total_heart_stars - required_heart_stars - filler_amount = math.floor(filler_items * (self.options.filler_percentage / 100.0)) - trap_amount = math.floor(filler_amount * (self.options.trap_percentage / 100.0)) - filler_amount -= trap_amount - non_required_heart_stars = filler_items - filler_amount - trap_amount + required_heart_stars = min(max(int(max_heart_stars * required_percentage), 5), 99) + filler_items = remaining_items - required_heart_stars + converted_heart_stars = math.floor((max_heart_stars - required_heart_stars) * (self.options.filler_percentage / 100.0)) + non_required_heart_stars = max_heart_stars - converted_heart_stars - required_heart_stars + filler_items -= non_required_heart_stars + trap_amount = math.floor(filler_items * (self.options.trap_percentage / 100.0)) + + filler_items -= trap_amount self.required_heart_stars = required_heart_stars # handle boss requirements here requirements = [required_heart_stars] @@ -261,8 +287,8 @@ class KDL3World(World): requirements.insert(i - 1, quotient * i) self.boss_requirements = requirements itempool.extend([self.create_item("Heart Star") for _ in range(required_heart_stars)]) - itempool.extend([self.create_item(self.get_filler_item_name(False)) - for _ in range(filler_amount + (remaining_items - total_heart_stars))]) + itempool.extend([self.create_item(self.get_filler_item_name(bool(self.options.starsanity.value))) + for _ in range(filler_items)]) itempool.extend([self.create_item(self.get_trap_item_name()) for _ in range(trap_amount)]) itempool.extend([self.create_item("Heart Star", True) for _ in range(non_required_heart_stars)]) @@ -273,15 +299,15 @@ class KDL3World(World): self.multiworld.get_location(location_table[self.player_levels[level][stage]] .replace("Complete", "Stage Completion"), self.player) \ .place_locked_item(KDL3Item( - f"{LocationName.level_names_inverse[level]} - Stage Completion", + f"{location_name.level_names_inverse[level]} - Stage Completion", ItemClassification.progression, None, self.player)) set_rules = set_rules def generate_basic(self) -> None: self.stage_shuffle_enabled = self.options.stage_shuffle > 0 - goal = self.options.goal - goal_location = self.multiworld.get_location(LocationName.goals[goal], self.player) + goal = self.options.goal.value + goal_location = self.multiworld.get_location(location_name.goals[goal], self.player) goal_location.place_locked_item(KDL3Item("Love-Love Rod", ItemClassification.progression, None, self.player)) for level in range(1, 6): self.multiworld.get_location(f"Level {level} Boss - Defeated", self.player) \ @@ -300,60 +326,65 @@ class KDL3World(World): else: self.boss_butch_bosses = [False for _ in range(6)] - def generate_output(self, output_directory: str): - rom_path = "" + def generate_output(self, output_directory: str) -> None: try: - rom = RomData(get_base_rom_path()) - patch_rom(self, rom) + patch = KDL3ProcedurePatch() + patch_rom(self, patch) - rom_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.sfc") - rom.write_to_file(rom_path) - self.rom_name = rom.name + self.rom_name = patch.name - patch = KDL3DeltaPatch(os.path.splitext(rom_path)[0] + KDL3DeltaPatch.patch_file_ending, player=self.player, - player_name=self.multiworld.player_name[self.player], patched_path=rom_path) - patch.write() + patch.write(os.path.join(output_directory, + f"{self.multiworld.get_out_file_name_base(self.player)}{patch.patch_file_ending}")) except Exception: raise finally: self.rom_name_available_event.set() # make sure threading continues and errors are collected - if os.path.exists(rom_path): - os.unlink(rom_path) - def modify_multidata(self, multidata: dict): + def modify_multidata(self, multidata: Dict[str, Any]) -> None: # wait for self.rom_name to be available. self.rom_name_available_event.wait() + assert isinstance(self.rom_name, bytes) rom_name = getattr(self, "rom_name", None) # we skip in case of error, so that the original error in the output thread is the one that gets raised if rom_name: - new_name = base64.b64encode(bytes(self.rom_name)).decode() + new_name = base64.b64encode(self.rom_name).decode() multidata["connect_names"][new_name] = multidata["connect_names"][self.multiworld.player_name[self.player]] + def fill_slot_data(self) -> Mapping[str, Any]: + # UT support + return {"player_levels": self.player_levels} + + def interpret_slot_data(self, slot_data: Mapping[str, Any]): + # UT support + player_levels = {int(key): value for key, value in slot_data["player_levels"].items()} + return {"player_levels": player_levels} + def write_spoiler(self, spoiler_handle: TextIO) -> None: if self.stage_shuffle_enabled: spoiler_handle.write(f"\nLevel Layout ({self.multiworld.get_player_name(self.player)}):\n") - for level in LocationName.level_names: - for stage, i in zip(self.player_levels[LocationName.level_names[level]], range(1, 7)): + for level in location_name.level_names: + for stage, i in zip(self.player_levels[location_name.level_names[level]], range(1, 7)): spoiler_handle.write(f"{level} {i}: {location_table[stage].replace(' - Complete', '')}\n") if self.options.animal_randomization: spoiler_handle.write(f"\nAnimal Friends ({self.multiworld.get_player_name(self.player)}):\n") - for level in self.player_levels: + for lvl in self.player_levels: for stage in range(6): - rooms = [room for room in self.rooms if room.level == level and room.stage == stage] + rooms = [room for room in self.rooms if room.level == lvl and room.stage == stage] animals = [] for room in rooms: animals.extend([location.item.name.replace(" Spawn", "") - for location in room.locations if "Animal" in location.name]) - spoiler_handle.write(f"{location_table[self.player_levels[level][stage]].replace(' - Complete','')}" + for location in room.locations if "Animal" in location.name + and location.item is not None]) + spoiler_handle.write(f"{location_table[self.player_levels[lvl][stage]].replace(' - Complete','')}" f": {', '.join(animals)}\n") if self.options.copy_ability_randomization: spoiler_handle.write(f"\nCopy Abilities ({self.multiworld.get_player_name(self.player)}):\n") for enemy in self.copy_abilities: spoiler_handle.write(f"{enemy}: {self.copy_abilities[enemy].replace('No Ability', 'None').replace(' Ability', '')}\n") - def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]): + def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]) -> None: if self.stage_shuffle_enabled: - regions = {LocationName.level_names[level]: level for level in LocationName.level_names} + regions = {location_name.level_names[level]: level for level in location_name.level_names} level_hint_data = {} for level in regions: for stage in range(7): @@ -361,6 +392,6 @@ class KDL3World(World): self.player).name.replace(" - Complete", "") stage_regions = [room for room in self.rooms if stage_name in room.name] for region in stage_regions: - for location in [location for location in region.locations if location.address]: + for location in [location for location in list(region.get_locations()) if location.address]: level_hint_data[location.address] = f"{regions[level]} {stage + 1 if stage < 6 else 'Boss'}" hint_data[self.player] = level_hint_data diff --git a/worlds/kdl3/Aesthetics.py b/worlds/kdl3/aesthetics.py similarity index 91% rename from worlds/kdl3/Aesthetics.py rename to worlds/kdl3/aesthetics.py index 8c7363908f..8b798ff93e 100644 --- a/worlds/kdl3/Aesthetics.py +++ b/worlds/kdl3/aesthetics.py @@ -1,5 +1,9 @@ import struct -from .Options import KirbyFlavorPreset, GooeyFlavorPreset +from .options import KirbyFlavorPreset, GooeyFlavorPreset +from typing import TYPE_CHECKING, Optional, Dict, List, Tuple + +if TYPE_CHECKING: + from . import KDL3World kirby_flavor_presets = { 1: { @@ -223,6 +227,23 @@ kirby_flavor_presets = { "14": "E6E6FA", "15": "976FBD", }, + 14: { + "1": "373B3E", + "2": "98d5d3", + "3": "1aa5ab", + "4": "168f95", + "5": "4f5559", + "6": "1dbac2", + "7": "137a7f", + "8": "093a3c", + "9": "86cecb", + "10": "a0afbc", + "11": "62bfbb", + "12": "50b8b4", + "13": "bec8d1", + "14": "bce4e2", + "15": "91a2b1", + } } gooey_flavor_presets = { @@ -398,21 +419,21 @@ gooey_target_palettes = { } -def get_kirby_palette(world): +def get_kirby_palette(world: "KDL3World") -> Optional[Dict[str, str]]: palette = world.options.kirby_flavor_preset.value if palette == KirbyFlavorPreset.option_custom: return world.options.kirby_flavor.value return kirby_flavor_presets.get(palette, None) -def get_gooey_palette(world): +def get_gooey_palette(world: "KDL3World") -> Optional[Dict[str, str]]: palette = world.options.gooey_flavor_preset.value if palette == GooeyFlavorPreset.option_custom: return world.options.gooey_flavor.value return gooey_flavor_presets.get(palette, None) -def rgb888_to_bgr555(red, green, blue) -> bytes: +def rgb888_to_bgr555(red: int, green: int, blue: int) -> bytes: red = red >> 3 green = green >> 3 blue = blue >> 3 @@ -420,15 +441,15 @@ def rgb888_to_bgr555(red, green, blue) -> bytes: return struct.pack("H", outcol) -def get_palette_bytes(palette, target, offset, factor): +def get_palette_bytes(palette: Dict[str, str], target: List[str], offset: int, factor: float) -> bytes: output_data = bytearray() for color in target: hexcol = palette[color] if hexcol.startswith("#"): hexcol = hexcol.replace("#", "") colint = int(hexcol, 16) - col = ((colint & 0xFF0000) >> 16, (colint & 0xFF00) >> 8, colint & 0xFF) + col: Tuple[int, ...] = ((colint & 0xFF0000) >> 16, (colint & 0xFF00) >> 8, colint & 0xFF) col = tuple(int(int(factor*x) + offset) for x in col) byte_data = rgb888_to_bgr555(col[0], col[1], col[2]) output_data.extend(bytearray(byte_data)) - return output_data + return bytes(output_data) diff --git a/worlds/kdl3/Client.py b/worlds/kdl3/client.py similarity index 90% rename from worlds/kdl3/Client.py rename to worlds/kdl3/client.py index 1ca21d550e..97bf68cbd9 100644 --- a/worlds/kdl3/Client.py +++ b/worlds/kdl3/client.py @@ -11,13 +11,13 @@ from MultiServer import mark_raw from NetUtils import ClientStatus, color from Utils import async_start from worlds.AutoSNIClient import SNIClient -from .Locations import boss_locations -from .Gifting import kdl3_gifting_options, kdl3_trap_gifts, kdl3_gifts, update_object, pop_object, initialize_giftboxes -from .ClientAddrs import consumable_addrs, star_addrs +from .locations import boss_locations +from .gifting import kdl3_gifting_options, kdl3_trap_gifts, kdl3_gifts, update_object, pop_object, initialize_giftboxes +from .client_addrs import consumable_addrs, star_addrs from typing import TYPE_CHECKING if TYPE_CHECKING: - from SNIClient import SNIClientCommandProcessor + from SNIClient import SNIClientCommandProcessor, SNIContext snes_logger = logging.getLogger("SNES") @@ -81,17 +81,16 @@ deathlink_messages = defaultdict(lambda: " was defeated.", { @mark_raw -def cmd_gift(self: "SNIClientCommandProcessor"): +def cmd_gift(self: "SNIClientCommandProcessor") -> None: """Toggles gifting for the current game.""" - if not getattr(self.ctx, "gifting", None): - self.ctx.gifting = True - else: - self.ctx.gifting = not self.ctx.gifting - self.output(f"Gifting set to {self.ctx.gifting}") + handler = self.ctx.client_handler + assert isinstance(handler, KDL3SNIClient) + handler.gifting = not handler.gifting + self.output(f"Gifting set to {handler.gifting}") async_start(update_object(self.ctx, f"Giftboxes;{self.ctx.team}", { f"{self.ctx.slot}": { - "IsOpen": self.ctx.gifting, + "IsOpen": handler.gifting, **kdl3_gifting_options } })) @@ -100,16 +99,17 @@ def cmd_gift(self: "SNIClientCommandProcessor"): class KDL3SNIClient(SNIClient): game = "Kirby's Dream Land 3" patch_suffix = ".apkdl3" - levels = None - consumables = None - stars = None - item_queue: typing.List = [] - initialize_gifting = False + levels: typing.Dict[int, typing.List[int]] = {} + consumables: typing.Optional[bool] = None + stars: typing.Optional[bool] = None + item_queue: typing.List[int] = [] + initialize_gifting: bool = False + gifting: bool = False giftbox_key: str = "" motherbox_key: str = "" client_random: random.Random = random.Random() - async def deathlink_kill_player(self, ctx) -> None: + async def deathlink_kill_player(self, ctx: "SNIContext") -> None: from SNIClient import DeathState, snes_buffered_write, snes_flush_writes, snes_read game_state = await snes_read(ctx, KDL3_GAME_STATE, 1) if game_state[0] == 0xFF: @@ -131,7 +131,7 @@ class KDL3SNIClient(SNIClient): ctx.death_state = DeathState.dead ctx.last_death_link = time.time() - async def validate_rom(self, ctx) -> bool: + async def validate_rom(self, ctx: "SNIContext") -> bool: from SNIClient import snes_read rom_name = await snes_read(ctx, KDL3_ROMNAME, 0x15) if rom_name is None or rom_name == bytes([0] * 0x15) or rom_name[:4] != b"KDL3": @@ -141,7 +141,7 @@ class KDL3SNIClient(SNIClient): ctx.game = self.game ctx.rom = rom_name - ctx.items_handling = 0b111 # always remote items + ctx.items_handling = 0b101 # default local items with remote start inventory ctx.allow_collect = True if "gift" not in ctx.command_processor.commands: ctx.command_processor.commands["gift"] = cmd_gift @@ -149,9 +149,10 @@ class KDL3SNIClient(SNIClient): death_link = await snes_read(ctx, KDL3_DEATH_LINK_ADDR, 1) if death_link: await ctx.update_death_link(bool(death_link[0] & 0b1)) + ctx.items_handling |= (death_link[0] & 0b10) # set local items if enabled return True - async def pop_item(self, ctx, in_stage): + async def pop_item(self, ctx: "SNIContext", in_stage: bool) -> None: from SNIClient import snes_buffered_write, snes_read if len(self.item_queue) > 0: item = self.item_queue.pop() @@ -168,8 +169,8 @@ class KDL3SNIClient(SNIClient): else: self.item_queue.append(item) # no more slots, get it next go around - async def pop_gift(self, ctx): - if ctx.stored_data[self.giftbox_key]: + async def pop_gift(self, ctx: "SNIContext") -> None: + if self.giftbox_key in ctx.stored_data and ctx.stored_data[self.giftbox_key]: from SNIClient import snes_read, snes_buffered_write key, gift = ctx.stored_data[self.giftbox_key].popitem() await pop_object(ctx, self.giftbox_key, key) @@ -214,7 +215,7 @@ class KDL3SNIClient(SNIClient): quality = min(10, quality * 2) else: # it's not really edible, but he'll eat it anyway - quality = self.client_random.choices(range(0, 2), {0: 75, 1: 25})[0] + quality = self.client_random.choices(range(0, 2), [75, 25])[0] kirby_hp = await snes_read(ctx, KDL3_KIRBY_HP, 1) gooey_hp = await snes_read(ctx, KDL3_KIRBY_HP + 2, 1) snes_buffered_write(ctx, KDL3_SOUND_FX, bytes([0x26])) @@ -224,7 +225,8 @@ class KDL3SNIClient(SNIClient): else: snes_buffered_write(ctx, KDL3_KIRBY_HP, struct.pack("H", min(kirby_hp[0] + quality, 10))) - async def pick_gift_recipient(self, ctx, gift): + async def pick_gift_recipient(self, ctx: "SNIContext", gift: int) -> None: + assert ctx.slot if gift != 4: gift_base = kdl3_gifts[gift] else: @@ -238,7 +240,7 @@ class KDL3SNIClient(SNIClient): if desire > most_applicable: most_applicable = desire most_applicable_slot = int(slot) - elif most_applicable_slot == ctx.slot and info["AcceptsAnyGift"]: + elif most_applicable_slot != ctx.slot and most_applicable == -1 and info["AcceptsAnyGift"]: # only send to ourselves if no one else will take it most_applicable_slot = int(slot) # print(most_applicable, most_applicable_slot) @@ -257,7 +259,7 @@ class KDL3SNIClient(SNIClient): item_uuid: item, }) - async def game_watcher(self, ctx) -> None: + async def game_watcher(self, ctx: "SNIContext") -> None: try: from SNIClient import snes_buffered_write, snes_flush_writes, snes_read rom = await snes_read(ctx, KDL3_ROMNAME, 0x15) @@ -278,11 +280,12 @@ class KDL3SNIClient(SNIClient): await initialize_giftboxes(ctx, self.giftbox_key, self.motherbox_key, bool(enable_gifting[0])) self.initialize_gifting = True # can't check debug anymore, without going and copying the value. might be important later. - if self.levels is None: + if not self.levels: self.levels = dict() for i in range(5): level_data = await snes_read(ctx, KDL3_LEVEL_ADDR + (14 * i), 14) - self.levels[i] = unpack("HHHHHHH", level_data) + self.levels[i] = [int.from_bytes(level_data[idx:idx+1], "little") + for idx in range(0, len(level_data), 2)] self.levels[5] = [0x0205, # Hyper Zone 0, # MG-5, can't send from here 0x0300, # Boss Butch @@ -371,7 +374,7 @@ class KDL3SNIClient(SNIClient): stages_raw = await snes_read(ctx, KDL3_COMPLETED_STAGES, 60) stages = struct.unpack("HHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", stages_raw) for i in range(30): - loc_id = 0x770000 + i + 1 + loc_id = 0x770000 + i if stages[i] == 1 and loc_id not in ctx.checked_locations: new_checks.append(loc_id) elif loc_id in ctx.checked_locations: @@ -381,8 +384,8 @@ class KDL3SNIClient(SNIClient): heart_stars = await snes_read(ctx, KDL3_HEART_STARS, 35) for i in range(5): start_ind = i * 7 - for j in range(1, 7): - level_ind = start_ind + j - 1 + for j in range(6): + level_ind = start_ind + j loc_id = 0x770100 + (6 * i) + j if heart_stars[level_ind] and loc_id not in ctx.checked_locations: new_checks.append(loc_id) @@ -401,6 +404,9 @@ class KDL3SNIClient(SNIClient): if star not in ctx.checked_locations and stars[star_addrs[star]] == 0x01: new_checks.append(star) + if not game_state: + return + if game_state[0] != 0xFF: await self.pop_gift(ctx) await self.pop_item(ctx, game_state[0] != 0xFF) @@ -408,7 +414,7 @@ class KDL3SNIClient(SNIClient): # boss status boss_flag_bytes = await snes_read(ctx, KDL3_BOSS_STATUS, 2) - boss_flag = unpack("H", boss_flag_bytes)[0] + boss_flag = int.from_bytes(boss_flag_bytes, "little") for bitmask, boss in zip(range(1, 11, 2), boss_locations.keys()): if boss_flag & (1 << bitmask) > 0 and boss not in ctx.checked_locations: new_checks.append(boss) diff --git a/worlds/kdl3/ClientAddrs.py b/worlds/kdl3/client_addrs.py similarity index 100% rename from worlds/kdl3/ClientAddrs.py rename to worlds/kdl3/client_addrs.py diff --git a/worlds/kdl3/Compression.py b/worlds/kdl3/compression.py similarity index 100% rename from worlds/kdl3/Compression.py rename to worlds/kdl3/compression.py diff --git a/worlds/kdl3/data/kdl3_basepatch.bsdiff4 b/worlds/kdl3/data/kdl3_basepatch.bsdiff4 index cd002121cd38dcb319ba2148ced46c9592c3905b..3b6b338d5a92ccec33693c644e73d13b15dbce46 100644 GIT binary patch literal 2646 zcmYM0dpOgJAIHBl&6Kej!zPWGSx$zL4oA0;#E^`}G z@=Gp-3TKo{ooTC7L@L*Gq;yi^*YEc`&vV{?z5n>UKcDCGJg-0AbdHC&rzeH{2Kc{X zH2yXK^?!yQ9O-0F+e_f=jTypO06^2~>i5L)jaPT-!TP8~5bprsJJ2K&m9<6(4@f}` zD%gPs#A4Yen`uNDjAu-n0k~086>S5|$GxE~mD}}?7IDIl4BykQJlEt}6XDl0XT8RELEq%`#bdEK+LHA#HGWeP9E<)u zECB4mR%{!8&#GRlk7$E(ssH@w_mH*`0JK+ES65 zp@E6|s252Q>pLLr2(f}gDCaX6NIfznGADBcI)&xZAnzv{WrtiK{1w=hX(J&cNtU*h7t_FblQN0)u4hU!@}JkY2Yn zW5a5{jF&sxdrVLRDgy&?pHQ0Aq}}4?91l%Er@a|$J{h?AJEw)&B&qn&JTaPB_cql> zt8IO7&e=0NGBjL`g&DP@;cf;#z`~IdZYbK>CGJ6B@w`111dAQ%mNnSgdhfB^DgV!1 zs?G;jsvR0(oBVC3%GVrT^viXL67Q!@hcFl(UT+Q@+Jn+6(;IE+HrrG35FI46&1Mc4 zpZ!e>TpG}-mMz6^((#GLHT;=UC$zPHRg^csLEszPe^?s3W5KOzDM5mDi5t6<{7!jD znATy$mhWjT!P_G#{#XO#ah@{ND$@&NFdYzV&g9=XeFxgAU>Gv93Nk29FnPX>`!=R9 z+fxurxovB1YC-2f5GMQ}Qh!wAt(?pTK7ZoKLq?>74T*oz?GO5knxXqMz z@EUPIlL~CMN|@kyXEjJ(t@J&6HZp9B5_n=7*%2WZSE{h)1i z^smKlNlVKQCC7Nw_*MQ`(#O>?!k5*RuS=^*ZA#7WBaw?QtG3*pd;N#atQuRW@2}4- z;Sd(LXFZ0Ma31l|3^hNkz}-teM|aM7Q^?Qa_{sCAi&!j7Aa#}k6j)j^tpaOZ12}D` z*|f`F`*D!kGjQsJ2f{urRi(h8$w0x1HZM8c?8guizs=aD~=pDuCV z$S^OHE+XVor4k^6k${q4@FNcc`5-`n%;iV7oe+M`GWB5DTgrVdg`eE_ zU#{^mK?Wp82CAq^0LT6Ty-o=`c>dH?suhNk+tn-Z4mVD&8$8Brt$~A}FWkOz@%VFn+`jcOEv4QV_d_}_nk<`slcqooyk!P!&2)P%Ke^#D zKs2w;yuf$JSkPz|yaCqTfajf&CPrCQ<(^B2WN5}&R%#PV%D(o)F9j3XU7Div3}2(` zcIFS)loZ#{A9yx#>;G-{?lNs;uCR!iYX0~XG5uFtJHTe(Ad~+*Ix6R?xkmWeKgqYD)%X;5R2k*%~<#dPK z=?a$|Wu{u%xqi5)Th}n;do8{_*XQt1MH84ijR@n4ZgO1<(Dl%%Zo0uMsntmX2;S6G z8`Vw!D0+ zx!`ez;#SmEpoO>VYAUo(C>PzqVdnx9n&a!lq=fbhlbJL5givj#2x_}}AnRa5MEb3_ zsGo1mjy2lVm6`|(>q^l6G;)y^#9Gq^j>tSkZ%fS>30OO0JMCxRqYa#QcRE{8+szc$Z)=&dkoW^q?=@5$^ddWPD&{$Ub4B;ZY;V#QXI+%7K)qLgA5paUnt;t7c2}+4 z*MBUvpjsxrejuPC^BvDiu>{j40J=}gA>qr}sDoS2qlV(g7ud@KJ?7Eqr#5YSnGzqO z@0i%A2Zu3>Yj?wb&OM3Q@0f1BU@f3~BXaQ9qk~&W5_6-&h7r=oJ9L{ZW&XQfq|Vpp zH1E3L(bv^Zt*{SB)mR3$63}o9H)1bipyc$yHjVo{F=2hZ|H-3Nt*xI2?YhF;M?H!w z_NmH0R_l%B?;ws=M@B|J$!_Q<-EQa`RiEwGTWr1Y*A~;*fh!k|qjU*|x#jmKZZ1B5 zjeL=Qjy^Wx#zJUsg&~ieaWOI^{;Q@lt|}nwr2b!Amo^4A?f9CsE@)>#y6Dr>uDhIt z2_5$6OY*35T~y$)m91>E{)p>)vxy|c4n zCKdgCkQ?huXfcGd}N6;FvkjXAL;gnmys4Y_&g}csYI|(;6OK z_)NZMq<3svP+R|tWY#Wr<-(J;YR6`ivf9t)=9Lwu_y|7@x-?lxY?#AT8lH`fxOr~- zAwg2shUSE4rojqLdMY=6l;q?WU>!T!rKAP*T3!cEWYw9siltk0O;XHP{_4E<(w$3s zIV7X9P?$@}7eraQVqv`Y^20_guoMnD@;GV2``wws$7~iU%#_3K0OguOa!Mn{snaLS z*fXe-UX6=H%X#0@%Pz*ffZrW{c6VoQ;iTDHfg&3#(!ob0HiXZvT3NBYwm501xcTwS zX6KP>o8YAjmMR?uCF0!icgxn~bu}v+$}jB?Tm_+0%m~OpqL9; zgG*x3sO6egnoELZ;?km(d8cKxS*|&Vnx(hy+;i@|&xiLs=lSxU=fm^!Vt5Dm`hvt+ z;4gK-|6>5szm3DJ=B#sdog|1`*Kw8fF$CWo*=8AqXpfU|e7&|Bs0sy8YKnsBc zm>6V=A%Fq+ZVMIWpnBYELjd0cMUxQeD198#!~;>sqeG-BXGAWHu3hEfr(Gi1$doc| zpSI_x$dgUvB4++zM!@a*%k=bM@y~y}wYo-Jl|H#%BY8AW5K*Uca|8T~XpJ#MyNU_DdEA+EuYv#eq;K#4(i54p{zcC}udOe=f6&mZ$@2AF zTgYkrb4gR9UR`T`m2>Jx6>Jx;;lZ`GhaJL%2^I=;){!{t+%N}W1TIL#o#SEp?51b{ z4uH7UC_r4HYgL{H1)k|bOc*UrKy73!f{fwd9={T{??rD#5`WRdh%BpDDY@*+b>vp< z`b7jCagl)?1rQQ*1cw76D!_i%8Vc?5W2CIg$avAoRU9x^G3` zP@e*TXi@K%|5z*qvyf}y?+hvq1uH;6g(9BI&qe5nlBDApl^F;m#G5BTxMnExL?AH{ zkx$D70HsucrczylVH1i(6o4oM5MRRo*G5Faxhre41I{LKZ|ulRHaLBJJn=Km%pPu9 z1=p$IOgbUXqbmSeFIxLYT_IZ1H#{}Ng%vo4He9H@uD&(& zVwc4me!yOg)QwLv4rht&nj)A@Hc|Xgd&+Z16y&Xxh&|+!dyxM}e0n=P`*T|JGC1HC z(y}L4-?x8odsO?tCC0|889cbB{(c5Pj!#KWEtRqkwb@So5&kgf4ECRlEQW%ziqg-x z%RDJh$5b!y$Uw23ayUAFEg#bYMIyjQ33n{F<1Jcd*b+OVJZA1ddZ8T5gS zxyy_z9k*)bvzxA7G&J+S4DHX(LHo7dv)TTc=Nnn2GvBkHYVlr}7E1SQ>6lL#83ymI z?Q4zK{I(aBk8LCwq}!}vTn|5I?dn^o%YIN~XyxTn5Rw!-Y`$yKSKLxaXmamL{wSU0 zdVLolmu>40eVQ>DR!09dGUz5}w4rpROMiPjWyh79sGyxJ`EuRgLG31o*-$NBX$fuG za;Y(+FX_|MYH8o<@*S7!Jo?4C*mt=ve8zQ=o`*<$3>rt9(Rc7C-*xPm>&wpS)5(v^ z1-EfJ9eQ~`h6N6M>V6(vu3l^WNp8=s9T>4|d2f8+=?k>P4tbxujzqmN>bE#A6zRDU z3HaE{TbmM0g!E=Bt7M4Uv^LvbujKrVLd!7fh2RyLZm}cl+j7H|cIo8YpWoCHQ#ER%;T6_oW#6i%%V-;ZL4Psb{7J-u=k5C zg?rB^;k1y){<{JrnH9!Gnxw8LkpZjUrEKfsM!e&;No4ahLu)_o`Lgzavn!QbZfhRS z4MiE8vNWHrhNSI7$oz&Xhty0C4A#QOw@Y|Nf--d0;Pt69oMQbzQ}X=Q;2njhin-7$ zUMGhp@PTf#0$_J)x7Y*@z&w8)Ijn1Cg?L!ubEhLb_DIWm!7bZLyg#lip7=lLIu_qX z10e2S$(8=<&%-OH52*c1niEMUMi2HJdA8nzTV_8?ozKZB>VaNrIL@d5&Fe4{BVP#$ zNPy8KaiDb#9R)z@=zpz#gEy#fVf14eTk7gqJG~Kw`e*CS&A-B%1OOlaK%|L^agvZt zp&p}*!C2M;F|N=&Q31pH=93h_Xx-PVLSR%X6_gQN^uA76%x1CkFV{?1*Gx=Fl0?8F zpn^>}1B);Wph~pB1CW~2lXy!g?Pd6*Z{y@tWQ)IeCrLR^%~*P#3K!Lrz!0o4)P>*W zu0+$J>pQLy5lBb3U|vbvO_v|c5m!4c_bJi*punIMPrB>%n-ug;_|#^-dJpy)YMNkt z5JI}=U3&m88bDl+o45+T!xCSdjsfvw`D}y4tO7DK*n7c(HTemyyMNr|qNV!km;l zD=Z8G%0l9t1$+K+>H7Fwx8T%ZRoC1Y-G7(*y*0~ZhL)-6R5U`o(>T}cAKQT zRtbg8OXaHZ5nM!DZPkiiP<_LNxAaR#b*!V>Q*U5nx^~mf4!^kFc%+l}dF9*RG!jBb zvb85+_N+c=y0rLu=!q$ALgMamYM*$;r{)Lw2CCO_BMrBuRQ5u?dAL&#EkX$&KO;a4 zqWV9SHwD|yciRCG;9=O}?eCBAz&T_%>TCmJq#`CHzNk0+)=`2D3~IYYg#R#32Q zdGj(IQ*ak^;KMv`c2ct3Q~UTG@knc+R)9ef(oshGCTk~_x- R2S2ZGPW5pM7JXgjzX0o>8Uz3U diff --git a/worlds/kdl3/Gifting.py b/worlds/kdl3/gifting.py similarity index 90% rename from worlds/kdl3/Gifting.py rename to worlds/kdl3/gifting.py index 8ccba7ec1a..e162609100 100644 --- a/worlds/kdl3/Gifting.py +++ b/worlds/kdl3/gifting.py @@ -1,8 +1,11 @@ # Small subfile to handle gifting info such as desired traits and giftbox management import typing +if typing.TYPE_CHECKING: + from SNIClient import SNIContext -async def update_object(ctx, key: str, value: typing.Dict): + +async def update_object(ctx: "SNIContext", key: str, value: typing.Dict[str, typing.Any]) -> None: await ctx.send_msgs([ { "cmd": "Set", @@ -16,7 +19,7 @@ async def update_object(ctx, key: str, value: typing.Dict): ]) -async def pop_object(ctx, key: str, value: str): +async def pop_object(ctx: "SNIContext", key: str, value: str) -> None: await ctx.send_msgs([ { "cmd": "Set", @@ -30,14 +33,14 @@ async def pop_object(ctx, key: str, value: str): ]) -async def initialize_giftboxes(ctx, giftbox_key: str, motherbox_key: str, is_open: bool): +async def initialize_giftboxes(ctx: "SNIContext", giftbox_key: str, motherbox_key: str, is_open: bool) -> None: ctx.set_notify(motherbox_key, giftbox_key) await update_object(ctx, f"Giftboxes;{ctx.team}", {f"{ctx.slot}": - { - "IsOpen": is_open, - **kdl3_gifting_options - }}) - ctx.gifting = is_open + { + "IsOpen": is_open, + **kdl3_gifting_options + }}) + ctx.client_handler.gifting = is_open kdl3_gifting_options = { diff --git a/worlds/kdl3/Items.py b/worlds/kdl3/items.py similarity index 95% rename from worlds/kdl3/Items.py rename to worlds/kdl3/items.py index 66c7f8fee3..72687a6065 100644 --- a/worlds/kdl3/Items.py +++ b/worlds/kdl3/items.py @@ -77,9 +77,9 @@ filler_item_weights = { } star_item_weights = { - "Little Star": 4, - "Medium Star": 2, - "Big Star": 1 + "Little Star": 16, + "Medium Star": 8, + "Big Star": 4 } total_filler_weights = { @@ -102,4 +102,4 @@ item_names = { "Animal Friend": set(animal_friend_table), } -lookup_name_to_id: typing.Dict[str, int] = {item_name: data.code for item_name, data in item_table.items() if data.code} +lookup_item_to_id: typing.Dict[str, int] = {item_name: data.code for item_name, data in item_table.items() if data.code} diff --git a/worlds/kdl3/locations.py b/worlds/kdl3/locations.py new file mode 100644 index 0000000000..4fa1bfad70 --- /dev/null +++ b/worlds/kdl3/locations.py @@ -0,0 +1,940 @@ +import typing +from BaseClasses import Location, Region +from .names import location_name + +if typing.TYPE_CHECKING: + from .room import KDL3Room + + +class KDL3Location(Location): + game: str = "Kirby's Dream Land 3" + room: typing.Optional["KDL3Room"] = None + + def __init__(self, player: int, name: str, address: typing.Optional[int], parent: typing.Union[Region, None]): + super().__init__(player, name, address, parent) + if not address: + self.show_in_spoiler = False + + +stage_locations = { + 0x770000: location_name.grass_land_1, + 0x770001: location_name.grass_land_2, + 0x770002: location_name.grass_land_3, + 0x770003: location_name.grass_land_4, + 0x770004: location_name.grass_land_5, + 0x770005: location_name.grass_land_6, + 0x770006: location_name.ripple_field_1, + 0x770007: location_name.ripple_field_2, + 0x770008: location_name.ripple_field_3, + 0x770009: location_name.ripple_field_4, + 0x77000A: location_name.ripple_field_5, + 0x77000B: location_name.ripple_field_6, + 0x77000C: location_name.sand_canyon_1, + 0x77000D: location_name.sand_canyon_2, + 0x77000E: location_name.sand_canyon_3, + 0x77000F: location_name.sand_canyon_4, + 0x770010: location_name.sand_canyon_5, + 0x770011: location_name.sand_canyon_6, + 0x770012: location_name.cloudy_park_1, + 0x770013: location_name.cloudy_park_2, + 0x770014: location_name.cloudy_park_3, + 0x770015: location_name.cloudy_park_4, + 0x770016: location_name.cloudy_park_5, + 0x770017: location_name.cloudy_park_6, + 0x770018: location_name.iceberg_1, + 0x770019: location_name.iceberg_2, + 0x77001A: location_name.iceberg_3, + 0x77001B: location_name.iceberg_4, + 0x77001C: location_name.iceberg_5, + 0x77001D: location_name.iceberg_6, +} + +heart_star_locations = { + 0x770100: location_name.grass_land_tulip, + 0x770101: location_name.grass_land_muchi, + 0x770102: location_name.grass_land_pitcherman, + 0x770103: location_name.grass_land_chao, + 0x770104: location_name.grass_land_mine, + 0x770105: location_name.grass_land_pierre, + 0x770106: location_name.ripple_field_kamuribana, + 0x770107: location_name.ripple_field_bakasa, + 0x770108: location_name.ripple_field_elieel, + 0x770109: location_name.ripple_field_toad, + 0x77010A: location_name.ripple_field_mama_pitch, + 0x77010B: location_name.ripple_field_hb002, + 0x77010C: location_name.sand_canyon_mushrooms, + 0x77010D: location_name.sand_canyon_auntie, + 0x77010E: location_name.sand_canyon_caramello, + 0x77010F: location_name.sand_canyon_hikari, + 0x770110: location_name.sand_canyon_nyupun, + 0x770111: location_name.sand_canyon_rob, + 0x770112: location_name.cloudy_park_hibanamodoki, + 0x770113: location_name.cloudy_park_piyokeko, + 0x770114: location_name.cloudy_park_mrball, + 0x770115: location_name.cloudy_park_mikarin, + 0x770116: location_name.cloudy_park_pick, + 0x770117: location_name.cloudy_park_hb007, + 0x770118: location_name.iceberg_kogoesou, + 0x770119: location_name.iceberg_samus, + 0x77011A: location_name.iceberg_kawasaki, + 0x77011B: location_name.iceberg_name, + 0x77011C: location_name.iceberg_shiro, + 0x77011D: location_name.iceberg_angel, +} + +boss_locations = { + 0x770200: location_name.grass_land_whispy, + 0x770201: location_name.ripple_field_acro, + 0x770202: location_name.sand_canyon_poncon, + 0x770203: location_name.cloudy_park_ado, + 0x770204: location_name.iceberg_dedede, +} + +consumable_locations = { + 0x770300: location_name.grass_land_1_u1, + 0x770301: location_name.grass_land_1_m1, + 0x770302: location_name.grass_land_2_u1, + 0x770303: location_name.grass_land_3_u1, + 0x770304: location_name.grass_land_3_m1, + 0x770305: location_name.grass_land_4_m1, + 0x770306: location_name.grass_land_4_u1, + 0x770307: location_name.grass_land_4_m2, + 0x770308: location_name.grass_land_4_m3, + 0x770309: location_name.grass_land_6_u1, + 0x77030A: location_name.grass_land_6_u2, + 0x77030B: location_name.ripple_field_2_u1, + 0x77030C: location_name.ripple_field_2_m1, + 0x77030D: location_name.ripple_field_3_m1, + 0x77030E: location_name.ripple_field_3_u1, + 0x77030F: location_name.ripple_field_4_m2, + 0x770310: location_name.ripple_field_4_u1, + 0x770311: location_name.ripple_field_4_m1, + 0x770312: location_name.ripple_field_5_u1, + 0x770313: location_name.ripple_field_5_m2, + 0x770314: location_name.ripple_field_5_m1, + 0x770315: location_name.sand_canyon_1_u1, + 0x770316: location_name.sand_canyon_2_u1, + 0x770317: location_name.sand_canyon_2_m1, + 0x770318: location_name.sand_canyon_4_m1, + 0x770319: location_name.sand_canyon_4_u1, + 0x77031A: location_name.sand_canyon_4_m2, + 0x77031B: location_name.sand_canyon_5_u1, + 0x77031C: location_name.sand_canyon_5_u3, + 0x77031D: location_name.sand_canyon_5_m1, + 0x77031E: location_name.sand_canyon_5_u4, + 0x77031F: location_name.sand_canyon_5_u2, + 0x770320: location_name.cloudy_park_1_m1, + 0x770321: location_name.cloudy_park_1_u1, + 0x770322: location_name.cloudy_park_4_u1, + 0x770323: location_name.cloudy_park_4_m1, + 0x770324: location_name.cloudy_park_5_m1, + 0x770325: location_name.cloudy_park_6_u1, + 0x770326: location_name.iceberg_3_m1, + 0x770327: location_name.iceberg_5_u1, + 0x770328: location_name.iceberg_5_u2, + 0x770329: location_name.iceberg_5_u3, + 0x77032A: location_name.iceberg_6_m1, + 0x77032B: location_name.iceberg_6_u1, +} + +level_consumables = { + 1: [0, 1], + 2: [2], + 3: [3, 4], + 4: [5, 6, 7, 8], + 6: [9, 10], + 8: [11, 12], + 9: [13, 14], + 10: [15, 16, 17], + 11: [18, 19, 20], + 13: [21], + 14: [22, 23], + 16: [24, 25, 26], + 17: [27, 28, 29, 30, 31], + 19: [32, 33], + 22: [34, 35], + 23: [36], + 24: [37], + 27: [38], + 29: [39, 40, 41], + 30: [42, 43], +} + +star_locations = { + 0x770401: location_name.grass_land_1_s1, + 0x770402: location_name.grass_land_1_s2, + 0x770403: location_name.grass_land_1_s3, + 0x770404: location_name.grass_land_1_s4, + 0x770405: location_name.grass_land_1_s5, + 0x770406: location_name.grass_land_1_s6, + 0x770407: location_name.grass_land_1_s7, + 0x770408: location_name.grass_land_1_s8, + 0x770409: location_name.grass_land_1_s9, + 0x77040a: location_name.grass_land_1_s10, + 0x77040b: location_name.grass_land_1_s11, + 0x77040c: location_name.grass_land_1_s12, + 0x77040d: location_name.grass_land_1_s13, + 0x77040e: location_name.grass_land_1_s14, + 0x77040f: location_name.grass_land_1_s15, + 0x770410: location_name.grass_land_1_s16, + 0x770411: location_name.grass_land_1_s17, + 0x770412: location_name.grass_land_1_s18, + 0x770413: location_name.grass_land_1_s19, + 0x770414: location_name.grass_land_1_s20, + 0x770415: location_name.grass_land_1_s21, + 0x770416: location_name.grass_land_1_s22, + 0x770417: location_name.grass_land_1_s23, + 0x770418: location_name.grass_land_2_s1, + 0x770419: location_name.grass_land_2_s2, + 0x77041a: location_name.grass_land_2_s3, + 0x77041b: location_name.grass_land_2_s4, + 0x77041c: location_name.grass_land_2_s5, + 0x77041d: location_name.grass_land_2_s6, + 0x77041e: location_name.grass_land_2_s7, + 0x77041f: location_name.grass_land_2_s8, + 0x770420: location_name.grass_land_2_s9, + 0x770421: location_name.grass_land_2_s10, + 0x770422: location_name.grass_land_2_s11, + 0x770423: location_name.grass_land_2_s12, + 0x770424: location_name.grass_land_2_s13, + 0x770425: location_name.grass_land_2_s14, + 0x770426: location_name.grass_land_2_s15, + 0x770427: location_name.grass_land_2_s16, + 0x770428: location_name.grass_land_2_s17, + 0x770429: location_name.grass_land_2_s18, + 0x77042a: location_name.grass_land_2_s19, + 0x77042b: location_name.grass_land_2_s20, + 0x77042c: location_name.grass_land_2_s21, + 0x77042d: location_name.grass_land_3_s1, + 0x77042e: location_name.grass_land_3_s2, + 0x77042f: location_name.grass_land_3_s3, + 0x770430: location_name.grass_land_3_s4, + 0x770431: location_name.grass_land_3_s5, + 0x770432: location_name.grass_land_3_s6, + 0x770433: location_name.grass_land_3_s7, + 0x770434: location_name.grass_land_3_s8, + 0x770435: location_name.grass_land_3_s9, + 0x770436: location_name.grass_land_3_s10, + 0x770437: location_name.grass_land_3_s11, + 0x770438: location_name.grass_land_3_s12, + 0x770439: location_name.grass_land_3_s13, + 0x77043a: location_name.grass_land_3_s14, + 0x77043b: location_name.grass_land_3_s15, + 0x77043c: location_name.grass_land_3_s16, + 0x77043d: location_name.grass_land_3_s17, + 0x77043e: location_name.grass_land_3_s18, + 0x77043f: location_name.grass_land_3_s19, + 0x770440: location_name.grass_land_3_s20, + 0x770441: location_name.grass_land_3_s21, + 0x770442: location_name.grass_land_3_s22, + 0x770443: location_name.grass_land_3_s23, + 0x770444: location_name.grass_land_3_s24, + 0x770445: location_name.grass_land_3_s25, + 0x770446: location_name.grass_land_3_s26, + 0x770447: location_name.grass_land_3_s27, + 0x770448: location_name.grass_land_3_s28, + 0x770449: location_name.grass_land_3_s29, + 0x77044a: location_name.grass_land_3_s30, + 0x77044b: location_name.grass_land_3_s31, + 0x77044c: location_name.grass_land_4_s1, + 0x77044d: location_name.grass_land_4_s2, + 0x77044e: location_name.grass_land_4_s3, + 0x77044f: location_name.grass_land_4_s4, + 0x770450: location_name.grass_land_4_s5, + 0x770451: location_name.grass_land_4_s6, + 0x770452: location_name.grass_land_4_s7, + 0x770453: location_name.grass_land_4_s8, + 0x770454: location_name.grass_land_4_s9, + 0x770455: location_name.grass_land_4_s10, + 0x770456: location_name.grass_land_4_s11, + 0x770457: location_name.grass_land_4_s12, + 0x770458: location_name.grass_land_4_s13, + 0x770459: location_name.grass_land_4_s14, + 0x77045a: location_name.grass_land_4_s15, + 0x77045b: location_name.grass_land_4_s16, + 0x77045c: location_name.grass_land_4_s17, + 0x77045d: location_name.grass_land_4_s18, + 0x77045e: location_name.grass_land_4_s19, + 0x77045f: location_name.grass_land_4_s20, + 0x770460: location_name.grass_land_4_s21, + 0x770461: location_name.grass_land_4_s22, + 0x770462: location_name.grass_land_4_s23, + 0x770463: location_name.grass_land_4_s24, + 0x770464: location_name.grass_land_4_s25, + 0x770465: location_name.grass_land_4_s26, + 0x770466: location_name.grass_land_4_s27, + 0x770467: location_name.grass_land_4_s28, + 0x770468: location_name.grass_land_4_s29, + 0x770469: location_name.grass_land_4_s30, + 0x77046a: location_name.grass_land_4_s31, + 0x77046b: location_name.grass_land_4_s32, + 0x77046c: location_name.grass_land_4_s33, + 0x77046d: location_name.grass_land_4_s34, + 0x77046e: location_name.grass_land_4_s35, + 0x77046f: location_name.grass_land_4_s36, + 0x770470: location_name.grass_land_4_s37, + 0x770471: location_name.grass_land_5_s1, + 0x770472: location_name.grass_land_5_s2, + 0x770473: location_name.grass_land_5_s3, + 0x770474: location_name.grass_land_5_s4, + 0x770475: location_name.grass_land_5_s5, + 0x770476: location_name.grass_land_5_s6, + 0x770477: location_name.grass_land_5_s7, + 0x770478: location_name.grass_land_5_s8, + 0x770479: location_name.grass_land_5_s9, + 0x77047a: location_name.grass_land_5_s10, + 0x77047b: location_name.grass_land_5_s11, + 0x77047c: location_name.grass_land_5_s12, + 0x77047d: location_name.grass_land_5_s13, + 0x77047e: location_name.grass_land_5_s14, + 0x77047f: location_name.grass_land_5_s15, + 0x770480: location_name.grass_land_5_s16, + 0x770481: location_name.grass_land_5_s17, + 0x770482: location_name.grass_land_5_s18, + 0x770483: location_name.grass_land_5_s19, + 0x770484: location_name.grass_land_5_s20, + 0x770485: location_name.grass_land_5_s21, + 0x770486: location_name.grass_land_5_s22, + 0x770487: location_name.grass_land_5_s23, + 0x770488: location_name.grass_land_5_s24, + 0x770489: location_name.grass_land_5_s25, + 0x77048a: location_name.grass_land_5_s26, + 0x77048b: location_name.grass_land_5_s27, + 0x77048c: location_name.grass_land_5_s28, + 0x77048d: location_name.grass_land_5_s29, + 0x77048e: location_name.grass_land_6_s1, + 0x77048f: location_name.grass_land_6_s2, + 0x770490: location_name.grass_land_6_s3, + 0x770491: location_name.grass_land_6_s4, + 0x770492: location_name.grass_land_6_s5, + 0x770493: location_name.grass_land_6_s6, + 0x770494: location_name.grass_land_6_s7, + 0x770495: location_name.grass_land_6_s8, + 0x770496: location_name.grass_land_6_s9, + 0x770497: location_name.grass_land_6_s10, + 0x770498: location_name.grass_land_6_s11, + 0x770499: location_name.grass_land_6_s12, + 0x77049a: location_name.grass_land_6_s13, + 0x77049b: location_name.grass_land_6_s14, + 0x77049c: location_name.grass_land_6_s15, + 0x77049d: location_name.grass_land_6_s16, + 0x77049e: location_name.grass_land_6_s17, + 0x77049f: location_name.grass_land_6_s18, + 0x7704a0: location_name.grass_land_6_s19, + 0x7704a1: location_name.grass_land_6_s20, + 0x7704a2: location_name.grass_land_6_s21, + 0x7704a3: location_name.grass_land_6_s22, + 0x7704a4: location_name.grass_land_6_s23, + 0x7704a5: location_name.grass_land_6_s24, + 0x7704a6: location_name.grass_land_6_s25, + 0x7704a7: location_name.grass_land_6_s26, + 0x7704a8: location_name.grass_land_6_s27, + 0x7704a9: location_name.grass_land_6_s28, + 0x7704aa: location_name.grass_land_6_s29, + 0x7704ab: location_name.ripple_field_1_s1, + 0x7704ac: location_name.ripple_field_1_s2, + 0x7704ad: location_name.ripple_field_1_s3, + 0x7704ae: location_name.ripple_field_1_s4, + 0x7704af: location_name.ripple_field_1_s5, + 0x7704b0: location_name.ripple_field_1_s6, + 0x7704b1: location_name.ripple_field_1_s7, + 0x7704b2: location_name.ripple_field_1_s8, + 0x7704b3: location_name.ripple_field_1_s9, + 0x7704b4: location_name.ripple_field_1_s10, + 0x7704b5: location_name.ripple_field_1_s11, + 0x7704b6: location_name.ripple_field_1_s12, + 0x7704b7: location_name.ripple_field_1_s13, + 0x7704b8: location_name.ripple_field_1_s14, + 0x7704b9: location_name.ripple_field_1_s15, + 0x7704ba: location_name.ripple_field_1_s16, + 0x7704bb: location_name.ripple_field_1_s17, + 0x7704bc: location_name.ripple_field_1_s18, + 0x7704bd: location_name.ripple_field_1_s19, + 0x7704be: location_name.ripple_field_2_s1, + 0x7704bf: location_name.ripple_field_2_s2, + 0x7704c0: location_name.ripple_field_2_s3, + 0x7704c1: location_name.ripple_field_2_s4, + 0x7704c2: location_name.ripple_field_2_s5, + 0x7704c3: location_name.ripple_field_2_s6, + 0x7704c4: location_name.ripple_field_2_s7, + 0x7704c5: location_name.ripple_field_2_s8, + 0x7704c6: location_name.ripple_field_2_s9, + 0x7704c7: location_name.ripple_field_2_s10, + 0x7704c8: location_name.ripple_field_2_s11, + 0x7704c9: location_name.ripple_field_2_s12, + 0x7704ca: location_name.ripple_field_2_s13, + 0x7704cb: location_name.ripple_field_2_s14, + 0x7704cc: location_name.ripple_field_2_s15, + 0x7704cd: location_name.ripple_field_2_s16, + 0x7704ce: location_name.ripple_field_2_s17, + 0x7704cf: location_name.ripple_field_3_s1, + 0x7704d0: location_name.ripple_field_3_s2, + 0x7704d1: location_name.ripple_field_3_s3, + 0x7704d2: location_name.ripple_field_3_s4, + 0x7704d3: location_name.ripple_field_3_s5, + 0x7704d4: location_name.ripple_field_3_s6, + 0x7704d5: location_name.ripple_field_3_s7, + 0x7704d6: location_name.ripple_field_3_s8, + 0x7704d7: location_name.ripple_field_3_s9, + 0x7704d8: location_name.ripple_field_3_s10, + 0x7704d9: location_name.ripple_field_3_s11, + 0x7704da: location_name.ripple_field_3_s12, + 0x7704db: location_name.ripple_field_3_s13, + 0x7704dc: location_name.ripple_field_3_s14, + 0x7704dd: location_name.ripple_field_3_s15, + 0x7704de: location_name.ripple_field_3_s16, + 0x7704df: location_name.ripple_field_3_s17, + 0x7704e0: location_name.ripple_field_3_s18, + 0x7704e1: location_name.ripple_field_3_s19, + 0x7704e2: location_name.ripple_field_3_s20, + 0x7704e3: location_name.ripple_field_3_s21, + 0x7704e4: location_name.ripple_field_4_s1, + 0x7704e5: location_name.ripple_field_4_s2, + 0x7704e6: location_name.ripple_field_4_s3, + 0x7704e7: location_name.ripple_field_4_s4, + 0x7704e8: location_name.ripple_field_4_s5, + 0x7704e9: location_name.ripple_field_4_s6, + 0x7704ea: location_name.ripple_field_4_s7, + 0x7704eb: location_name.ripple_field_4_s8, + 0x7704ec: location_name.ripple_field_4_s9, + 0x7704ed: location_name.ripple_field_4_s10, + 0x7704ee: location_name.ripple_field_4_s11, + 0x7704ef: location_name.ripple_field_4_s12, + 0x7704f0: location_name.ripple_field_4_s13, + 0x7704f1: location_name.ripple_field_4_s14, + 0x7704f2: location_name.ripple_field_4_s15, + 0x7704f3: location_name.ripple_field_4_s16, + 0x7704f4: location_name.ripple_field_4_s17, + 0x7704f5: location_name.ripple_field_4_s18, + 0x7704f6: location_name.ripple_field_4_s19, + 0x7704f7: location_name.ripple_field_4_s20, + 0x7704f8: location_name.ripple_field_4_s21, + 0x7704f9: location_name.ripple_field_4_s22, + 0x7704fa: location_name.ripple_field_4_s23, + 0x7704fb: location_name.ripple_field_4_s24, + 0x7704fc: location_name.ripple_field_4_s25, + 0x7704fd: location_name.ripple_field_4_s26, + 0x7704fe: location_name.ripple_field_4_s27, + 0x7704ff: location_name.ripple_field_4_s28, + 0x770500: location_name.ripple_field_4_s29, + 0x770501: location_name.ripple_field_4_s30, + 0x770502: location_name.ripple_field_4_s31, + 0x770503: location_name.ripple_field_4_s32, + 0x770504: location_name.ripple_field_4_s33, + 0x770505: location_name.ripple_field_4_s34, + 0x770506: location_name.ripple_field_4_s35, + 0x770507: location_name.ripple_field_4_s36, + 0x770508: location_name.ripple_field_4_s37, + 0x770509: location_name.ripple_field_4_s38, + 0x77050a: location_name.ripple_field_4_s39, + 0x77050b: location_name.ripple_field_4_s40, + 0x77050c: location_name.ripple_field_4_s41, + 0x77050d: location_name.ripple_field_4_s42, + 0x77050e: location_name.ripple_field_4_s43, + 0x77050f: location_name.ripple_field_4_s44, + 0x770510: location_name.ripple_field_4_s45, + 0x770511: location_name.ripple_field_4_s46, + 0x770512: location_name.ripple_field_4_s47, + 0x770513: location_name.ripple_field_4_s48, + 0x770514: location_name.ripple_field_4_s49, + 0x770515: location_name.ripple_field_4_s50, + 0x770516: location_name.ripple_field_4_s51, + 0x770517: location_name.ripple_field_5_s1, + 0x770518: location_name.ripple_field_5_s2, + 0x770519: location_name.ripple_field_5_s3, + 0x77051a: location_name.ripple_field_5_s4, + 0x77051b: location_name.ripple_field_5_s5, + 0x77051c: location_name.ripple_field_5_s6, + 0x77051d: location_name.ripple_field_5_s7, + 0x77051e: location_name.ripple_field_5_s8, + 0x77051f: location_name.ripple_field_5_s9, + 0x770520: location_name.ripple_field_5_s10, + 0x770521: location_name.ripple_field_5_s11, + 0x770522: location_name.ripple_field_5_s12, + 0x770523: location_name.ripple_field_5_s13, + 0x770524: location_name.ripple_field_5_s14, + 0x770525: location_name.ripple_field_5_s15, + 0x770526: location_name.ripple_field_5_s16, + 0x770527: location_name.ripple_field_5_s17, + 0x770528: location_name.ripple_field_5_s18, + 0x770529: location_name.ripple_field_5_s19, + 0x77052a: location_name.ripple_field_5_s20, + 0x77052b: location_name.ripple_field_5_s21, + 0x77052c: location_name.ripple_field_5_s22, + 0x77052d: location_name.ripple_field_5_s23, + 0x77052e: location_name.ripple_field_5_s24, + 0x77052f: location_name.ripple_field_5_s25, + 0x770530: location_name.ripple_field_5_s26, + 0x770531: location_name.ripple_field_5_s27, + 0x770532: location_name.ripple_field_5_s28, + 0x770533: location_name.ripple_field_5_s29, + 0x770534: location_name.ripple_field_5_s30, + 0x770535: location_name.ripple_field_5_s31, + 0x770536: location_name.ripple_field_5_s32, + 0x770537: location_name.ripple_field_5_s33, + 0x770538: location_name.ripple_field_5_s34, + 0x770539: location_name.ripple_field_5_s35, + 0x77053a: location_name.ripple_field_5_s36, + 0x77053b: location_name.ripple_field_5_s37, + 0x77053c: location_name.ripple_field_5_s38, + 0x77053d: location_name.ripple_field_5_s39, + 0x77053e: location_name.ripple_field_5_s40, + 0x77053f: location_name.ripple_field_5_s41, + 0x770540: location_name.ripple_field_5_s42, + 0x770541: location_name.ripple_field_5_s43, + 0x770542: location_name.ripple_field_5_s44, + 0x770543: location_name.ripple_field_5_s45, + 0x770544: location_name.ripple_field_5_s46, + 0x770545: location_name.ripple_field_5_s47, + 0x770546: location_name.ripple_field_5_s48, + 0x770547: location_name.ripple_field_5_s49, + 0x770548: location_name.ripple_field_5_s50, + 0x770549: location_name.ripple_field_5_s51, + 0x77054a: location_name.ripple_field_6_s1, + 0x77054b: location_name.ripple_field_6_s2, + 0x77054c: location_name.ripple_field_6_s3, + 0x77054d: location_name.ripple_field_6_s4, + 0x77054e: location_name.ripple_field_6_s5, + 0x77054f: location_name.ripple_field_6_s6, + 0x770550: location_name.ripple_field_6_s7, + 0x770551: location_name.ripple_field_6_s8, + 0x770552: location_name.ripple_field_6_s9, + 0x770553: location_name.ripple_field_6_s10, + 0x770554: location_name.ripple_field_6_s11, + 0x770555: location_name.ripple_field_6_s12, + 0x770556: location_name.ripple_field_6_s13, + 0x770557: location_name.ripple_field_6_s14, + 0x770558: location_name.ripple_field_6_s15, + 0x770559: location_name.ripple_field_6_s16, + 0x77055a: location_name.ripple_field_6_s17, + 0x77055b: location_name.ripple_field_6_s18, + 0x77055c: location_name.ripple_field_6_s19, + 0x77055d: location_name.ripple_field_6_s20, + 0x77055e: location_name.ripple_field_6_s21, + 0x77055f: location_name.ripple_field_6_s22, + 0x770560: location_name.ripple_field_6_s23, + 0x770561: location_name.sand_canyon_1_s1, + 0x770562: location_name.sand_canyon_1_s2, + 0x770563: location_name.sand_canyon_1_s3, + 0x770564: location_name.sand_canyon_1_s4, + 0x770565: location_name.sand_canyon_1_s5, + 0x770566: location_name.sand_canyon_1_s6, + 0x770567: location_name.sand_canyon_1_s7, + 0x770568: location_name.sand_canyon_1_s8, + 0x770569: location_name.sand_canyon_1_s9, + 0x77056a: location_name.sand_canyon_1_s10, + 0x77056b: location_name.sand_canyon_1_s11, + 0x77056c: location_name.sand_canyon_1_s12, + 0x77056d: location_name.sand_canyon_1_s13, + 0x77056e: location_name.sand_canyon_1_s14, + 0x77056f: location_name.sand_canyon_1_s15, + 0x770570: location_name.sand_canyon_1_s16, + 0x770571: location_name.sand_canyon_1_s17, + 0x770572: location_name.sand_canyon_1_s18, + 0x770573: location_name.sand_canyon_1_s19, + 0x770574: location_name.sand_canyon_1_s20, + 0x770575: location_name.sand_canyon_1_s21, + 0x770576: location_name.sand_canyon_1_s22, + 0x770577: location_name.sand_canyon_2_s1, + 0x770578: location_name.sand_canyon_2_s2, + 0x770579: location_name.sand_canyon_2_s3, + 0x77057a: location_name.sand_canyon_2_s4, + 0x77057b: location_name.sand_canyon_2_s5, + 0x77057c: location_name.sand_canyon_2_s6, + 0x77057d: location_name.sand_canyon_2_s7, + 0x77057e: location_name.sand_canyon_2_s8, + 0x77057f: location_name.sand_canyon_2_s9, + 0x770580: location_name.sand_canyon_2_s10, + 0x770581: location_name.sand_canyon_2_s11, + 0x770582: location_name.sand_canyon_2_s12, + 0x770583: location_name.sand_canyon_2_s13, + 0x770584: location_name.sand_canyon_2_s14, + 0x770585: location_name.sand_canyon_2_s15, + 0x770586: location_name.sand_canyon_2_s16, + 0x770587: location_name.sand_canyon_2_s17, + 0x770588: location_name.sand_canyon_2_s18, + 0x770589: location_name.sand_canyon_2_s19, + 0x77058a: location_name.sand_canyon_2_s20, + 0x77058b: location_name.sand_canyon_2_s21, + 0x77058c: location_name.sand_canyon_2_s22, + 0x77058d: location_name.sand_canyon_2_s23, + 0x77058e: location_name.sand_canyon_2_s24, + 0x77058f: location_name.sand_canyon_2_s25, + 0x770590: location_name.sand_canyon_2_s26, + 0x770591: location_name.sand_canyon_2_s27, + 0x770592: location_name.sand_canyon_2_s28, + 0x770593: location_name.sand_canyon_2_s29, + 0x770594: location_name.sand_canyon_2_s30, + 0x770595: location_name.sand_canyon_2_s31, + 0x770596: location_name.sand_canyon_2_s32, + 0x770597: location_name.sand_canyon_2_s33, + 0x770598: location_name.sand_canyon_2_s34, + 0x770599: location_name.sand_canyon_2_s35, + 0x77059a: location_name.sand_canyon_2_s36, + 0x77059b: location_name.sand_canyon_2_s37, + 0x77059c: location_name.sand_canyon_2_s38, + 0x77059d: location_name.sand_canyon_2_s39, + 0x77059e: location_name.sand_canyon_2_s40, + 0x77059f: location_name.sand_canyon_2_s41, + 0x7705a0: location_name.sand_canyon_2_s42, + 0x7705a1: location_name.sand_canyon_2_s43, + 0x7705a2: location_name.sand_canyon_2_s44, + 0x7705a3: location_name.sand_canyon_2_s45, + 0x7705a4: location_name.sand_canyon_2_s46, + 0x7705a5: location_name.sand_canyon_2_s47, + 0x7705a6: location_name.sand_canyon_2_s48, + 0x7705a7: location_name.sand_canyon_3_s1, + 0x7705a8: location_name.sand_canyon_3_s2, + 0x7705a9: location_name.sand_canyon_3_s3, + 0x7705aa: location_name.sand_canyon_3_s4, + 0x7705ab: location_name.sand_canyon_3_s5, + 0x7705ac: location_name.sand_canyon_3_s6, + 0x7705ad: location_name.sand_canyon_3_s7, + 0x7705ae: location_name.sand_canyon_3_s8, + 0x7705af: location_name.sand_canyon_3_s9, + 0x7705b0: location_name.sand_canyon_3_s10, + 0x7705b1: location_name.sand_canyon_4_s1, + 0x7705b2: location_name.sand_canyon_4_s2, + 0x7705b3: location_name.sand_canyon_4_s3, + 0x7705b4: location_name.sand_canyon_4_s4, + 0x7705b5: location_name.sand_canyon_4_s5, + 0x7705b6: location_name.sand_canyon_4_s6, + 0x7705b7: location_name.sand_canyon_4_s7, + 0x7705b8: location_name.sand_canyon_4_s8, + 0x7705b9: location_name.sand_canyon_4_s9, + 0x7705ba: location_name.sand_canyon_4_s10, + 0x7705bb: location_name.sand_canyon_4_s11, + 0x7705bc: location_name.sand_canyon_4_s12, + 0x7705bd: location_name.sand_canyon_4_s13, + 0x7705be: location_name.sand_canyon_4_s14, + 0x7705bf: location_name.sand_canyon_4_s15, + 0x7705c0: location_name.sand_canyon_4_s16, + 0x7705c1: location_name.sand_canyon_4_s17, + 0x7705c2: location_name.sand_canyon_4_s18, + 0x7705c3: location_name.sand_canyon_4_s19, + 0x7705c4: location_name.sand_canyon_4_s20, + 0x7705c5: location_name.sand_canyon_4_s21, + 0x7705c6: location_name.sand_canyon_4_s22, + 0x7705c7: location_name.sand_canyon_4_s23, + 0x7705c8: location_name.sand_canyon_5_s1, + 0x7705c9: location_name.sand_canyon_5_s2, + 0x7705ca: location_name.sand_canyon_5_s3, + 0x7705cb: location_name.sand_canyon_5_s4, + 0x7705cc: location_name.sand_canyon_5_s5, + 0x7705cd: location_name.sand_canyon_5_s6, + 0x7705ce: location_name.sand_canyon_5_s7, + 0x7705cf: location_name.sand_canyon_5_s8, + 0x7705d0: location_name.sand_canyon_5_s9, + 0x7705d1: location_name.sand_canyon_5_s10, + 0x7705d2: location_name.sand_canyon_5_s11, + 0x7705d3: location_name.sand_canyon_5_s12, + 0x7705d4: location_name.sand_canyon_5_s13, + 0x7705d5: location_name.sand_canyon_5_s14, + 0x7705d6: location_name.sand_canyon_5_s15, + 0x7705d7: location_name.sand_canyon_5_s16, + 0x7705d8: location_name.sand_canyon_5_s17, + 0x7705d9: location_name.sand_canyon_5_s18, + 0x7705da: location_name.sand_canyon_5_s19, + 0x7705db: location_name.sand_canyon_5_s20, + 0x7705dc: location_name.sand_canyon_5_s21, + 0x7705dd: location_name.sand_canyon_5_s22, + 0x7705de: location_name.sand_canyon_5_s23, + 0x7705df: location_name.sand_canyon_5_s24, + 0x7705e0: location_name.sand_canyon_5_s25, + 0x7705e1: location_name.sand_canyon_5_s26, + 0x7705e2: location_name.sand_canyon_5_s27, + 0x7705e3: location_name.sand_canyon_5_s28, + 0x7705e4: location_name.sand_canyon_5_s29, + 0x7705e5: location_name.sand_canyon_5_s30, + 0x7705e6: location_name.sand_canyon_5_s31, + 0x7705e7: location_name.sand_canyon_5_s32, + 0x7705e8: location_name.sand_canyon_5_s33, + 0x7705e9: location_name.sand_canyon_5_s34, + 0x7705ea: location_name.sand_canyon_5_s35, + 0x7705eb: location_name.sand_canyon_5_s36, + 0x7705ec: location_name.sand_canyon_5_s37, + 0x7705ed: location_name.sand_canyon_5_s38, + 0x7705ee: location_name.sand_canyon_5_s39, + 0x7705ef: location_name.sand_canyon_5_s40, + 0x7705f0: location_name.cloudy_park_1_s1, + 0x7705f1: location_name.cloudy_park_1_s2, + 0x7705f2: location_name.cloudy_park_1_s3, + 0x7705f3: location_name.cloudy_park_1_s4, + 0x7705f4: location_name.cloudy_park_1_s5, + 0x7705f5: location_name.cloudy_park_1_s6, + 0x7705f6: location_name.cloudy_park_1_s7, + 0x7705f7: location_name.cloudy_park_1_s8, + 0x7705f8: location_name.cloudy_park_1_s9, + 0x7705f9: location_name.cloudy_park_1_s10, + 0x7705fa: location_name.cloudy_park_1_s11, + 0x7705fb: location_name.cloudy_park_1_s12, + 0x7705fc: location_name.cloudy_park_1_s13, + 0x7705fd: location_name.cloudy_park_1_s14, + 0x7705fe: location_name.cloudy_park_1_s15, + 0x7705ff: location_name.cloudy_park_1_s16, + 0x770600: location_name.cloudy_park_1_s17, + 0x770601: location_name.cloudy_park_1_s18, + 0x770602: location_name.cloudy_park_1_s19, + 0x770603: location_name.cloudy_park_1_s20, + 0x770604: location_name.cloudy_park_1_s21, + 0x770605: location_name.cloudy_park_1_s22, + 0x770606: location_name.cloudy_park_1_s23, + 0x770607: location_name.cloudy_park_2_s1, + 0x770608: location_name.cloudy_park_2_s2, + 0x770609: location_name.cloudy_park_2_s3, + 0x77060a: location_name.cloudy_park_2_s4, + 0x77060b: location_name.cloudy_park_2_s5, + 0x77060c: location_name.cloudy_park_2_s6, + 0x77060d: location_name.cloudy_park_2_s7, + 0x77060e: location_name.cloudy_park_2_s8, + 0x77060f: location_name.cloudy_park_2_s9, + 0x770610: location_name.cloudy_park_2_s10, + 0x770611: location_name.cloudy_park_2_s11, + 0x770612: location_name.cloudy_park_2_s12, + 0x770613: location_name.cloudy_park_2_s13, + 0x770614: location_name.cloudy_park_2_s14, + 0x770615: location_name.cloudy_park_2_s15, + 0x770616: location_name.cloudy_park_2_s16, + 0x770617: location_name.cloudy_park_2_s17, + 0x770618: location_name.cloudy_park_2_s18, + 0x770619: location_name.cloudy_park_2_s19, + 0x77061a: location_name.cloudy_park_2_s20, + 0x77061b: location_name.cloudy_park_2_s21, + 0x77061c: location_name.cloudy_park_2_s22, + 0x77061d: location_name.cloudy_park_2_s23, + 0x77061e: location_name.cloudy_park_2_s24, + 0x77061f: location_name.cloudy_park_2_s25, + 0x770620: location_name.cloudy_park_2_s26, + 0x770621: location_name.cloudy_park_2_s27, + 0x770622: location_name.cloudy_park_2_s28, + 0x770623: location_name.cloudy_park_2_s29, + 0x770624: location_name.cloudy_park_2_s30, + 0x770625: location_name.cloudy_park_2_s31, + 0x770626: location_name.cloudy_park_2_s32, + 0x770627: location_name.cloudy_park_2_s33, + 0x770628: location_name.cloudy_park_2_s34, + 0x770629: location_name.cloudy_park_2_s35, + 0x77062a: location_name.cloudy_park_2_s36, + 0x77062b: location_name.cloudy_park_2_s37, + 0x77062c: location_name.cloudy_park_2_s38, + 0x77062d: location_name.cloudy_park_2_s39, + 0x77062e: location_name.cloudy_park_2_s40, + 0x77062f: location_name.cloudy_park_2_s41, + 0x770630: location_name.cloudy_park_2_s42, + 0x770631: location_name.cloudy_park_2_s43, + 0x770632: location_name.cloudy_park_2_s44, + 0x770633: location_name.cloudy_park_2_s45, + 0x770634: location_name.cloudy_park_2_s46, + 0x770635: location_name.cloudy_park_2_s47, + 0x770636: location_name.cloudy_park_2_s48, + 0x770637: location_name.cloudy_park_2_s49, + 0x770638: location_name.cloudy_park_2_s50, + 0x770639: location_name.cloudy_park_2_s51, + 0x77063a: location_name.cloudy_park_2_s52, + 0x77063b: location_name.cloudy_park_2_s53, + 0x77063c: location_name.cloudy_park_2_s54, + 0x77063d: location_name.cloudy_park_3_s1, + 0x77063e: location_name.cloudy_park_3_s2, + 0x77063f: location_name.cloudy_park_3_s3, + 0x770640: location_name.cloudy_park_3_s4, + 0x770641: location_name.cloudy_park_3_s5, + 0x770642: location_name.cloudy_park_3_s6, + 0x770643: location_name.cloudy_park_3_s7, + 0x770644: location_name.cloudy_park_3_s8, + 0x770645: location_name.cloudy_park_3_s9, + 0x770646: location_name.cloudy_park_3_s10, + 0x770647: location_name.cloudy_park_3_s11, + 0x770648: location_name.cloudy_park_3_s12, + 0x770649: location_name.cloudy_park_3_s13, + 0x77064a: location_name.cloudy_park_3_s14, + 0x77064b: location_name.cloudy_park_3_s15, + 0x77064c: location_name.cloudy_park_3_s16, + 0x77064d: location_name.cloudy_park_3_s17, + 0x77064e: location_name.cloudy_park_3_s18, + 0x77064f: location_name.cloudy_park_3_s19, + 0x770650: location_name.cloudy_park_3_s20, + 0x770651: location_name.cloudy_park_3_s21, + 0x770652: location_name.cloudy_park_3_s22, + 0x770653: location_name.cloudy_park_4_s1, + 0x770654: location_name.cloudy_park_4_s2, + 0x770655: location_name.cloudy_park_4_s3, + 0x770656: location_name.cloudy_park_4_s4, + 0x770657: location_name.cloudy_park_4_s5, + 0x770658: location_name.cloudy_park_4_s6, + 0x770659: location_name.cloudy_park_4_s7, + 0x77065a: location_name.cloudy_park_4_s8, + 0x77065b: location_name.cloudy_park_4_s9, + 0x77065c: location_name.cloudy_park_4_s10, + 0x77065d: location_name.cloudy_park_4_s11, + 0x77065e: location_name.cloudy_park_4_s12, + 0x77065f: location_name.cloudy_park_4_s13, + 0x770660: location_name.cloudy_park_4_s14, + 0x770661: location_name.cloudy_park_4_s15, + 0x770662: location_name.cloudy_park_4_s16, + 0x770663: location_name.cloudy_park_4_s17, + 0x770664: location_name.cloudy_park_4_s18, + 0x770665: location_name.cloudy_park_4_s19, + 0x770666: location_name.cloudy_park_4_s20, + 0x770667: location_name.cloudy_park_4_s21, + 0x770668: location_name.cloudy_park_4_s22, + 0x770669: location_name.cloudy_park_4_s23, + 0x77066a: location_name.cloudy_park_4_s24, + 0x77066b: location_name.cloudy_park_4_s25, + 0x77066c: location_name.cloudy_park_4_s26, + 0x77066d: location_name.cloudy_park_4_s27, + 0x77066e: location_name.cloudy_park_4_s28, + 0x77066f: location_name.cloudy_park_4_s29, + 0x770670: location_name.cloudy_park_4_s30, + 0x770671: location_name.cloudy_park_4_s31, + 0x770672: location_name.cloudy_park_4_s32, + 0x770673: location_name.cloudy_park_4_s33, + 0x770674: location_name.cloudy_park_4_s34, + 0x770675: location_name.cloudy_park_4_s35, + 0x770676: location_name.cloudy_park_4_s36, + 0x770677: location_name.cloudy_park_4_s37, + 0x770678: location_name.cloudy_park_4_s38, + 0x770679: location_name.cloudy_park_4_s39, + 0x77067a: location_name.cloudy_park_4_s40, + 0x77067b: location_name.cloudy_park_4_s41, + 0x77067c: location_name.cloudy_park_4_s42, + 0x77067d: location_name.cloudy_park_4_s43, + 0x77067e: location_name.cloudy_park_4_s44, + 0x77067f: location_name.cloudy_park_4_s45, + 0x770680: location_name.cloudy_park_4_s46, + 0x770681: location_name.cloudy_park_4_s47, + 0x770682: location_name.cloudy_park_4_s48, + 0x770683: location_name.cloudy_park_4_s49, + 0x770684: location_name.cloudy_park_4_s50, + 0x770685: location_name.cloudy_park_5_s1, + 0x770686: location_name.cloudy_park_5_s2, + 0x770687: location_name.cloudy_park_5_s3, + 0x770688: location_name.cloudy_park_5_s4, + 0x770689: location_name.cloudy_park_5_s5, + 0x77068a: location_name.cloudy_park_5_s6, + 0x77068b: location_name.cloudy_park_6_s1, + 0x77068c: location_name.cloudy_park_6_s2, + 0x77068d: location_name.cloudy_park_6_s3, + 0x77068e: location_name.cloudy_park_6_s4, + 0x77068f: location_name.cloudy_park_6_s5, + 0x770690: location_name.cloudy_park_6_s6, + 0x770691: location_name.cloudy_park_6_s7, + 0x770692: location_name.cloudy_park_6_s8, + 0x770693: location_name.cloudy_park_6_s9, + 0x770694: location_name.cloudy_park_6_s10, + 0x770695: location_name.cloudy_park_6_s11, + 0x770696: location_name.cloudy_park_6_s12, + 0x770697: location_name.cloudy_park_6_s13, + 0x770698: location_name.cloudy_park_6_s14, + 0x770699: location_name.cloudy_park_6_s15, + 0x77069a: location_name.cloudy_park_6_s16, + 0x77069b: location_name.cloudy_park_6_s17, + 0x77069c: location_name.cloudy_park_6_s18, + 0x77069d: location_name.cloudy_park_6_s19, + 0x77069e: location_name.cloudy_park_6_s20, + 0x77069f: location_name.cloudy_park_6_s21, + 0x7706a0: location_name.cloudy_park_6_s22, + 0x7706a1: location_name.cloudy_park_6_s23, + 0x7706a2: location_name.cloudy_park_6_s24, + 0x7706a3: location_name.cloudy_park_6_s25, + 0x7706a4: location_name.cloudy_park_6_s26, + 0x7706a5: location_name.cloudy_park_6_s27, + 0x7706a6: location_name.cloudy_park_6_s28, + 0x7706a7: location_name.cloudy_park_6_s29, + 0x7706a8: location_name.cloudy_park_6_s30, + 0x7706a9: location_name.cloudy_park_6_s31, + 0x7706aa: location_name.cloudy_park_6_s32, + 0x7706ab: location_name.cloudy_park_6_s33, + 0x7706ac: location_name.iceberg_1_s1, + 0x7706ad: location_name.iceberg_1_s2, + 0x7706ae: location_name.iceberg_1_s3, + 0x7706af: location_name.iceberg_1_s4, + 0x7706b0: location_name.iceberg_1_s5, + 0x7706b1: location_name.iceberg_1_s6, + 0x7706b2: location_name.iceberg_2_s1, + 0x7706b3: location_name.iceberg_2_s2, + 0x7706b4: location_name.iceberg_2_s3, + 0x7706b5: location_name.iceberg_2_s4, + 0x7706b6: location_name.iceberg_2_s5, + 0x7706b7: location_name.iceberg_2_s6, + 0x7706b8: location_name.iceberg_2_s7, + 0x7706b9: location_name.iceberg_2_s8, + 0x7706ba: location_name.iceberg_2_s9, + 0x7706bb: location_name.iceberg_2_s10, + 0x7706bc: location_name.iceberg_2_s11, + 0x7706bd: location_name.iceberg_2_s12, + 0x7706be: location_name.iceberg_2_s13, + 0x7706bf: location_name.iceberg_2_s14, + 0x7706c0: location_name.iceberg_2_s15, + 0x7706c1: location_name.iceberg_2_s16, + 0x7706c2: location_name.iceberg_2_s17, + 0x7706c3: location_name.iceberg_2_s18, + 0x7706c4: location_name.iceberg_2_s19, + 0x7706c5: location_name.iceberg_3_s1, + 0x7706c6: location_name.iceberg_3_s2, + 0x7706c7: location_name.iceberg_3_s3, + 0x7706c8: location_name.iceberg_3_s4, + 0x7706c9: location_name.iceberg_3_s5, + 0x7706ca: location_name.iceberg_3_s6, + 0x7706cb: location_name.iceberg_3_s7, + 0x7706cc: location_name.iceberg_3_s8, + 0x7706cd: location_name.iceberg_3_s9, + 0x7706ce: location_name.iceberg_3_s10, + 0x7706cf: location_name.iceberg_3_s11, + 0x7706d0: location_name.iceberg_3_s12, + 0x7706d1: location_name.iceberg_3_s13, + 0x7706d2: location_name.iceberg_3_s14, + 0x7706d3: location_name.iceberg_3_s15, + 0x7706d4: location_name.iceberg_3_s16, + 0x7706d5: location_name.iceberg_3_s17, + 0x7706d6: location_name.iceberg_3_s18, + 0x7706d7: location_name.iceberg_3_s19, + 0x7706d8: location_name.iceberg_3_s20, + 0x7706d9: location_name.iceberg_3_s21, + 0x7706da: location_name.iceberg_4_s1, + 0x7706db: location_name.iceberg_4_s2, + 0x7706dc: location_name.iceberg_4_s3, + 0x7706dd: location_name.iceberg_5_s1, + 0x7706de: location_name.iceberg_5_s2, + 0x7706df: location_name.iceberg_5_s3, + 0x7706e0: location_name.iceberg_5_s4, + 0x7706e1: location_name.iceberg_5_s5, + 0x7706e2: location_name.iceberg_5_s6, + 0x7706e3: location_name.iceberg_5_s7, + 0x7706e4: location_name.iceberg_5_s8, + 0x7706e5: location_name.iceberg_5_s9, + 0x7706e6: location_name.iceberg_5_s10, + 0x7706e7: location_name.iceberg_5_s11, + 0x7706e8: location_name.iceberg_5_s12, + 0x7706e9: location_name.iceberg_5_s13, + 0x7706ea: location_name.iceberg_5_s14, + 0x7706eb: location_name.iceberg_5_s15, + 0x7706ec: location_name.iceberg_5_s16, + 0x7706ed: location_name.iceberg_5_s17, + 0x7706ee: location_name.iceberg_5_s18, + 0x7706ef: location_name.iceberg_5_s19, + 0x7706f0: location_name.iceberg_5_s20, + 0x7706f1: location_name.iceberg_5_s21, + 0x7706f2: location_name.iceberg_5_s22, + 0x7706f3: location_name.iceberg_5_s23, + 0x7706f4: location_name.iceberg_5_s24, + 0x7706f5: location_name.iceberg_5_s25, + 0x7706f6: location_name.iceberg_5_s26, + 0x7706f7: location_name.iceberg_5_s27, + 0x7706f8: location_name.iceberg_5_s28, + 0x7706f9: location_name.iceberg_5_s29, + 0x7706fa: location_name.iceberg_5_s30, + 0x7706fb: location_name.iceberg_5_s31, + 0x7706fc: location_name.iceberg_5_s32, + 0x7706fd: location_name.iceberg_5_s33, + 0x7706fe: location_name.iceberg_5_s34, + 0x7706ff: location_name.iceberg_6_s1, + +} + +location_table = { + **stage_locations, + **heart_star_locations, + **boss_locations, + **consumable_locations, + **star_locations +} diff --git a/worlds/kdl3/Names/__init__.py b/worlds/kdl3/names/__init__.py similarity index 100% rename from worlds/kdl3/Names/__init__.py rename to worlds/kdl3/names/__init__.py diff --git a/worlds/kdl3/Names/AnimalFriendSpawns.py b/worlds/kdl3/names/animal_friend_spawns.py similarity index 95% rename from worlds/kdl3/Names/AnimalFriendSpawns.py rename to worlds/kdl3/names/animal_friend_spawns.py index 4520cf1438..5c1ba39697 100644 --- a/worlds/kdl3/Names/AnimalFriendSpawns.py +++ b/worlds/kdl3/names/animal_friend_spawns.py @@ -1,3 +1,5 @@ +from typing import List + grass_land_1_a1 = "Grass Land 1 - Animal 1" # Nago grass_land_1_a2 = "Grass Land 1 - Animal 2" # Rick grass_land_2_a1 = "Grass Land 2 - Animal 1" # ChuChu @@ -197,3 +199,12 @@ animal_friend_spawns = { iceberg_6_a5: "ChuChu Spawn", iceberg_6_a6: "Nago Spawn", } + +problematic_sets: List[List[str]] = [ + # Animal groups that must be guaranteed unique. Potential for softlocks on future-ER if not. + [ripple_field_4_a1, ripple_field_4_a2, ripple_field_4_a3], + [sand_canyon_3_a1, sand_canyon_3_a2, sand_canyon_3_a3], + [cloudy_park_6_a1, cloudy_park_6_a2, cloudy_park_6_a3], + [iceberg_6_a1, iceberg_6_a2, iceberg_6_a3], + [iceberg_6_a4, iceberg_6_a5, iceberg_6_a6] +] diff --git a/worlds/kdl3/Names/EnemyAbilities.py b/worlds/kdl3/names/enemy_abilities.py similarity index 99% rename from worlds/kdl3/Names/EnemyAbilities.py rename to worlds/kdl3/names/enemy_abilities.py index 016e3033ab..ace15054da 100644 --- a/worlds/kdl3/Names/EnemyAbilities.py +++ b/worlds/kdl3/names/enemy_abilities.py @@ -809,7 +809,7 @@ vanilla_enemies = {'Waddle Dee': 'No Ability', enemy_restrictive: List[Tuple[List[str], List[str]]] = [ # abilities, enemies, set_all (False to set any) - (["Burning Ability", "Stone Ability"], ["Rocky", "Sparky", "Babut", "Squishy", ]), # Ribbon Field 5 - 7 + (["Stone Ability"], ["Rocky", "Sparky", "Babut", "Squishy", ]), # Ribbon Field 5 - 7 # Sand Canyon 6 (["Parasol Ability", "Cutter Ability"], ['Bukiset (Parasol)', 'Bukiset (Cutter)']), (["Spark Ability", "Clean Ability"], ['Bukiset (Spark)', 'Bukiset (Clean)']), diff --git a/worlds/kdl3/Names/LocationName.py b/worlds/kdl3/names/location_name.py similarity index 100% rename from worlds/kdl3/Names/LocationName.py rename to worlds/kdl3/names/location_name.py diff --git a/worlds/kdl3/Options.py b/worlds/kdl3/options.py similarity index 82% rename from worlds/kdl3/Options.py rename to worlds/kdl3/options.py index e0a4f12f15..b9163794ad 100644 --- a/worlds/kdl3/Options.py +++ b/worlds/kdl3/options.py @@ -1,13 +1,21 @@ import random from dataclasses import dataclass +from typing import List -from Options import DeathLink, Choice, Toggle, OptionDict, Range, PlandoBosses, DefaultOnToggle, \ - PerGameCommonOptions, PlandoConnections -from .Names import LocationName +from Options import DeathLinkMixin, Choice, Toggle, OptionDict, Range, PlandoBosses, DefaultOnToggle, \ + PerGameCommonOptions, Visibility, NamedRange, OptionGroup, PlandoConnections +from .names import location_name + + +class RemoteItems(DefaultOnToggle): + """ + Enables receiving items from your own world, primarily for co-op play. + """ + display_name = "Remote Items" class KDL3PlandoConnections(PlandoConnections): - entrances = exits = {f"{i} {j}" for i in LocationName.level_names for j in range(1, 7)} + entrances = exits = {f"{i} {j}" for i in location_name.level_names for j in range(1, 7)} class Goal(Choice): @@ -30,6 +38,7 @@ class Goal(Choice): return cls.name_lookup[value].upper() return super().get_option_name(value) + class GoalSpeed(Choice): """ Normal: the goal is unlocked after purifying the five bosses @@ -40,13 +49,14 @@ class GoalSpeed(Choice): option_fast = 1 -class TotalHeartStars(Range): +class MaxHeartStars(Range): """ Maximum number of heart stars to include in the pool of items. + If fewer available locations exist in the pool than this number, the number of available locations will be used instead. """ display_name = "Max Heart Stars" range_start = 5 # set to 5 so strict bosses does not degrade - range_end = 50 # 30 default locations + 30 stage clears + 5 bosses - 14 progression items = 51, so round down + range_end = 99 # previously set to 50, set to highest it can be should there be less locations than heart stars default = 30 @@ -84,9 +94,9 @@ class BossShuffle(PlandoBosses): Singularity: All (non-Zero) bosses will be replaced with a single boss Supports plando placement. """ - bosses = frozenset(LocationName.boss_names.keys()) + bosses = frozenset(location_name.boss_names.keys()) - locations = frozenset(LocationName.level_names.keys()) + locations = frozenset(location_name.level_names.keys()) duplicate_bosses = True @@ -278,7 +288,8 @@ class KirbyFlavorPreset(Choice): option_orange = 11 option_lime = 12 option_lavender = 13 - option_custom = 14 + option_miku = 14 + option_custom = 15 default = 0 @classmethod @@ -296,6 +307,7 @@ class KirbyFlavor(OptionDict): A custom color for Kirby. To use a custom color, set the preset to Custom and then define a dict of keys from "1" to "15", with their values being an HTML hex color. """ + display_name = "Custom Kirby Flavor" default = { "1": "B01810", "2": "F0E0E8", @@ -313,6 +325,7 @@ class KirbyFlavor(OptionDict): "14": "F8F8F8", "15": "B03830", } + visibility = Visibility.template | Visibility.spoiler # likely never supported on guis class GooeyFlavorPreset(Choice): @@ -352,6 +365,7 @@ class GooeyFlavor(OptionDict): A custom color for Gooey. To use a custom color, set the preset to Custom and then define a dict of keys from "1" to "15", with their values being an HTML hex color. """ + display_name = "Custom Gooey Flavor" default = { "1": "000808", "2": "102838", @@ -363,6 +377,7 @@ class GooeyFlavor(OptionDict): "8": "D0C0C0", "9": "F8F8F8", } + visibility = Visibility.template | Visibility.spoiler # likely never supported on guis class MusicShuffle(Choice): @@ -402,14 +417,27 @@ class Gifting(Toggle): display_name = "Gifting" +class TotalHeartStars(NamedRange): + """ + Deprecated. Use max_heart_stars instead. Supported for only one version. + """ + default = -1 + range_start = 5 + range_end = 99 + special_range_names = { + "default": -1 + } + visibility = Visibility.none + + @dataclass -class KDL3Options(PerGameCommonOptions): +class KDL3Options(PerGameCommonOptions, DeathLinkMixin): + remote_items: RemoteItems plando_connections: KDL3PlandoConnections - death_link: DeathLink game_language: GameLanguage goal: Goal goal_speed: GoalSpeed - total_heart_stars: TotalHeartStars + max_heart_stars: MaxHeartStars heart_stars_required: HeartStarsRequired filler_percentage: FillerPercentage trap_percentage: TrapPercentage @@ -435,3 +463,17 @@ class KDL3Options(PerGameCommonOptions): gooey_flavor: GooeyFlavor music_shuffle: MusicShuffle virtual_console: VirtualConsoleChanges + + total_heart_stars: TotalHeartStars # remove in 2 versions + + +kdl3_option_groups: List[OptionGroup] = [ + OptionGroup("Goal Options", [Goal, GoalSpeed, MaxHeartStars, HeartStarsRequired, JumpingTarget, ]), + OptionGroup("World Options", [RemoteItems, StrictBosses, OpenWorld, OpenWorldBossRequirement, ConsumableChecks, + StarChecks, FillerPercentage, TrapPercentage, GooeyTrapPercentage, + SlowTrapPercentage, AbilityTrapPercentage, LevelShuffle, BossShuffle, + AnimalRandomization, CopyAbilityRandomization, BossRequirementRandom, + Gifting, ]), + OptionGroup("Cosmetic Options", [GameLanguage, BossShuffleAllowBB, KirbyFlavorPreset, KirbyFlavor, + GooeyFlavorPreset, GooeyFlavor, MusicShuffle, VirtualConsoleChanges, ]), +] diff --git a/worlds/kdl3/Presets.py b/worlds/kdl3/presets.py similarity index 98% rename from worlds/kdl3/Presets.py rename to worlds/kdl3/presets.py index d3a7146ded..491ad9dca9 100644 --- a/worlds/kdl3/Presets.py +++ b/worlds/kdl3/presets.py @@ -25,6 +25,7 @@ all_random = { "ow_boss_requirement": "random", "boss_requirement_random": "random", "consumables": "random", + "starsanity": "random", "kirby_flavor_preset": "random", "gooey_flavor_preset": "random", "music_shuffle": "random", diff --git a/worlds/kdl3/Regions.py b/worlds/kdl3/regions.py similarity index 66% rename from worlds/kdl3/Regions.py rename to worlds/kdl3/regions.py index 407dcf9680..c47e5dee40 100644 --- a/worlds/kdl3/Regions.py +++ b/worlds/kdl3/regions.py @@ -1,60 +1,62 @@ import orjson import os from pkgutil import get_data +from copy import deepcopy -from typing import TYPE_CHECKING, List, Dict, Optional, Union -from BaseClasses import Region +from typing import TYPE_CHECKING, List, Dict, Optional, Union, Callable +from BaseClasses import Region, CollectionState from worlds.generic.Rules import add_item_rule -from .Locations import KDL3Location -from .Names import LocationName -from .Options import BossShuffle -from .Room import KDL3Room +from .locations import KDL3Location +from .names import location_name +from .options import BossShuffle +from .room import KDL3Room if TYPE_CHECKING: from . import KDL3World default_levels = { - 1: [0x770001, 0x770002, 0x770003, 0x770004, 0x770005, 0x770006, 0x770200], - 2: [0x770007, 0x770008, 0x770009, 0x77000A, 0x77000B, 0x77000C, 0x770201], - 3: [0x77000D, 0x77000E, 0x77000F, 0x770010, 0x770011, 0x770012, 0x770202], - 4: [0x770013, 0x770014, 0x770015, 0x770016, 0x770017, 0x770018, 0x770203], - 5: [0x770019, 0x77001A, 0x77001B, 0x77001C, 0x77001D, 0x77001E, 0x770204], + 1: [0x770000, 0x770001, 0x770002, 0x770003, 0x770004, 0x770005, 0x770200], + 2: [0x770006, 0x770007, 0x770008, 0x770009, 0x77000A, 0x77000B, 0x770201], + 3: [0x77000C, 0x77000D, 0x77000E, 0x77000F, 0x770010, 0x770011, 0x770202], + 4: [0x770012, 0x770013, 0x770014, 0x770015, 0x770016, 0x770017, 0x770203], + 5: [0x770018, 0x770019, 0x77001A, 0x77001B, 0x77001C, 0x77001D, 0x770204], } first_stage_blacklist = { # We want to confirm that the first stage can be completed without any items - 0x77000B, # 2-5 needs Kine - 0x770011, # 3-5 needs Cutter - 0x77001C, # 5-4 needs Burning + 0x77000A, # 2-5 needs Kine + 0x770010, # 3-5 needs Cutter + 0x77001B, # 5-4 needs Burning } first_world_limit = { # We need to limit the number of very restrictive stages in level 1 on solo gens *first_stage_blacklist, # all three of the blacklist stages need 2+ items for both checks + 0x770006, 0x770007, - 0x770008, - 0x770013, - 0x77001E, + 0x770012, + 0x77001D, } def generate_valid_level(world: "KDL3World", level: int, stage: int, - possible_stages: List[int], placed_stages: List[int]): + possible_stages: List[int], placed_stages: List[Optional[int]]) -> int: new_stage = world.random.choice(possible_stages) if level == 1: if stage == 0 and new_stage in first_stage_blacklist: + possible_stages.remove(new_stage) return generate_valid_level(world, level, stage, possible_stages, placed_stages) elif (not (world.multiworld.players > 1 or world.options.consumables or world.options.starsanity) and - new_stage in first_world_limit and - sum(p_stage in first_world_limit for p_stage in placed_stages) + new_stage in first_world_limit and + sum(p_stage in first_world_limit for p_stage in placed_stages) >= (2 if world.options.open_world else 1)): return generate_valid_level(world, level, stage, possible_stages, placed_stages) return new_stage -def generate_rooms(world: "KDL3World", level_regions: Dict[int, Region]): - level_names = {LocationName.level_names[level]: level for level in LocationName.level_names} +def generate_rooms(world: "KDL3World", level_regions: Dict[int, Region]) -> None: + level_names = {location_name.level_names[level]: level for level in location_name.level_names} room_data = orjson.loads(get_data(__name__, os.path.join("data", "Rooms.json"))) rooms: Dict[str, KDL3Room] = dict() for room_entry in room_data: @@ -63,7 +65,7 @@ def generate_rooms(world: "KDL3World", level_regions: Dict[int, Region]): room_entry["default_exits"], room_entry["animal_pointers"], room_entry["enemies"], room_entry["entity_load"], room_entry["consumables"], room_entry["consumables_pointer"]) room.add_locations({location: world.location_name_to_id[location] if location in world.location_name_to_id else - None for location in room_entry["locations"] + None for location in room_entry["locations"] if (not any(x in location for x in ["1-Up", "Maxim"]) or world.options.consumables.value) and ("Star" not in location or world.options.starsanity.value)}, @@ -83,8 +85,8 @@ def generate_rooms(world: "KDL3World", level_regions: Dict[int, Region]): if room.stage == 7: first_rooms[0x770200 + room.level - 1] = room else: - first_rooms[0x770000 + ((room.level - 1) * 6) + room.stage] = room - exits = dict() + first_rooms[0x770000 + ((room.level - 1) * 6) + room.stage - 1] = room + exits: Dict[str, Callable[[CollectionState], bool]] = dict() for def_exit in room.default_exits: target = f"{level_names[room.level]} {room.stage} - {def_exit['room']}" access_rule = tuple(def_exit["access_rule"]) @@ -115,50 +117,54 @@ def generate_rooms(world: "KDL3World", level_regions: Dict[int, Region]): if world.options.open_world: level_regions[level].add_exits([first_rooms[0x770200 + level - 1].name]) else: - world.multiworld.get_location(world.location_id_to_name[world.player_levels[level][5]], world.player)\ + world.multiworld.get_location(world.location_id_to_name[world.player_levels[level][5]], world.player) \ .parent_region.add_exits([first_rooms[0x770200 + level - 1].name]) -def generate_valid_levels(world: "KDL3World", enforce_world: bool, enforce_pattern: bool) -> dict: - levels: Dict[int, List[Optional[int]]] = { - 1: [None] * 7, - 2: [None] * 7, - 3: [None] * 7, - 4: [None] * 7, - 5: [None] * 7, - } +def generate_valid_levels(world: "KDL3World", shuffle_mode: int) -> Dict[int, List[int]]: + if shuffle_mode: + levels: Dict[int, List[Optional[int]]] = { + 1: [None] * 7, + 2: [None] * 7, + 3: [None] * 7, + 4: [None] * 7, + 5: [None] * 7, + } - possible_stages = [default_levels[level][stage] for level in default_levels for stage in range(6)] - if world.options.plando_connections: - for connection in world.options.plando_connections: - try: - entrance_world, entrance_stage = connection.entrance.rsplit(" ", 1) - stage_world, stage_stage = connection.exit.rsplit(" ", 1) - new_stage = default_levels[LocationName.level_names[stage_world.strip()]][int(stage_stage) - 1] - levels[LocationName.level_names[entrance_world.strip()]][int(entrance_stage) - 1] = new_stage - possible_stages.remove(new_stage) + possible_stages = [default_levels[level][stage] for level in default_levels for stage in range(6)] + if world.options.plando_connections: + for connection in world.options.plando_connections: + try: + entrance_world, entrance_stage = connection.entrance.rsplit(" ", 1) + stage_world, stage_stage = connection.exit.rsplit(" ", 1) + new_stage = default_levels[location_name.level_names[stage_world.strip()]][int(stage_stage) - 1] + levels[location_name.level_names[entrance_world.strip()]][int(entrance_stage) - 1] = new_stage + possible_stages.remove(new_stage) - except Exception: - raise Exception( - f"Invalid connection: {connection.entrance} =>" - f" {connection.exit} for player {world.player} ({world.player_name})") + except Exception: + raise Exception( + f"Invalid connection: {connection.entrance} =>" + f" {connection.exit} for player {world.player} ({world.player_name})") - for level in range(1, 6): - for stage in range(6): - # Randomize bosses separately - try: + for level in range(1, 6): + for stage in range(6): + # Randomize bosses separately if levels[level][stage] is None: stage_candidates = [candidate for candidate in possible_stages - if (enforce_world and candidate in default_levels[level]) - or (enforce_pattern and ((candidate - 1) & 0x00FFFF) % 6 == stage) - or (enforce_pattern == enforce_world) + if (shuffle_mode == 1 and candidate in default_levels[level]) + or (shuffle_mode == 2 and (candidate & 0x00FFFF) % 6 == stage) + or (shuffle_mode == 3) ] + if not stage_candidates: + raise Exception( + f"Failed to find valid stage for {level}-{stage}. Remaining Stages:{possible_stages}") new_stage = generate_valid_level(world, level, stage, stage_candidates, levels[level]) possible_stages.remove(new_stage) levels[level][stage] = new_stage - except Exception: - raise Exception(f"Failed to find valid stage for {level}-{stage}. Remaining Stages:{possible_stages}") - + else: + levels = deepcopy(default_levels) + for level in levels: + levels[level][6] = None # now handle bosses boss_shuffle: Union[int, str] = world.options.boss_shuffle.value plando_bosses = [] @@ -168,17 +174,17 @@ def generate_valid_levels(world: "KDL3World", enforce_world: bool, enforce_patte boss_shuffle = BossShuffle.options[options.pop()] for option in options: if "-" in option: - loc, boss = option.split("-") + loc, plando_boss = option.split("-") loc = loc.title() - boss = boss.title() - levels[LocationName.level_names[loc]][6] = LocationName.boss_names[boss] - plando_bosses.append(LocationName.boss_names[boss]) + plando_boss = plando_boss.title() + levels[location_name.level_names[loc]][6] = location_name.boss_names[plando_boss] + plando_bosses.append(location_name.boss_names[plando_boss]) else: option = option.title() for level in levels: if levels[level][6] is None: - levels[level][6] = LocationName.boss_names[option] - plando_bosses.append(LocationName.boss_names[option]) + levels[level][6] = location_name.boss_names[option] + plando_bosses.append(location_name.boss_names[option]) if boss_shuffle > 0: if boss_shuffle == BossShuffle.option_full: @@ -223,15 +229,14 @@ def create_levels(world: "KDL3World") -> None: 5: level5, } level_shuffle = world.options.stage_shuffle.value - if level_shuffle != 0: - world.player_levels = generate_valid_levels( - world, - level_shuffle == 1, - level_shuffle == 2) + if hasattr(world.multiworld, "re_gen_passthrough"): + world.player_levels = getattr(world.multiworld, "re_gen_passthrough")["Kirby's Dream Land 3"]["player_levels"] + else: + world.player_levels = generate_valid_levels(world, level_shuffle) generate_rooms(world, levels) - level6.add_locations({LocationName.goals[world.options.goal]: None}, KDL3Location) + level6.add_locations({location_name.goals[world.options.goal.value]: None}, KDL3Location) menu.connect(level1, "Start Game") level1.connect(level2, "To Level 2") diff --git a/worlds/kdl3/rom.py b/worlds/kdl3/rom.py new file mode 100644 index 0000000000..3dd10ce1c4 --- /dev/null +++ b/worlds/kdl3/rom.py @@ -0,0 +1,602 @@ +import typing +from pkgutil import get_data + +import Utils +from typing import Optional, TYPE_CHECKING, Tuple, Dict, List +import hashlib +import os +import struct + +import settings +from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes, APPatchExtension +from .aesthetics import get_palette_bytes, kirby_target_palettes, get_kirby_palette, gooey_target_palettes, \ + get_gooey_palette +from .compression import hal_decompress +import bsdiff4 + +if TYPE_CHECKING: + from . import KDL3World + +KDL3UHASH = "201e7658f6194458a3869dde36bf8ec2" +KDL3JHASH = "b2f2d004ea640c3db66df958fce122b2" + +level_pointers = { + 0x770000: 0x0084, + 0x770001: 0x009C, + 0x770002: 0x00B8, + 0x770003: 0x00D8, + 0x770004: 0x0104, + 0x770005: 0x0124, + 0x770006: 0x014C, + 0x770007: 0x0170, + 0x770008: 0x0190, + 0x770009: 0x01B0, + 0x77000A: 0x01E8, + 0x77000B: 0x0218, + 0x77000C: 0x024C, + 0x77000D: 0x0270, + 0x77000E: 0x02A0, + 0x77000F: 0x02C4, + 0x770010: 0x02EC, + 0x770011: 0x0314, + 0x770012: 0x03CC, + 0x770013: 0x0404, + 0x770014: 0x042C, + 0x770015: 0x044C, + 0x770016: 0x0478, + 0x770017: 0x049C, + 0x770018: 0x04E4, + 0x770019: 0x0504, + 0x77001A: 0x0530, + 0x77001B: 0x0554, + 0x77001C: 0x05A8, + 0x77001D: 0x0640, + 0x770200: 0x0148, + 0x770201: 0x0248, + 0x770202: 0x03C8, + 0x770203: 0x04E0, + 0x770204: 0x06A4, + 0x770205: 0x06A8, +} + +bb_bosses = { + 0x770200: 0xED85F1, + 0x770201: 0xF01360, + 0x770202: 0xEDA3DF, + 0x770203: 0xEDC2B9, + 0x770204: 0xED7C3F, + 0x770205: 0xEC29D2, +} + +level_sprites = { + 0x19B2C6: 1827, + 0x1A195C: 1584, + 0x19F6F3: 1679, + 0x19DC8B: 1717, + 0x197900: 1872 +} + +stage_tiles = { + 0: [ + 0, 1, 2, + 16, 17, 18, + 32, 33, 34, + 48, 49, 50 + ], + 1: [ + 3, 4, 5, + 19, 20, 21, + 35, 36, 37, + 51, 52, 53 + ], + 2: [ + 6, 7, 8, + 22, 23, 24, + 38, 39, 40, + 54, 55, 56 + ], + 3: [ + 9, 10, 11, + 25, 26, 27, + 41, 42, 43, + 57, 58, 59, + ], + 4: [ + 12, 13, 64, + 28, 29, 65, + 44, 45, 66, + 60, 61, 67 + ], + 5: [ + 14, 15, 68, + 30, 31, 69, + 46, 47, 70, + 62, 63, 71 + ] +} + +heart_star_address = 0x2D0000 +heart_star_size = 456 +consumable_address = 0x2F91DD +consumable_size = 698 + +stage_palettes = [0x60964, 0x60B64, 0x60D64, 0x60F64, 0x61164] + +music_choices = [ + 2, # Boss 1 + 3, # Boss 2 (Unused) + 4, # Boss 3 (Miniboss) + 7, # Dedede + 9, # Event 2 (used once) + 10, # Field 1 + 11, # Field 2 + 12, # Field 3 + 13, # Field 4 + 14, # Field 5 + 15, # Field 6 + 16, # Field 7 + 17, # Field 8 + 18, # Field 9 + 19, # Field 10 + 20, # Field 11 + 21, # Field 12 (Gourmet Race) + 23, # Dark Matter in the Hyper Zone + 24, # Zero + 25, # Level 1 + 26, # Level 2 + 27, # Level 4 + 28, # Level 3 + 29, # Heart Star Failed + 30, # Level 5 + 31, # Minigame + 38, # Animal Friend 1 + 39, # Animal Friend 2 + 40, # Animal Friend 3 +] +# extra room pointers we don't want to track other than for music +room_music = { + 3079990: 23, # Zero + 2983409: 2, # BB Whispy + 3150688: 2, # BB Acro + 2991071: 2, # BB PonCon + 2998969: 2, # BB Ado + 2980927: 7, # BB Dedede + 2894290: 23 # BB Zero +} + +enemy_remap = { + "Waddle Dee": 0, + "Bronto Burt": 2, + "Rocky": 3, + "Bobo": 5, + "Chilly": 6, + "Poppy Bros Jr.": 7, + "Sparky": 8, + "Polof": 9, + "Broom Hatter": 11, + "Cappy": 12, + "Bouncy": 13, + "Nruff": 15, + "Glunk": 16, + "Togezo": 18, + "Kabu": 19, + "Mony": 20, + "Blipper": 21, + "Squishy": 22, + "Gabon": 24, + "Oro": 25, + "Galbo": 26, + "Sir Kibble": 27, + "Nidoo": 28, + "Kany": 29, + "Sasuke": 30, + "Yaban": 32, + "Boten": 33, + "Coconut": 34, + "Doka": 35, + "Icicle": 36, + "Pteran": 39, + "Loud": 40, + "Como": 41, + "Klinko": 42, + "Babut": 43, + "Wappa": 44, + "Mariel": 45, + "Tick": 48, + "Apolo": 49, + "Popon Ball": 50, + "KeKe": 51, + "Magoo": 53, + "Raft Waddle Dee": 57, + "Madoo": 58, + "Corori": 60, + "Kapar": 67, + "Batamon": 68, + "Peran": 72, + "Bobin": 73, + "Mopoo": 74, + "Gansan": 75, + "Bukiset (Burning)": 76, + "Bukiset (Stone)": 77, + "Bukiset (Ice)": 78, + "Bukiset (Needle)": 79, + "Bukiset (Clean)": 80, + "Bukiset (Parasol)": 81, + "Bukiset (Spark)": 82, + "Bukiset (Cutter)": 83, + "Waddle Dee Drawing": 84, + "Bronto Burt Drawing": 85, + "Bouncy Drawing": 86, + "Kabu (Dekabu)": 87, + "Wapod": 88, + "Propeller": 89, + "Dogon": 90, + "Joe": 91 +} + +miniboss_remap = { + "Captain Stitch": 0, + "Yuki": 1, + "Blocky": 2, + "Jumper Shoot": 3, + "Boboo": 4, + "Haboki": 5 +} + +ability_remap = { + "No Ability": 0, + "Burning Ability": 1, + "Stone Ability": 2, + "Ice Ability": 3, + "Needle Ability": 4, + "Clean Ability": 5, + "Parasol Ability": 6, + "Spark Ability": 7, + "Cutter Ability": 8, +} + + +class RomData: + def __init__(self, file: bytes, name: typing.Optional[str] = None): + self.file = bytearray(file) + self.name = name + + def read_byte(self, offset: int) -> int: + return self.file[offset] + + def read_bytes(self, offset: int, length: int) -> bytearray: + return self.file[offset:offset + length] + + def write_byte(self, offset: int, value: int) -> None: + self.file[offset] = value + + def write_bytes(self, offset: int, values: typing.Sequence[int]) -> None: + self.file[offset:offset + len(values)] = values + + def get_bytes(self) -> bytes: + return bytes(self.file) + + +def handle_level_sprites(stages: List[Tuple[int, ...]], sprites: List[bytearray], palettes: List[List[bytearray]]) \ + -> Tuple[List[bytearray], List[bytearray]]: + palette_by_level = list() + for palette in palettes: + palette_by_level.extend(palette[10:16]) + out_palettes = list() + for i in range(5): + for j in range(6): + palettes[i][10 + j] = palette_by_level[stages[i][j]] + out_palettes.append(bytearray([x for palette in palettes[i] for x in palette])) + tiles_by_level = list() + for spritesheet in sprites: + decompressed = hal_decompress(spritesheet) + tiles = [decompressed[i:i + 32] for i in range(0, 2304, 32)] + tiles_by_level.extend([[tiles[x] for x in stage_tiles[stage]] for stage in stage_tiles]) + out_sprites = list() + for world in range(5): + levels = [stages[world][x] for x in range(6)] + world_tiles: typing.List[bytes] = [bytes() for _ in range(72)] + for i in range(6): + for x in range(12): + world_tiles[stage_tiles[i][x]] = tiles_by_level[levels[i]][x] + out_sprites.append(bytearray()) + for tile in world_tiles: + out_sprites[world].extend(tile) + # insert our fake compression + out_sprites[world][0:0] = [0xe3, 0xff] + out_sprites[world][1026:1026] = [0xe3, 0xff] + out_sprites[world][2052:2052] = [0xe0, 0xff] + out_sprites[world].append(0xff) + return out_sprites, out_palettes + + +def write_heart_star_sprites(rom: RomData) -> None: + compressed = rom.read_bytes(heart_star_address, heart_star_size) + decompressed = hal_decompress(compressed) + patch = get_data(__name__, os.path.join("data", "APHeartStar.bsdiff4")) + patched = bytearray(bsdiff4.patch(decompressed, patch)) + rom.write_bytes(0x1AF7DF, patched) + patched[0:0] = [0xE3, 0xFF] + patched.append(0xFF) + rom.write_bytes(0x1CD000, patched) + rom.write_bytes(0x3F0EBF, [0x00, 0xD0, 0x39]) + + +def write_consumable_sprites(rom: RomData, consumables: bool, stars: bool) -> None: + compressed = rom.read_bytes(consumable_address, consumable_size) + decompressed = hal_decompress(compressed) + patched = bytearray(decompressed) + if consumables: + patch = get_data(__name__, os.path.join("data", "APConsumable.bsdiff4")) + patched = bytearray(bsdiff4.patch(bytes(patched), patch)) + if stars: + patch = get_data(__name__, os.path.join("data", "APStars.bsdiff4")) + patched = bytearray(bsdiff4.patch(bytes(patched), patch)) + patched[0:0] = [0xE3, 0xFF] + patched.append(0xFF) + rom.write_bytes(0x1CD500, patched) + rom.write_bytes(0x3F0DAE, [0x00, 0xD5, 0x39]) + + +class KDL3PatchExtensions(APPatchExtension): + game = "Kirby's Dream Land 3" + + @staticmethod + def apply_post_patch(_: APProcedurePatch, rom: bytes) -> bytes: + rom_data = RomData(rom) + write_heart_star_sprites(rom_data) + if rom_data.read_bytes(0x3D014, 1)[0] > 0: + stages = [struct.unpack("HHHHHHH", rom_data.read_bytes(0x3D020 + x * 14, 14)) for x in range(5)] + palettes = [rom_data.read_bytes(full_pal, 512) for full_pal in stage_palettes] + read_palettes = [[palette[i:i + 32] for i in range(0, 512, 32)] for palette in palettes] + sprites = [rom_data.read_bytes(offset, level_sprites[offset]) for offset in level_sprites] + sprites, palettes = handle_level_sprites(stages, sprites, read_palettes) + for addr, palette in zip(stage_palettes, palettes): + rom_data.write_bytes(addr, palette) + for addr, level_sprite in zip([0x1CA000, 0x1CA920, 0x1CB230, 0x1CBB40, 0x1CC450], sprites): + rom_data.write_bytes(addr, level_sprite) + rom_data.write_bytes(0x460A, [0x00, 0xA0, 0x39, 0x20, 0xA9, 0x39, 0x30, 0xB2, 0x39, 0x40, 0xBB, 0x39, + 0x50, 0xC4, 0x39]) + write_consumable_sprites(rom_data, rom_data.read_byte(0x3D018) > 0, rom_data.read_byte(0x3D01A) > 0) + return rom_data.get_bytes() + + +class KDL3ProcedurePatch(APProcedurePatch, APTokenMixin): + hash = [KDL3UHASH, KDL3JHASH] + game = "Kirby's Dream Land 3" + patch_file_ending = ".apkdl3" + procedure = [ + ("apply_bsdiff4", ["kdl3_basepatch.bsdiff4"]), + ("apply_tokens", ["token_patch.bin"]), + ("apply_post_patch", []), + ("calc_snes_crc", []) + ] + name: bytes # used to pass to __init__ + + @classmethod + def get_source_data(cls) -> bytes: + return get_base_rom_bytes() + + +def patch_rom(world: "KDL3World", patch: KDL3ProcedurePatch) -> None: + patch.write_file("kdl3_basepatch.bsdiff4", + get_data(__name__, os.path.join("data", "kdl3_basepatch.bsdiff4"))) + + # Write open world patch + if world.options.open_world: + patch.write_token(APTokenTypes.WRITE, 0x143C7, bytes([0xAD, 0xC1, 0x5A, 0xCD, 0xC1, 0x5A, ])) + # changes the stage flag function to compare $5AC1 to $5AC1, + # always running the "new stage" function + # This has further checks present for bosses already, so we just + # need to handle regular stages + # write check for boss to be unlocked + + if world.options.consumables: + # reroute maxim tomatoes to use the 1-UP function, then null out the function + patch.write_token(APTokenTypes.WRITE, 0x3002F, bytes([0x37, 0x00])) + patch.write_token(APTokenTypes.WRITE, 0x30037, bytes([0xA9, 0x26, 0x00, # LDA #$0026 + 0x22, 0x27, 0xD9, 0x00, # JSL $00D927 + 0xA4, 0xD2, # LDY $D2 + 0x6B, # RTL + 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, + 0xEA, # NOP #10 + ])) + + # stars handling is built into the rom, so no changes there + + rooms = world.rooms + if world.options.music_shuffle > 0: + if world.options.music_shuffle == 1: + shuffled_music = music_choices.copy() + world.random.shuffle(shuffled_music) + music_map = dict(zip(music_choices, shuffled_music)) + # Avoid putting star twinkle in the pool + music_map[5] = world.random.choice(music_choices) + # Heart Star music doesn't work on regular stages + music_map[8] = world.random.choice(music_choices) + for room in rooms: + room.music = music_map[room.music] + for room_ptr in room_music: + patch.write_token(APTokenTypes.WRITE, room_ptr + 2, bytes([music_map[room_music[room_ptr]]])) + for i, old_music in zip(range(5), [25, 26, 28, 27, 30]): + # level themes + patch.write_token(APTokenTypes.WRITE, 0x133F2 + i, bytes([music_map[old_music]])) + # Zero + patch.write_token(APTokenTypes.WRITE, 0x9AE79, music_map[0x18].to_bytes(1, "little")) + # Heart Star success and fail + patch.write_token(APTokenTypes.WRITE, 0x4A388, music_map[0x08].to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x4A38D, music_map[0x1D].to_bytes(1, "little")) + elif world.options.music_shuffle == 2: + for room in rooms: + room.music = world.random.choice(music_choices) + for room_ptr in room_music: + patch.write_token(APTokenTypes.WRITE, room_ptr + 2, + world.random.choice(music_choices).to_bytes(1, "little")) + for i in range(5): + # level themes + patch.write_token(APTokenTypes.WRITE, 0x133F2 + i, + world.random.choice(music_choices).to_bytes(1, "little")) + # Zero + patch.write_token(APTokenTypes.WRITE, 0x9AE79, world.random.choice(music_choices).to_bytes(1, "little")) + # Heart Star success and fail + patch.write_token(APTokenTypes.WRITE, 0x4A388, world.random.choice(music_choices).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x4A38D, world.random.choice(music_choices).to_bytes(1, "little")) + + for room in rooms: + room.patch(patch, bool(world.options.consumables.value), not bool(world.options.remote_items.value)) + + if world.options.virtual_console in [1, 3]: + # Flash Reduction + patch.write_token(APTokenTypes.WRITE, 0x9AE68, b"\x10") + patch.write_token(APTokenTypes.WRITE, 0x9AE8E, bytes([0x08, 0x00, 0x22, 0x5D, 0xF7, 0x00, 0xA2, 0x08, ])) + patch.write_token(APTokenTypes.WRITE, 0x9AEA1, b"\x08") + patch.write_token(APTokenTypes.WRITE, 0x9AEC9, b"\x01") + patch.write_token(APTokenTypes.WRITE, 0x9AED2, bytes([0xA9, 0x1F])) + patch.write_token(APTokenTypes.WRITE, 0x9AEE1, b"\x08") + + if world.options.virtual_console in [2, 3]: + # Hyper Zone BB colors + patch.write_token(APTokenTypes.WRITE, 0x2C5E16, bytes([0xEE, 0x1B, 0x18, 0x5B, 0xD3, 0x4A, 0xF4, 0x3B, ])) + patch.write_token(APTokenTypes.WRITE, 0x2C8217, bytes([0xFF, 0x1E, ])) + + # boss requirements + patch.write_token(APTokenTypes.WRITE, 0x3D000, + struct.pack("HHHHH", world.boss_requirements[0], world.boss_requirements[1], + world.boss_requirements[2], world.boss_requirements[3], + world.boss_requirements[4])) + patch.write_token(APTokenTypes.WRITE, 0x3D00A, + struct.pack("H", world.required_heart_stars if world.options.goal_speed == 1 else 0xFFFF)) + patch.write_token(APTokenTypes.WRITE, 0x3D00C, world.options.goal_speed.value.to_bytes(2, "little")) + patch.write_token(APTokenTypes.WRITE, 0x3D00E, world.options.open_world.value.to_bytes(2, "little")) + patch.write_token(APTokenTypes.WRITE, 0x3D010, ((world.options.remote_items.value << 1) + + world.options.death_link.value).to_bytes(2, "little")) + patch.write_token(APTokenTypes.WRITE, 0x3D012, world.options.goal.value.to_bytes(2, "little")) + patch.write_token(APTokenTypes.WRITE, 0x3D014, world.options.stage_shuffle.value.to_bytes(2, "little")) + patch.write_token(APTokenTypes.WRITE, 0x3D016, world.options.ow_boss_requirement.value.to_bytes(2, "little")) + patch.write_token(APTokenTypes.WRITE, 0x3D018, world.options.consumables.value.to_bytes(2, "little")) + patch.write_token(APTokenTypes.WRITE, 0x3D01A, world.options.starsanity.value.to_bytes(2, "little")) + patch.write_token(APTokenTypes.WRITE, 0x3D01C, world.options.gifting.value.to_bytes(2, "little") + if world.multiworld.players > 1 else bytes([0, 0])) + patch.write_token(APTokenTypes.WRITE, 0x3D01E, world.options.strict_bosses.value.to_bytes(2, "little")) + # don't write gifting for solo game, since there's no one to send anything to + + for level in world.player_levels: + for i in range(len(world.player_levels[level])): + patch.write_token(APTokenTypes.WRITE, 0x3F002E + ((level - 1) * 14) + (i * 2), + struct.pack("H", level_pointers[world.player_levels[level][i]])) + patch.write_token(APTokenTypes.WRITE, 0x3D020 + (level - 1) * 14 + (i * 2), + struct.pack("H", world.player_levels[level][i] & 0x00FFFF)) + if (i == 0) or (i > 0 and i % 6 != 0): + patch.write_token(APTokenTypes.WRITE, 0x3D080 + (level - 1) * 12 + (i * 2), + struct.pack("H", (world.player_levels[level][i] & 0x00FFFF) % 6)) + + for i in range(6): + if world.boss_butch_bosses[i]: + patch.write_token(APTokenTypes.WRITE, 0x3F0000 + (level_pointers[0x770200 + i]), + struct.pack("I", bb_bosses[0x770200 + i])) + + # copy ability shuffle + if world.options.copy_ability_randomization.value > 0: + for enemy in world.copy_abilities: + if enemy in miniboss_remap: + patch.write_token(APTokenTypes.WRITE, 0xB417E + (miniboss_remap[enemy] << 1), + struct.pack("H", ability_remap[world.copy_abilities[enemy]])) + else: + patch.write_token(APTokenTypes.WRITE, 0xB3CAC + (enemy_remap[enemy] << 1), + struct.pack("H", ability_remap[world.copy_abilities[enemy]])) + # following only needs done on non-door rando + # incredibly lucky this follows the same order (including 5E == star block) + patch.write_token(APTokenTypes.WRITE, 0x2F77EA, + (0x5E + (ability_remap[world.copy_abilities["Sparky"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2F7811, + (0x5E + (ability_remap[world.copy_abilities["Sparky"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2F9BC4, + (0x5E + (ability_remap[world.copy_abilities["Blocky"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2F9BEB, + (0x5E + (ability_remap[world.copy_abilities["Blocky"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2FAC06, + (0x5E + (ability_remap[world.copy_abilities["Jumper Shoot"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2FAC2D, + (0x5E + (ability_remap[world.copy_abilities["Jumper Shoot"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2F9E7B, + (0x5E + (ability_remap[world.copy_abilities["Yuki"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2F9EA2, + (0x5E + (ability_remap[world.copy_abilities["Yuki"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2FA951, + (0x5E + (ability_remap[world.copy_abilities["Sir Kibble"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2FA978, + (0x5E + (ability_remap[world.copy_abilities["Sir Kibble"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2FA132, + (0x5E + (ability_remap[world.copy_abilities["Haboki"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2FA159, + (0x5E + (ability_remap[world.copy_abilities["Haboki"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2FA3E8, + (0x5E + (ability_remap[world.copy_abilities["Boboo"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2FA40F, + (0x5E + (ability_remap[world.copy_abilities["Boboo"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2F90E2, + (0x5E + (ability_remap[world.copy_abilities["Captain Stitch"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2F9109, + (0x5E + (ability_remap[world.copy_abilities["Captain Stitch"]] << 1)).to_bytes(1, "little")) + + if world.options.copy_ability_randomization == 2: + for enemy in enemy_remap: + # we just won't include it for minibosses + patch.write_token(APTokenTypes.WRITE, 0xB3E40 + (enemy_remap[enemy] << 1), + struct.pack("h", world.random.randint(-1, 2))) + + # write jumping goal + patch.write_token(APTokenTypes.WRITE, 0x94F8, struct.pack("H", world.options.jumping_target)) + patch.write_token(APTokenTypes.WRITE, 0x944E, struct.pack("H", world.options.jumping_target)) + + from Utils import __version__ + patch_name = bytearray( + f'KDL3{__version__.replace(".", "")[0:3]}_{world.player}_{world.multiworld.seed:11}\0', 'utf8')[:21] + patch_name.extend([0] * (21 - len(patch_name))) + patch.name = bytes(patch_name) + patch.write_token(APTokenTypes.WRITE, 0x3C000, patch.name) + patch.write_token(APTokenTypes.WRITE, 0x3C020, world.options.game_language.value.to_bytes(1, "little")) + + patch.write_token(APTokenTypes.COPY, 0x7FC0, (21, 0x3C000)) + patch.write_token(APTokenTypes.COPY, 0x7FD9, (1, 0x3C020)) + + # handle palette + if world.options.kirby_flavor_preset.value != 0: + for addr in kirby_target_palettes: + target = kirby_target_palettes[addr] + palette = get_kirby_palette(world) + if palette is not None: + patch.write_token(APTokenTypes.WRITE, addr, get_palette_bytes(palette, target[0], target[1], target[2])) + + if world.options.gooey_flavor_preset.value != 0: + for addr in gooey_target_palettes: + target = gooey_target_palettes[addr] + palette = get_gooey_palette(world) + if palette is not None: + patch.write_token(APTokenTypes.WRITE, addr, get_palette_bytes(palette, target[0], target[1], target[2])) + + patch.write_file("token_patch.bin", patch.get_token_binary()) + + +def get_base_rom_bytes() -> bytes: + rom_file: str = get_base_rom_path() + base_rom_bytes: Optional[bytes] = getattr(get_base_rom_bytes, "base_rom_bytes", None) + if not base_rom_bytes: + base_rom_bytes = bytes(Utils.read_snes_rom(open(rom_file, "rb"))) + + basemd5 = hashlib.md5() + basemd5.update(base_rom_bytes) + if basemd5.hexdigest() not in {KDL3UHASH, KDL3JHASH}: + raise Exception("Supplied Base Rom does not match known MD5 for US or JP release. " + "Get the correct game and version, then dump it") + get_base_rom_bytes.base_rom_bytes = base_rom_bytes + return base_rom_bytes + + +def get_base_rom_path(file_name: str = "") -> str: + options: settings.Settings = settings.get_settings() + if not file_name: + file_name = options["kdl3_options"]["rom_file"] + if not os.path.exists(file_name): + file_name = Utils.user_path(file_name) + return file_name diff --git a/worlds/kdl3/room.py b/worlds/kdl3/room.py new file mode 100644 index 0000000000..bcc1c7a709 --- /dev/null +++ b/worlds/kdl3/room.py @@ -0,0 +1,133 @@ +import struct +from typing import Optional, Dict, TYPE_CHECKING, List, Union +from BaseClasses import Region, ItemClassification, MultiWorld +from worlds.Files import APTokenTypes +from .client_addrs import consumable_addrs, star_addrs + +if TYPE_CHECKING: + from .rom import KDL3ProcedurePatch + +animal_map = { + "Rick Spawn": 0, + "Kine Spawn": 1, + "Coo Spawn": 2, + "Nago Spawn": 3, + "ChuChu Spawn": 4, + "Pitch Spawn": 5 +} + + +class KDL3Room(Region): + pointer: int = 0 + level: int = 0 + stage: int = 0 + room: int = 0 + music: int = 0 + default_exits: List[Dict[str, Union[int, List[str]]]] + animal_pointers: List[int] + enemies: List[str] + entity_load: List[List[int]] + consumables: List[Dict[str, Union[int, str]]] + + def __init__(self, name: str, player: int, multiworld: MultiWorld, hint: Optional[str], level: int, + stage: int, room: int, pointer: int, music: int, + default_exits: List[Dict[str, List[str]]], + animal_pointers: List[int], enemies: List[str], + entity_load: List[List[int]], + consumables: List[Dict[str, Union[int, str]]], consumable_pointer: int) -> None: + super().__init__(name, player, multiworld, hint) + self.level = level + self.stage = stage + self.room = room + self.pointer = pointer + self.music = music + self.default_exits = default_exits + self.animal_pointers = animal_pointers + self.enemies = enemies + self.entity_load = entity_load + self.consumables = consumables + self.consumable_pointer = consumable_pointer + + def patch(self, patch: "KDL3ProcedurePatch", consumables: bool, local_items: bool) -> None: + patch.write_token(APTokenTypes.WRITE, self.pointer + 2, self.music.to_bytes(1, "little")) + animals = [x.item.name for x in self.locations if "Animal" in x.name and x.item] + if len(animals) > 0: + for current_animal, address in zip(animals, self.animal_pointers): + patch.write_token(APTokenTypes.WRITE, self.pointer + address + 7, + animal_map[current_animal].to_bytes(1, "little")) + if local_items: + for location in self.get_locations(): + if location.item is None or location.item.player != self.player: + continue + item = location.item.code + if item is None: + continue + item_idx = item & 0x00000F + location_idx = location.address & 0xFFFF + if location_idx & 0xF00 in (0x300, 0x400, 0x500, 0x600): + # consumable or star, need remapped + location_base = location_idx & 0xF00 + if location_base == 0x300: + # consumable + location_idx = consumable_addrs[location_idx & 0xFF] | 0x1000 + else: + # star + location_idx = star_addrs[location.address] | 0x2000 + if item & 0x000070 == 0: + patch.write_token(APTokenTypes.WRITE, 0x4B000 + location_idx, bytes([item_idx | 0x10])) + elif item & 0x000010 > 0: + patch.write_token(APTokenTypes.WRITE, 0x4B000 + location_idx, bytes([item_idx | 0x20])) + elif item & 0x000020 > 0: + patch.write_token(APTokenTypes.WRITE, 0x4B000 + location_idx, bytes([item_idx | 0x40])) + elif item & 0x000040 > 0: + patch.write_token(APTokenTypes.WRITE, 0x4B000 + location_idx, bytes([item_idx | 0x80])) + + if consumables: + load_len = len(self.entity_load) + for consumable in self.consumables: + location = next(x for x in self.locations if x.name == consumable["name"]) + assert location.item is not None + is_progression = location.item.classification & ItemClassification.progression + if load_len == 8: + # edge case, there is exactly 1 room with 8 entities and only 1 consumable among them + if not (any(x in self.entity_load for x in [[0, 22], [1, 22]]) + and any(x in self.entity_load for x in [[2, 22], [3, 22]])): + replacement_target = self.entity_load.index( + next(x for x in self.entity_load if x in [[0, 22], [1, 22], [2, 22], [3, 22]])) + if is_progression: + vtype = 0 + else: + vtype = 2 + patch.write_token(APTokenTypes.WRITE, self.pointer + 88 + (replacement_target * 2), + vtype.to_bytes(1, "little")) + self.entity_load[replacement_target] = [vtype, 22] + else: + if is_progression: + # we need to see if 1-ups are in our load list + if any(x not in self.entity_load for x in [[0, 22], [1, 22]]): + self.entity_load.append([0, 22]) + else: + if any(x not in self.entity_load for x in [[2, 22], [3, 22]]): + # edge case: if (1, 22) is in, we need to load (3, 22) instead + if [1, 22] in self.entity_load: + self.entity_load.append([3, 22]) + else: + self.entity_load.append([2, 22]) + if load_len < len(self.entity_load): + patch.write_token(APTokenTypes.WRITE, self.pointer + 88 + (load_len * 2), + bytes(self.entity_load[load_len])) + patch.write_token(APTokenTypes.WRITE, self.pointer + 104 + (load_len * 2), + bytes(struct.pack("H", self.consumable_pointer))) + if is_progression: + if [1, 22] in self.entity_load: + vtype = 1 + else: + vtype = 0 + else: + if [3, 22] in self.entity_load: + vtype = 3 + else: + vtype = 2 + assert isinstance(consumable["pointer"], int) + patch.write_token(APTokenTypes.WRITE, self.pointer + consumable["pointer"] + 7, + vtype.to_bytes(1, "little")) diff --git a/worlds/kdl3/Rules.py b/worlds/kdl3/rules.py similarity index 70% rename from worlds/kdl3/Rules.py rename to worlds/kdl3/rules.py index 6a85ef84f0..a08e99257e 100644 --- a/worlds/kdl3/Rules.py +++ b/worlds/kdl3/rules.py @@ -1,7 +1,7 @@ from worlds.generic.Rules import set_rule, add_rule -from .Names import LocationName, EnemyAbilities -from .Locations import location_table -from .Options import GoalSpeed +from .names import location_name, enemy_abilities, animal_friend_spawns +from .locations import location_table +from .options import GoalSpeed import typing if typing.TYPE_CHECKING: @@ -10,9 +10,9 @@ if typing.TYPE_CHECKING: def can_reach_boss(state: "CollectionState", player: int, level: int, open_world: int, - ow_boss_req: int, player_levels: typing.Dict[int, typing.Dict[int, int]]): + ow_boss_req: int, player_levels: typing.Dict[int, typing.List[int]]) -> bool: if open_world: - return state.has(f"{LocationName.level_names_inverse[level]} - Stage Completion", player, ow_boss_req) + return state.has(f"{location_name.level_names_inverse[level]} - Stage Completion", player, ow_boss_req) else: return state.can_reach(location_table[player_levels[level][5]], "Location", player) @@ -86,11 +86,11 @@ ability_map: typing.Dict[str, typing.Callable[["CollectionState", int], bool]] = } -def can_assemble_rob(state: "CollectionState", player: int, copy_abilities: typing.Dict[str, str]): +def can_assemble_rob(state: "CollectionState", player: int, copy_abilities: typing.Dict[str, str]) -> bool: # check animal requirements if not (can_reach_coo(state, player) and can_reach_kine(state, player)): return False - for abilities, bukisets in EnemyAbilities.enemy_restrictive[1:5]: + for abilities, bukisets in enemy_abilities.enemy_restrictive[1:5]: iterator = iter(x for x in bukisets if copy_abilities[x] in abilities) target_bukiset = next(iterator, None) can_reach = False @@ -103,7 +103,7 @@ def can_assemble_rob(state: "CollectionState", player: int, copy_abilities: typi return can_reach_parasol(state, player) and can_reach_stone(state, player) -def can_fix_angel_wings(state: "CollectionState", player: int, copy_abilities: typing.Dict[str, str]): +def can_fix_angel_wings(state: "CollectionState", player: int, copy_abilities: typing.Dict[str, str]) -> bool: can_reach = True for enemy in {"Sparky", "Blocky", "Jumper Shoot", "Yuki", "Sir Kibble", "Haboki", "Boboo", "Captain Stitch"}: can_reach = can_reach & ability_map[copy_abilities[enemy]](state, player) @@ -112,114 +112,114 @@ def can_fix_angel_wings(state: "CollectionState", player: int, copy_abilities: t def set_rules(world: "KDL3World") -> None: # Level 1 - set_rule(world.multiworld.get_location(LocationName.grass_land_muchi, world.player), + set_rule(world.multiworld.get_location(location_name.grass_land_muchi, world.player), lambda state: can_reach_chuchu(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.grass_land_chao, world.player), + set_rule(world.multiworld.get_location(location_name.grass_land_chao, world.player), lambda state: can_reach_stone(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.grass_land_mine, world.player), + set_rule(world.multiworld.get_location(location_name.grass_land_mine, world.player), lambda state: can_reach_kine(state, world.player)) # Level 2 - set_rule(world.multiworld.get_location(LocationName.ripple_field_5, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_5, world.player), lambda state: can_reach_kine(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.ripple_field_kamuribana, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_kamuribana, world.player), lambda state: can_reach_pitch(state, world.player) and can_reach_clean(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.ripple_field_bakasa, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_bakasa, world.player), lambda state: can_reach_kine(state, world.player) and can_reach_parasol(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.ripple_field_toad, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_toad, world.player), lambda state: can_reach_needle(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.ripple_field_mama_pitch, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_mama_pitch, world.player), lambda state: (can_reach_pitch(state, world.player) and can_reach_kine(state, world.player) and can_reach_burning(state, world.player) and can_reach_stone(state, world.player))) # Level 3 - set_rule(world.multiworld.get_location(LocationName.sand_canyon_5, world.player), + set_rule(world.multiworld.get_location(location_name.sand_canyon_5, world.player), lambda state: can_reach_cutter(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.sand_canyon_auntie, world.player), + set_rule(world.multiworld.get_location(location_name.sand_canyon_auntie, world.player), lambda state: can_reach_clean(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.sand_canyon_nyupun, world.player), + set_rule(world.multiworld.get_location(location_name.sand_canyon_nyupun, world.player), lambda state: can_reach_chuchu(state, world.player) and can_reach_cutter(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.sand_canyon_rob, world.player), + set_rule(world.multiworld.get_location(location_name.sand_canyon_rob, world.player), lambda state: can_assemble_rob(state, world.player, world.copy_abilities) ) # Level 4 - set_rule(world.multiworld.get_location(LocationName.cloudy_park_hibanamodoki, world.player), + set_rule(world.multiworld.get_location(location_name.cloudy_park_hibanamodoki, world.player), lambda state: can_reach_coo(state, world.player) and can_reach_clean(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.cloudy_park_piyokeko, world.player), + set_rule(world.multiworld.get_location(location_name.cloudy_park_piyokeko, world.player), lambda state: can_reach_needle(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.cloudy_park_mikarin, world.player), + set_rule(world.multiworld.get_location(location_name.cloudy_park_mikarin, world.player), lambda state: can_reach_coo(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.cloudy_park_pick, world.player), + set_rule(world.multiworld.get_location(location_name.cloudy_park_pick, world.player), lambda state: can_reach_rick(state, world.player)) # Level 5 - set_rule(world.multiworld.get_location(LocationName.iceberg_4, world.player), + set_rule(world.multiworld.get_location(location_name.iceberg_4, world.player), lambda state: can_reach_burning(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.iceberg_kogoesou, world.player), + set_rule(world.multiworld.get_location(location_name.iceberg_kogoesou, world.player), lambda state: can_reach_burning(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.iceberg_samus, world.player), + set_rule(world.multiworld.get_location(location_name.iceberg_samus, world.player), lambda state: can_reach_ice(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.iceberg_name, world.player), + set_rule(world.multiworld.get_location(location_name.iceberg_name, world.player), lambda state: (can_reach_coo(state, world.player) and can_reach_burning(state, world.player) and can_reach_chuchu(state, world.player))) # ChuChu is guaranteed here, but we use this for consistency - set_rule(world.multiworld.get_location(LocationName.iceberg_shiro, world.player), + set_rule(world.multiworld.get_location(location_name.iceberg_shiro, world.player), lambda state: can_reach_nago(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.iceberg_angel, world.player), + set_rule(world.multiworld.get_location(location_name.iceberg_angel, world.player), lambda state: can_fix_angel_wings(state, world.player, world.copy_abilities)) # Consumables if world.options.consumables: - set_rule(world.multiworld.get_location(LocationName.grass_land_1_u1, world.player), + set_rule(world.multiworld.get_location(location_name.grass_land_1_u1, world.player), lambda state: can_reach_parasol(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.grass_land_1_m1, world.player), + set_rule(world.multiworld.get_location(location_name.grass_land_1_m1, world.player), lambda state: can_reach_spark(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.grass_land_2_u1, world.player), + set_rule(world.multiworld.get_location(location_name.grass_land_2_u1, world.player), lambda state: can_reach_needle(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.ripple_field_2_u1, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_2_u1, world.player), lambda state: can_reach_kine(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.ripple_field_2_m1, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_2_m1, world.player), lambda state: can_reach_kine(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.ripple_field_3_u1, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_3_u1, world.player), lambda state: can_reach_cutter(state, world.player) or can_reach_spark(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.ripple_field_4_u1, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_4_u1, world.player), lambda state: can_reach_stone(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.ripple_field_4_m2, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_4_m2, world.player), lambda state: can_reach_stone(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.ripple_field_5_m1, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_5_m1, world.player), lambda state: can_reach_kine(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.ripple_field_5_u1, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_5_u1, world.player), lambda state: (can_reach_kine(state, world.player) and can_reach_burning(state, world.player) and can_reach_stone(state, world.player))) - set_rule(world.multiworld.get_location(LocationName.ripple_field_5_m2, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_5_m2, world.player), lambda state: (can_reach_kine(state, world.player) and can_reach_burning(state, world.player) and can_reach_stone(state, world.player))) - set_rule(world.multiworld.get_location(LocationName.sand_canyon_4_u1, world.player), + set_rule(world.multiworld.get_location(location_name.sand_canyon_4_u1, world.player), lambda state: can_reach_clean(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.sand_canyon_4_m2, world.player), + set_rule(world.multiworld.get_location(location_name.sand_canyon_4_m2, world.player), lambda state: can_reach_needle(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.sand_canyon_5_u2, world.player), + set_rule(world.multiworld.get_location(location_name.sand_canyon_5_u2, world.player), lambda state: can_reach_ice(state, world.player) and (can_reach_rick(state, world.player) or can_reach_coo(state, world.player) or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player) or can_reach_nago(state, world.player))) - set_rule(world.multiworld.get_location(LocationName.sand_canyon_5_u3, world.player), + set_rule(world.multiworld.get_location(location_name.sand_canyon_5_u3, world.player), lambda state: can_reach_ice(state, world.player) and (can_reach_rick(state, world.player) or can_reach_coo(state, world.player) or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player) or can_reach_nago(state, world.player))) - set_rule(world.multiworld.get_location(LocationName.sand_canyon_5_u4, world.player), + set_rule(world.multiworld.get_location(location_name.sand_canyon_5_u4, world.player), lambda state: can_reach_ice(state, world.player) and (can_reach_rick(state, world.player) or can_reach_coo(state, world.player) or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player) or can_reach_nago(state, world.player))) - set_rule(world.multiworld.get_location(LocationName.cloudy_park_6_u1, world.player), + set_rule(world.multiworld.get_location(location_name.cloudy_park_6_u1, world.player), lambda state: can_reach_cutter(state, world.player)) if world.options.starsanity: @@ -274,50 +274,57 @@ def set_rules(world: "KDL3World") -> None: # copy ability access edge cases # Kirby cannot eat enemies fully submerged in water. Vast majority of cases, the enemy can be brought to the surface # and eaten by inhaling while falling on top of them - set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_2_E3, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_2_E3, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) - set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_3_E6, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_3_E6, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) # Ripple Field 4 E5, E7, and E8 are doable, but too strict to leave in logic - set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_4_E5, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_4_E5, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) - set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_4_E7, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_4_E7, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) - set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_4_E8, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_4_E8, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) - set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_5_E1, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_5_E1, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) - set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_5_E2, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_5_E2, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) - set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_5_E3, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_5_E3, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) - set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_5_E4, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_5_E4, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) - set_rule(world.multiworld.get_location(EnemyAbilities.Sand_Canyon_4_E7, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Sand_Canyon_4_E7, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) - set_rule(world.multiworld.get_location(EnemyAbilities.Sand_Canyon_4_E8, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Sand_Canyon_4_E8, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) - set_rule(world.multiworld.get_location(EnemyAbilities.Sand_Canyon_4_E9, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Sand_Canyon_4_E9, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) - set_rule(world.multiworld.get_location(EnemyAbilities.Sand_Canyon_4_E10, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Sand_Canyon_4_E10, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) + # animal friend rules + set_rule(world.multiworld.get_location(animal_friend_spawns.iceberg_4_a2, world.player), + lambda state: can_reach_coo(state, world.player) and can_reach_burning(state, world.player)) + set_rule(world.multiworld.get_location(animal_friend_spawns.iceberg_4_a3, world.player), + lambda state: can_reach_chuchu(state, world.player) and can_reach_coo(state, world.player) + and can_reach_burning(state, world.player)) + for boss_flag, purification, i in zip(["Level 1 Boss - Purified", "Level 2 Boss - Purified", "Level 3 Boss - Purified", "Level 4 Boss - Purified", "Level 5 Boss - Purified"], - [LocationName.grass_land_whispy, LocationName.ripple_field_acro, - LocationName.sand_canyon_poncon, LocationName.cloudy_park_ado, - LocationName.iceberg_dedede], + [location_name.grass_land_whispy, location_name.ripple_field_acro, + location_name.sand_canyon_poncon, location_name.cloudy_park_ado, + location_name.iceberg_dedede], range(1, 6)): set_rule(world.multiworld.get_location(boss_flag, world.player), - lambda state, i=i: (state.has("Heart Star", world.player, world.boss_requirements[i - 1]) - and can_reach_boss(state, world.player, i, + lambda state, x=i: (state.has("Heart Star", world.player, world.boss_requirements[x - 1]) + and can_reach_boss(state, world.player, x, world.options.open_world.value, world.options.ow_boss_requirement.value, world.player_levels))) set_rule(world.multiworld.get_location(purification, world.player), - lambda state, i=i: (state.has("Heart Star", world.player, world.boss_requirements[i - 1]) - and can_reach_boss(state, world.player, i, + lambda state, x=i: (state.has("Heart Star", world.player, world.boss_requirements[x - 1]) + and can_reach_boss(state, world.player, x, world.options.open_world.value, world.options.ow_boss_requirement.value, world.player_levels))) @@ -327,12 +334,12 @@ def set_rules(world: "KDL3World") -> None: for level in range(2, 6): set_rule(world.multiworld.get_entrance(f"To Level {level}", world.player), - lambda state, i=level: state.has(f"Level {i - 1} Boss Defeated", world.player)) + lambda state, x=level: state.has(f"Level {x - 1} Boss Defeated", world.player)) if world.options.strict_bosses: for level in range(2, 6): add_rule(world.multiworld.get_entrance(f"To Level {level}", world.player), - lambda state, i=level: state.has(f"Level {i - 1} Boss Purified", world.player)) + lambda state, x=level: state.has(f"Level {x - 1} Boss Purified", world.player)) if world.options.goal_speed == GoalSpeed.option_normal: add_rule(world.multiworld.get_entrance("To Level 6", world.player), diff --git a/worlds/kdl3/data/APPauseIcons.dat b/worlds/kdl3/src/APPauseIcons.dat similarity index 100% rename from worlds/kdl3/data/APPauseIcons.dat rename to worlds/kdl3/src/APPauseIcons.dat diff --git a/worlds/kdl3/src/kdl3_basepatch.asm b/worlds/kdl3/src/kdl3_basepatch.asm index e419d0632f..95c85f032c 100644 --- a/worlds/kdl3/src/kdl3_basepatch.asm +++ b/worlds/kdl3/src/kdl3_basepatch.asm @@ -58,6 +58,10 @@ org $01AFC8 org $01B013 SEC ; Remove Dedede Bad Ending +org $01B050 + JSL HookBossPurify + NOP + org $02B7B0 ; Zero unlock LDA $80A0 CMP #$0001 @@ -160,7 +164,6 @@ CopyAbilityAnimalOverride: STA $39DF, X RTL -org $079A00 HeartStarCheck: TXA CMP #$0000 ; is this level 1 @@ -201,7 +204,6 @@ HeartStarCheck: SEC RTL -org $079A80 OpenWorldUnlock: PHX LDX $900E ; Are we on open world? @@ -224,7 +226,6 @@ OpenWorldUnlock: PLX RTL -org $079B00 MainLoopHook: STA $D4 INC $3524 @@ -239,16 +240,18 @@ MainLoopHook: BEQ .Return ; return if we are LDA $5541 ; gooey status BPL .Slowness ; gooey is already spawned + LDA $39D1 ; is kirby alive? + BEQ .Slowness ; branch if he isn't + ; maybe BMI here too? LDA $8080 CMP #$0000 ; did we get a gooey trap BEQ .Slowness ; branch if we did not JSL GooeySpawn - STZ $8080 + DEC $8080 .Slowness: LDA $8082 ; slowness BEQ .Eject ; are we under the effects of a slowness trap - DEC - STA $8082 ; dec by 1 each frame + DEC $8082 ; dec by 1 each frame .Eject: PHX PHY @@ -258,14 +261,13 @@ MainLoopHook: BEQ .PullVars ; branch if we haven't received eject LDA #$2000 ; select button press STA $60C1 ; write to controller mirror - STZ $8084 + DEC $8084 .PullVars: PLY PLX .Return: RTL -org $079B80 HeartStarGraphicFix: LDA #$0000 PHX @@ -288,7 +290,7 @@ HeartStarGraphicFix: ASL TAX LDA $07D080, X ; table of original stage number - CMP #$0003 ; is the current stage a minigame stage? + CMP #$0002 ; is the current stage a minigame stage? BEQ .ReturnTrue ; branch if so CLC BRA .Return @@ -299,7 +301,6 @@ HeartStarGraphicFix: PLX RTL -org $079BF0 ParseItemQueue: ; Local item queue parsing NOP @@ -336,8 +337,6 @@ ParseItemQueue: AND #$000F ASL TAY - LDA $8080,Y - BNE .LoopCheck JSL .ApplyNegative RTL .ApplyAbility: @@ -418,35 +417,73 @@ ParseItemQueue: CPY #$0005 BCS .PlayNone LDA $8080,Y - BNE .Return + CPY #$0002 + BNE .Increment + CLC LDA #$0384 + ADC $8080, Y + BVC .PlayNegative + LDA #$FFFF + .PlayNegative: STA $8080,Y LDA #$00A7 BRA .PlaySFXLong + .Increment: + INC + STA $8080, Y + BRA .PlayNegative .PlayNone: LDA #$0000 BRA .PlaySFXLong -org $079D00 AnimalFriendSpawn: PHA CPX #$0002 ; is this an animal friend? BNE .Return XBA PHA + PHX + PHA + LDX #$0000 + .CheckSpawned: + LDA $05CA, X + BNE .Continue + LDA #$0002 + CMP $074A, X + BNE .ContinueCheck + PLA + PHA + XBA + CMP $07CA, X + BEQ .AlreadySpawned + .ContinueCheck: + INX + INX + BRA .CheckSpawned + .Continue: + PLA + PLX ASL TAY PLA INC CMP $8000, Y ; do we have this animal friend BEQ .Return ; we have this animal friend + .False: INX .Return: PLY LDA #$9999 RTL + .AlreadySpawned: + PLA + PLX + ASL + TAY + PLA + BRA .False + -org $079E00 WriteBWRAM: LDY #$6001 ;starting addr LDA #$1FFE ;bytes to write @@ -479,7 +516,6 @@ WriteBWRAM: .Return: RTL -org $079E80 ConsumableSet: PHA PHX @@ -507,7 +543,6 @@ ConsumableSet: ASL TAX LDA $07D020, X ; current stage - DEC ASL #6 TAX PLA @@ -519,8 +554,16 @@ ConsumableSet: BRA .LoopHead ; return to loop head .ApplyCheck: LDA $A000, X ; consumables index + PHA ORA #$0001 STA $A000, X + PLA + AND #$00FF + BNE .Return + TXA + ORA #$1000 + JSL ApplyLocalCheck + .Return: PLY PLX PLA @@ -528,7 +571,6 @@ ConsumableSet: AND #$00FF RTL -org $079F00 NormalGoalSet: PHX LDA $07D012 @@ -549,7 +591,6 @@ NormalGoalSet: STA $5AC1 ; cutscene RTL -org $079F80 FinalIcebergFix: PHX PHY @@ -572,7 +613,7 @@ FinalIcebergFix: ASL TAX LDA $07D020, X - CMP #$001E + CMP #$001D BEQ .ReturnTrue CLC BRA .Return @@ -583,7 +624,6 @@ FinalIcebergFix: PLX RTL -org $07A000 StrictBosses: PHX LDA $901E ; Do we have strict bosses enabled? @@ -610,7 +650,6 @@ StrictBosses: LDA $53CD RTL -org $07A030 NintenHalken: LDX #$0005 .Halken: @@ -628,7 +667,6 @@ NintenHalken: LDA #$0001 RTL -org $07A080 StageCompleteSet: PHX LDA $5AC1 ; completed stage cutscene @@ -656,9 +694,17 @@ StageCompleteSet: ASL TAX LDA $9020, X ; load the stage we completed - DEC ASL TAX + PHX + LDA $8200, X + AND #$00FF + BNE .ApplyClear + TXA + LSR + JSL ApplyLocalCheck + .ApplyClear: + PLX LDA #$0001 ORA $8200, X STA $8200, X @@ -668,7 +714,6 @@ StageCompleteSet: CMP $53CB RTL -org $07A100 OpenWorldBossUnlock: PHX PHY @@ -699,7 +744,6 @@ OpenWorldBossUnlock: .LoopStage: PLX LDY $9020, X ; get stage id - DEY INX INX PHA @@ -732,7 +776,6 @@ OpenWorldBossUnlock: PLX RTL -org $07A180 GooeySpawn: PHY PHX @@ -768,7 +811,6 @@ GooeySpawn: PLY RTL -org $07A200 SpeedTrap: PHX LDX $8082 ; do we have slowness @@ -780,7 +822,6 @@ SpeedTrap: EOR #$FFFF RTL -org $07A280 HeartStarVisual: CPX #$0000 BEQ .SkipInx @@ -844,7 +885,6 @@ HeartStarVisual: .Return: RTL -org $07A300 LoadFont: JSL $00D29F ; play sfx PHX @@ -915,7 +955,6 @@ LoadFont: PLX RTL -org $07A380 HeartStarVisual2: LDA #$2C80 STA $0000, Y @@ -1029,14 +1068,12 @@ HeartStarVisual2: STA $0000, Y RTL -org $07A480 HeartStarSelectFix: PHX TXA ASL TAX LDA $9020, X - DEC TAX .LoopHead: CMP #$0006 @@ -1051,15 +1088,31 @@ HeartStarSelectFix: AND #$00FF RTL -org $07A500 HeartStarCutsceneFix: TAX LDA $53D3 DEC STA $5AC3 + LDA $53A7, X + AND #$00FF + BNE .Return + PHX + TXA + .Loop: + CMP #$0007 + BCC .Continue + SEC + SBC #$0007 + DEX + BRA .Loop + .Continue: + TXA + ORA #$0100 + JSL ApplyLocalCheck + PLX + .Return RTL -org $07A510 GiftGiving: CMP #$0008 .This: @@ -1075,7 +1128,6 @@ GiftGiving: PLX JML $CABC18 -org $07A550 PauseMenu: JSL $00D29F PHX @@ -1136,7 +1188,6 @@ PauseMenu: PLX RTL -org $07A600 StarsSet: PHA PHX @@ -1166,7 +1217,6 @@ StarsSet: ASL TAX LDA $07D020, X - DEC ASL ASL ASL @@ -1183,8 +1233,15 @@ StarsSet: BRA .2LoopHead .2LoopEnd: LDA $B000, X + PHA ORA #$0001 STA $B000, X + PLA + AND #$00FF + BNE .Return + TXA + ORA #$2000 + JSL ApplyLocalCheck .Return: PLY PLX @@ -1199,6 +1256,48 @@ StarsSet: STA $39D7 BRA .Return +ApplyLocalCheck: +; args: A-address of check following $08B000 + TAX + LDA $09B000, X + AND #$00FF + TAY + LDX #$0000 + .Loop: + LDA $C000, X + BEQ .Apply + INX + INX + CPX #$0010 + BCC .Loop + BRA .Return ; this is dangerous, could lose a check here + .Apply: + TYA + STA $C000, X + .Return: + RTL + +HookBossPurify: + ORA $B0 + STA $53D5 + LDA $B0 + LDX #$0000 + LSR + .Loop: + BIT #$0001 + BNE .Apply + LSR + LSR + INX + CPX #$0005 + BCS .Return + BRA .Loop + .Apply: + TXA + ORA #$0200 + JSL ApplyLocalCheck + .Return: + RTL org $07C000 db "KDL3_BASEPATCH_ARCHI" @@ -1234,4 +1333,7 @@ org $07E040 db $3A, $01 db $3B, $05 db $3C, $05 - db $3D, $05 \ No newline at end of file + db $3D, $05 + +org $07F000 +incbin "APPauseIcons.dat" \ No newline at end of file diff --git a/worlds/kdl3/test/__init__.py b/worlds/kdl3/test/__init__.py index 4d3f4d70fa..92f1d7261f 100644 --- a/worlds/kdl3/test/__init__.py +++ b/worlds/kdl3/test/__init__.py @@ -6,6 +6,8 @@ from test.bases import WorldTestBase from test.general import gen_steps from worlds import AutoWorld from worlds.AutoWorld import call_all +# mypy: ignore-errors +# This is a copy of core code, and I'm not smart enough to solve the errors in here class KDL3TestBase(WorldTestBase): diff --git a/worlds/kdl3/test/test_goal.py b/worlds/kdl3/test/test_goal.py index ce53642a97..2c6ae614d4 100644 --- a/worlds/kdl3/test/test_goal.py +++ b/worlds/kdl3/test/test_goal.py @@ -5,12 +5,12 @@ class TestFastGoal(KDL3TestBase): options = { "open_world": False, "goal_speed": "fast", - "total_heart_stars": 30, + "max_heart_stars": 30, "heart_stars_required": 50, "filler_percentage": 0, } - def test_goal(self): + def test_goal(self) -> None: self.assertBeatable(False) heart_stars = self.get_items_by_name("Heart Star") self.collect(heart_stars[0:14]) @@ -30,12 +30,12 @@ class TestNormalGoal(KDL3TestBase): options = { "open_world": False, "goal_speed": "normal", - "total_heart_stars": 30, + "max_heart_stars": 30, "heart_stars_required": 50, "filler_percentage": 0, } - def test_goal(self): + def test_goal(self) -> None: self.assertBeatable(False) heart_stars = self.get_items_by_name("Heart Star") self.collect(heart_stars[0:14]) @@ -51,14 +51,14 @@ class TestNormalGoal(KDL3TestBase): self.assertEqual(self.count("Heart Star"), 30, str(self.multiworld.seed)) self.assertBeatable(True) - def test_kine(self): + def test_kine(self) -> None: self.collect_by_name(["Cutter", "Burning", "Heart Star"]) self.assertBeatable(False) - def test_cutter(self): + def test_cutter(self) -> None: self.collect_by_name(["Kine", "Burning", "Heart Star"]) self.assertBeatable(False) - def test_burning(self): + def test_burning(self) -> None: self.collect_by_name(["Cutter", "Kine", "Heart Star"]) self.assertBeatable(False) diff --git a/worlds/kdl3/test/test_locations.py b/worlds/kdl3/test/test_locations.py index bde9abc409..024f1b11a5 100644 --- a/worlds/kdl3/test/test_locations.py +++ b/worlds/kdl3/test/test_locations.py @@ -1,6 +1,6 @@ from . import KDL3TestBase +from ..names import location_name from Options import PlandoConnection -from ..Names import LocationName import typing @@ -12,31 +12,31 @@ class TestLocations(KDL3TestBase): # these ensure we can always reach all stages physically } - def test_simple_heart_stars(self): - self.run_location_test(LocationName.grass_land_muchi, ["ChuChu"]) - self.run_location_test(LocationName.grass_land_chao, ["Stone"]) - self.run_location_test(LocationName.grass_land_mine, ["Kine"]) - self.run_location_test(LocationName.ripple_field_kamuribana, ["Pitch", "Clean"]) - self.run_location_test(LocationName.ripple_field_bakasa, ["Kine", "Parasol"]) - self.run_location_test(LocationName.ripple_field_toad, ["Needle"]) - self.run_location_test(LocationName.ripple_field_mama_pitch, ["Pitch", "Kine", "Burning", "Stone"]) - self.run_location_test(LocationName.sand_canyon_auntie, ["Clean"]) - self.run_location_test(LocationName.sand_canyon_nyupun, ["ChuChu", "Cutter"]) - self.run_location_test(LocationName.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Spark", "Ice"]) - self.run_location_test(LocationName.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Clean", "Ice"]), - self.run_location_test(LocationName.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Spark", "Needle"]), - self.run_location_test(LocationName.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Clean", "Needle"]), - self.run_location_test(LocationName.cloudy_park_hibanamodoki, ["Coo", "Clean"]) - self.run_location_test(LocationName.cloudy_park_piyokeko, ["Needle"]) - self.run_location_test(LocationName.cloudy_park_mikarin, ["Coo"]) - self.run_location_test(LocationName.cloudy_park_pick, ["Rick"]) - self.run_location_test(LocationName.iceberg_kogoesou, ["Burning"]) - self.run_location_test(LocationName.iceberg_samus, ["Ice"]) - self.run_location_test(LocationName.iceberg_name, ["Burning", "Coo", "ChuChu"]) - self.run_location_test(LocationName.iceberg_angel, ["Cutter", "Burning", "Spark", "Parasol", "Needle", "Clean", + def test_simple_heart_stars(self) -> None: + self.run_location_test(location_name.grass_land_muchi, ["ChuChu"]) + self.run_location_test(location_name.grass_land_chao, ["Stone"]) + self.run_location_test(location_name.grass_land_mine, ["Kine"]) + self.run_location_test(location_name.ripple_field_kamuribana, ["Pitch", "Clean"]) + self.run_location_test(location_name.ripple_field_bakasa, ["Kine", "Parasol"]) + self.run_location_test(location_name.ripple_field_toad, ["Needle"]) + self.run_location_test(location_name.ripple_field_mama_pitch, ["Pitch", "Kine", "Burning", "Stone"]) + self.run_location_test(location_name.sand_canyon_auntie, ["Clean"]) + self.run_location_test(location_name.sand_canyon_nyupun, ["ChuChu", "Cutter"]) + self.run_location_test(location_name.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Spark", "Ice"]) + self.run_location_test(location_name.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Clean", "Ice"]) + self.run_location_test(location_name.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Spark", "Needle"]) + self.run_location_test(location_name.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Clean", "Needle"]) + self.run_location_test(location_name.cloudy_park_hibanamodoki, ["Coo", "Clean"]) + self.run_location_test(location_name.cloudy_park_piyokeko, ["Needle"]) + self.run_location_test(location_name.cloudy_park_mikarin, ["Coo"]) + self.run_location_test(location_name.cloudy_park_pick, ["Rick"]) + self.run_location_test(location_name.iceberg_kogoesou, ["Burning"]) + self.run_location_test(location_name.iceberg_samus, ["Ice"]) + self.run_location_test(location_name.iceberg_name, ["Burning", "Coo", "ChuChu"]) + self.run_location_test(location_name.iceberg_angel, ["Cutter", "Burning", "Spark", "Parasol", "Needle", "Clean", "Stone", "Ice"]) - def run_location_test(self, location: str, itempool: typing.List[str]): + def run_location_test(self, location: str, itempool: typing.List[str]) -> None: items = itempool.copy() while len(itempool) > 0: self.assertFalse(self.can_reach_location(location), str(self.multiworld.seed)) @@ -57,7 +57,7 @@ class TestShiro(KDL3TestBase): "plando_options": "connections" } - def test_shiro(self): + def test_shiro(self) -> None: self.assertFalse(self.can_reach_location("Iceberg 5 - Shiro"), str(self.multiworld.seed)) self.collect_by_name("Nago") self.assertFalse(self.can_reach_location("Iceberg 5 - Shiro"), str(self.multiworld.seed)) diff --git a/worlds/kdl3/test/test_shuffles.py b/worlds/kdl3/test/test_shuffles.py index d676b641b0..3ba376d068 100644 --- a/worlds/kdl3/test/test_shuffles.py +++ b/worlds/kdl3/test/test_shuffles.py @@ -1,47 +1,61 @@ -from typing import List, Tuple +from typing import List, Tuple, Optional from . import KDL3TestBase -from ..Room import KDL3Room +from ..room import KDL3Room +from ..names import animal_friend_spawns class TestCopyAbilityShuffle(KDL3TestBase): options = { "open_world": False, "goal_speed": "normal", - "total_heart_stars": 30, + "max_heart_stars": 30, "heart_stars_required": 50, "filler_percentage": 0, "copy_ability_randomization": "enabled", } - def test_goal(self): - self.assertBeatable(False) - heart_stars = self.get_items_by_name("Heart Star") - self.collect(heart_stars[0:14]) - self.assertEqual(self.count("Heart Star"), 14, str(self.multiworld.seed)) - self.assertBeatable(False) - self.collect(heart_stars[14:15]) - self.assertEqual(self.count("Heart Star"), 15, str(self.multiworld.seed)) - self.assertBeatable(False) - self.collect_by_name(["Burning", "Cutter", "Kine"]) - self.assertBeatable(True) - self.remove([self.get_item_by_name("Love-Love Rod")]) - self.collect(heart_stars) - self.assertEqual(self.count("Heart Star"), 30, str(self.multiworld.seed)) - self.assertBeatable(True) + def test_goal(self) -> None: + try: + self.assertBeatable(False) + heart_stars = self.get_items_by_name("Heart Star") + self.collect(heart_stars[0:14]) + self.assertEqual(self.count("Heart Star"), 14, str(self.multiworld.seed)) + self.assertBeatable(False) + self.collect(heart_stars[14:15]) + self.assertEqual(self.count("Heart Star"), 15, str(self.multiworld.seed)) + self.assertBeatable(False) + self.collect_by_name(["Burning", "Cutter", "Kine"]) + self.assertBeatable(True) + self.remove([self.get_item_by_name("Love-Love Rod")]) + self.collect(heart_stars) + self.assertEqual(self.count("Heart Star"), 30, str(self.multiworld.seed)) + self.assertBeatable(True) + except AssertionError as ex: + # if assert beatable fails, this will catch and print the seed + raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex - def test_kine(self): - self.collect_by_name(["Cutter", "Burning", "Heart Star"]) - self.assertBeatable(False) + def test_kine(self) -> None: + try: + self.collect_by_name(["Cutter", "Burning", "Heart Star"]) + self.assertBeatable(False) + except AssertionError as ex: + raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex - def test_cutter(self): - self.collect_by_name(["Kine", "Burning", "Heart Star"]) - self.assertBeatable(False) + def test_cutter(self) -> None: + try: + self.collect_by_name(["Kine", "Burning", "Heart Star"]) + self.assertBeatable(False) + except AssertionError as ex: + raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex - def test_burning(self): - self.collect_by_name(["Cutter", "Kine", "Heart Star"]) - self.assertBeatable(False) + def test_burning(self) -> None: + try: + self.collect_by_name(["Cutter", "Kine", "Heart Star"]) + self.assertBeatable(False) + except AssertionError as ex: + raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex - def test_cutter_and_burning_reachable(self): + def test_cutter_and_burning_reachable(self) -> None: rooms = self.multiworld.worlds[1].rooms copy_abilities = self.multiworld.worlds[1].copy_abilities sand_canyon_5 = self.multiworld.get_region("Sand Canyon 5 - 9", 1) @@ -63,7 +77,7 @@ class TestCopyAbilityShuffle(KDL3TestBase): else: self.fail("Could not reach Burning Ability before Iceberg 4!") - def test_valid_abilities_for_ROB(self): + def test_valid_abilities_for_ROB(self) -> None: # there exists a subset of 4-7 abilities that will allow us access to ROB heart star on default settings self.collect_by_name(["Heart Star", "Kine", "Coo"]) # we will guaranteed need Coo, Kine, and Heart Stars to reach # first we need to identify our bukiset requirements @@ -74,13 +88,13 @@ class TestCopyAbilityShuffle(KDL3TestBase): ({"Stone Ability", "Burning Ability"}, {'Bukiset (Stone)', 'Bukiset (Burning)'}), ] copy_abilities = self.multiworld.worlds[1].copy_abilities - required_abilities: List[Tuple[str]] = [] + required_abilities: List[List[str]] = [] for abilities, bukisets in groups: potential_abilities: List[str] = list() for bukiset in bukisets: if copy_abilities[bukiset] in abilities: potential_abilities.append(copy_abilities[bukiset]) - required_abilities.append(tuple(potential_abilities)) + required_abilities.append(potential_abilities) collected_abilities = list() for group in required_abilities: self.assertFalse(len(group) == 0, str(self.multiworld.seed)) @@ -103,91 +117,147 @@ class TestAnimalShuffle(KDL3TestBase): options = { "open_world": False, "goal_speed": "normal", - "total_heart_stars": 30, + "max_heart_stars": 30, "heart_stars_required": 50, "filler_percentage": 0, "animal_randomization": "full", } - def test_goal(self): - self.assertBeatable(False) - heart_stars = self.get_items_by_name("Heart Star") - self.collect(heart_stars[0:14]) - self.assertEqual(self.count("Heart Star"), 14, str(self.multiworld.seed)) - self.assertBeatable(False) - self.collect(heart_stars[14:15]) - self.assertEqual(self.count("Heart Star"), 15, str(self.multiworld.seed)) - self.assertBeatable(False) - self.collect_by_name(["Burning", "Cutter", "Kine"]) - self.assertBeatable(True) - self.remove([self.get_item_by_name("Love-Love Rod")]) - self.collect(heart_stars) - self.assertEqual(self.count("Heart Star"), 30, str(self.multiworld.seed)) - self.assertBeatable(True) + def test_goal(self) -> None: + try: + self.assertBeatable(False) + heart_stars = self.get_items_by_name("Heart Star") + self.collect(heart_stars[0:14]) + self.assertEqual(self.count("Heart Star"), 14, str(self.multiworld.seed)) + self.assertBeatable(False) + self.collect(heart_stars[14:15]) + self.assertEqual(self.count("Heart Star"), 15, str(self.multiworld.seed)) + self.assertBeatable(False) + self.collect_by_name(["Burning", "Cutter", "Kine"]) + self.assertBeatable(True) + self.remove([self.get_item_by_name("Love-Love Rod")]) + self.collect(heart_stars) + self.assertEqual(self.count("Heart Star"), 30, str(self.multiworld.seed)) + self.assertBeatable(True) + except AssertionError as ex: + # if assert beatable fails, this will catch and print the seed + raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex - def test_kine(self): - self.collect_by_name(["Cutter", "Burning", "Heart Star"]) - self.assertBeatable(False) + def test_kine(self) -> None: + try: + self.collect_by_name(["Cutter", "Burning", "Heart Star"]) + self.assertBeatable(False) + except AssertionError as ex: + raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex - def test_cutter(self): - self.collect_by_name(["Kine", "Burning", "Heart Star"]) - self.assertBeatable(False) + def test_cutter(self) -> None: + try: + self.collect_by_name(["Kine", "Burning", "Heart Star"]) + self.assertBeatable(False) + except AssertionError as ex: + raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex - def test_burning(self): - self.collect_by_name(["Cutter", "Kine", "Heart Star"]) - self.assertBeatable(False) + def test_burning(self) -> None: + try: + self.collect_by_name(["Cutter", "Kine", "Heart Star"]) + self.assertBeatable(False) + except AssertionError as ex: + raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex - def test_locked_animals(self): - self.assertTrue(self.multiworld.get_location("Ripple Field 5 - Animal 2", 1).item.name == "Pitch Spawn") - self.assertTrue(self.multiworld.get_location("Iceberg 4 - Animal 1", 1).item.name == "ChuChu Spawn") - self.assertTrue(self.multiworld.get_location("Sand Canyon 6 - Animal 1", 1).item.name in {"Kine Spawn", "Coo Spawn"}) + def test_locked_animals(self) -> None: + ripple_field_5 = self.multiworld.get_location("Ripple Field 5 - Animal 2", 1) + self.assertTrue(ripple_field_5.item is not None and ripple_field_5.item.name == "Pitch Spawn", + f"Multiworld did not place Pitch, Seed: {self.multiworld.seed}") + iceberg_4 = self.multiworld.get_location("Iceberg 4 - Animal 1", 1) + self.assertTrue(iceberg_4.item is not None and iceberg_4.item.name == "ChuChu Spawn", + f"Multiworld did not place ChuChu, Seed: {self.multiworld.seed}") + sand_canyon_6 = self.multiworld.get_location("Sand Canyon 6 - Animal 1", 1) + self.assertTrue(sand_canyon_6.item is not None and sand_canyon_6.item.name in + {"Kine Spawn", "Coo Spawn"}, f"Multiworld did not place Coo/Kine, Seed: {self.multiworld.seed}") + + def test_problematic(self) -> None: + for spawns in animal_friend_spawns.problematic_sets: + placed = [self.multiworld.get_location(spawn, 1).item for spawn in spawns] + placed_names = set([item.name for item in placed]) + self.assertEqual(len(placed), len(placed_names), + f"Duplicate animal placed in problematic locations:" + f" {[spawn.location for spawn in placed]}, " + f"Seed: {self.multiworld.seed}") class TestAllShuffle(KDL3TestBase): options = { "open_world": False, "goal_speed": "normal", - "total_heart_stars": 30, + "max_heart_stars": 30, "heart_stars_required": 50, "filler_percentage": 0, "animal_randomization": "full", "copy_ability_randomization": "enabled", } - def test_goal(self): - self.assertBeatable(False) - heart_stars = self.get_items_by_name("Heart Star") - self.collect(heart_stars[0:14]) - self.assertEqual(self.count("Heart Star"), 14, str(self.multiworld.seed)) - self.assertBeatable(False) - self.collect(heart_stars[14:15]) - self.assertEqual(self.count("Heart Star"), 15, str(self.multiworld.seed)) - self.assertBeatable(False) - self.collect_by_name(["Burning", "Cutter", "Kine"]) - self.assertBeatable(True) - self.remove([self.get_item_by_name("Love-Love Rod")]) - self.collect(heart_stars) - self.assertEqual(self.count("Heart Star"), 30, str(self.multiworld.seed)) - self.assertBeatable(True) + def test_goal(self) -> None: + try: + self.assertBeatable(False) + heart_stars = self.get_items_by_name("Heart Star") + self.collect(heart_stars[0:14]) + self.assertEqual(self.count("Heart Star"), 14, str(self.multiworld.seed)) + self.assertBeatable(False) + self.collect(heart_stars[14:15]) + self.assertEqual(self.count("Heart Star"), 15, str(self.multiworld.seed)) + self.assertBeatable(False) + self.collect_by_name(["Burning", "Cutter", "Kine"]) + self.assertBeatable(True) + self.remove([self.get_item_by_name("Love-Love Rod")]) + self.collect(heart_stars) + self.assertEqual(self.count("Heart Star"), 30, str(self.multiworld.seed)) + self.assertBeatable(True) + except AssertionError as ex: + # if assert beatable fails, this will catch and print the seed + raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex - def test_kine(self): - self.collect_by_name(["Cutter", "Burning", "Heart Star"]) - self.assertBeatable(False) + def test_kine(self) -> None: + try: + self.collect_by_name(["Cutter", "Burning", "Heart Star"]) + self.assertBeatable(False) + except AssertionError as ex: + raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex - def test_cutter(self): - self.collect_by_name(["Kine", "Burning", "Heart Star"]) - self.assertBeatable(False) + def test_cutter(self) -> None: + try: + self.collect_by_name(["Kine", "Burning", "Heart Star"]) + self.assertBeatable(False) + except AssertionError as ex: + raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex - def test_burning(self): - self.collect_by_name(["Cutter", "Kine", "Heart Star"]) - self.assertBeatable(False) + def test_burning(self) -> None: + try: + self.collect_by_name(["Cutter", "Kine", "Heart Star"]) + self.assertBeatable(False) + except AssertionError as ex: + raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex - def test_locked_animals(self): - self.assertTrue(self.multiworld.get_location("Ripple Field 5 - Animal 2", 1).item.name == "Pitch Spawn") - self.assertTrue(self.multiworld.get_location("Iceberg 4 - Animal 1", 1).item.name == "ChuChu Spawn") - self.assertTrue(self.multiworld.get_location("Sand Canyon 6 - Animal 1", 1).item.name in {"Kine Spawn", "Coo Spawn"}) + def test_locked_animals(self) -> None: + ripple_field_5 = self.multiworld.get_location("Ripple Field 5 - Animal 2", 1) + self.assertTrue(ripple_field_5.item is not None and ripple_field_5.item.name == "Pitch Spawn", + f"Multiworld did not place Pitch, Seed: {self.multiworld.seed}") + iceberg_4 = self.multiworld.get_location("Iceberg 4 - Animal 1", 1) + self.assertTrue(iceberg_4.item is not None and iceberg_4.item.name == "ChuChu Spawn", + f"Multiworld did not place ChuChu, Seed: {self.multiworld.seed}") + sand_canyon_6 = self.multiworld.get_location("Sand Canyon 6 - Animal 1", 1) + self.assertTrue(sand_canyon_6.item is not None and sand_canyon_6.item.name in + {"Kine Spawn", "Coo Spawn"}, f"Multiworld did not place Coo/Kine, Seed: {self.multiworld.seed}") - def test_cutter_and_burning_reachable(self): + def test_problematic(self) -> None: + for spawns in animal_friend_spawns.problematic_sets: + placed = [self.multiworld.get_location(spawn, 1).item for spawn in spawns] + placed_names = set([item.name for item in placed]) + self.assertEqual(len(placed), len(placed_names), + f"Duplicate animal placed in problematic locations:" + f" {[spawn.location for spawn in placed]}, " + f"Seed: {self.multiworld.seed}") + + def test_cutter_and_burning_reachable(self) -> None: rooms = self.multiworld.worlds[1].rooms copy_abilities = self.multiworld.worlds[1].copy_abilities sand_canyon_5 = self.multiworld.get_region("Sand Canyon 5 - 9", 1) @@ -209,7 +279,7 @@ class TestAllShuffle(KDL3TestBase): else: self.fail("Could not reach Burning Ability before Iceberg 4!") - def test_valid_abilities_for_ROB(self): + def test_valid_abilities_for_ROB(self) -> None: # there exists a subset of 4-7 abilities that will allow us access to ROB heart star on default settings self.collect_by_name(["Heart Star", "Kine", "Coo"]) # we will guaranteed need Coo, Kine, and Heart Stars to reach # first we need to identify our bukiset requirements @@ -220,13 +290,13 @@ class TestAllShuffle(KDL3TestBase): ({"Stone Ability", "Burning Ability"}, {'Bukiset (Stone)', 'Bukiset (Burning)'}), ] copy_abilities = self.multiworld.worlds[1].copy_abilities - required_abilities: List[Tuple[str]] = [] + required_abilities: List[List[str]] = [] for abilities, bukisets in groups: potential_abilities: List[str] = list() for bukiset in bukisets: if copy_abilities[bukiset] in abilities: potential_abilities.append(copy_abilities[bukiset]) - required_abilities.append(tuple(potential_abilities)) + required_abilities.append(potential_abilities) collected_abilities = list() for group in required_abilities: self.assertFalse(len(group) == 0, str(self.multiworld.seed)) @@ -242,4 +312,4 @@ class TestAllShuffle(KDL3TestBase): self.collect_by_name(["Cutter"]) self.assertTrue(self.can_reach_location("Sand Canyon 6 - Professor Hector & R.O.B"), - ''.join(str(self.multiworld.seed)).join(collected_abilities)) + f"Seed: {self.multiworld.seed}, Collected: {collected_abilities}") From 8ed466bf245e600cefb186fea547960b6b3de31f Mon Sep 17 00:00:00 2001 From: Kory Dondzila Date: Sat, 31 Aug 2024 06:30:42 -0500 Subject: [PATCH 02/13] Shivers: Add collect behavior option. (#3854) * Add collect behavior option. * Add comma Co-authored-by: Scipio Wright --------- Co-authored-by: Scipio Wright --- worlds/shivers/Options.py | 16 ++++++++++++++++ worlds/shivers/__init__.py | 3 ++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/worlds/shivers/Options.py b/worlds/shivers/Options.py index 2f33eb18e5..72791bef3e 100644 --- a/worlds/shivers/Options.py +++ b/worlds/shivers/Options.py @@ -92,6 +92,21 @@ class FullPots(Choice): option_mixed = 2 +class PuzzleCollectBehavior(Choice): + """ + Defines what happens to puzzles on collect. + - Solve None: No puzzles will be solved when collected. + - Prevent Out Of Logic Access: All puzzles, except Red Door and Skull Door, will be solved when collected. + This prevents out of logic access to Gods Room and Slide. + - Solve All: All puzzles will be solved when collected. (original behavior) + """ + display_name = "Puzzle Collect Behavior" + option_solve_none = 0 + option_prevent_out_of_logic_access = 1 + option_solve_all = 2 + default = 1 + + @dataclass class ShiversOptions(PerGameCommonOptions): ixupi_captures_needed: IxupiCapturesNeeded @@ -104,3 +119,4 @@ class ShiversOptions(PerGameCommonOptions): early_lightning: EarlyLightning location_pot_pieces: LocationPotPieces full_pots: FullPots + puzzle_collect_behavior: PuzzleCollectBehavior diff --git a/worlds/shivers/__init__.py b/worlds/shivers/__init__.py index a2d7bc1464..3ca87ae164 100644 --- a/worlds/shivers/__init__.py +++ b/worlds/shivers/__init__.py @@ -219,7 +219,8 @@ class ShiversWorld(World): "ElevatorsStaySolved": self.options.elevators_stay_solved.value, "EarlyBeth": self.options.early_beth.value, "EarlyLightning": self.options.early_lightning.value, - "FrontDoorUsable": self.options.front_door_usable.value + "FrontDoorUsable": self.options.front_door_usable.value, + "PuzzleCollectBehavior": self.options.puzzle_collect_behavior.value, } From f81335d614fdec431564062d9f71b4553a5c9355 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Sat, 31 Aug 2024 04:44:09 -0700 Subject: [PATCH 03/13] DS3: Don't return early in the location loop (#3856) This caused behavior errors when some locations in a group were excluded and others were not. --- worlds/dark_souls_3/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index c31a3681df..f6e5cde615 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -1292,10 +1292,10 @@ class DarkSouls3World(World): locations = location if isinstance(location, list) else [location] for location in locations: data = location_dictionary[location] - if data.dlc and not self.options.enable_dlc: return - if data.ngp and not self.options.enable_ngp: return + if data.dlc and not self.options.enable_dlc: continue + if data.ngp and not self.options.enable_ngp: continue - if not self._is_location_available(location): return + if not self._is_location_available(location): continue if isinstance(rule, str): assert item_dictionary[rule].classification == ItemClassification.progression rule = lambda state, item=rule: state.has(item, self.player) From b37bb60891a9a45838491a621562f8f970e34c55 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Sat, 31 Aug 2024 07:44:48 -0400 Subject: [PATCH 04/13] DS3: Prevent prioritized+excluded locations (#3855) --- worlds/dark_souls_3/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index f6e5cde615..46c7ef1336 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -1252,6 +1252,9 @@ class DarkSouls3World(World): lambda item: not item.advancement ) + # Prevent the player from prioritizing and "excluding" the same location + self.options.priority_locations.value -= allow_useful_locations + if self.options.excluded_location_behavior == "allow_useful": self.options.exclude_locations.value.clear() From 7e0219c214dca799e85b908e0f7a14d5430ca460 Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Sat, 31 Aug 2024 07:49:33 -0400 Subject: [PATCH 05/13] SM and SMZ3 option_definitions deprecation fix (#3372) * - 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 * replaced deprecated use of option_definitions for SM and SMZ3 by options_dataclass * fixed missed references to option_definitions * Update worlds/sm/variaRandomizer/utils/utils.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * fixed conflicts and made SMZ3 accessibility related code more future proof * Update worlds/smz3/Options.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/smz3/__init__.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/sm/Options.py | 121 ++++++++++++----------- worlds/sm/__init__.py | 38 ++++--- worlds/sm/variaRandomizer/randomizer.py | 14 +-- worlds/sm/variaRandomizer/utils/utils.py | 72 +++++++------- worlds/smz3/Options.py | 36 +++---- worlds/smz3/__init__.py | 40 ++++---- 6 files changed, 161 insertions(+), 160 deletions(-) diff --git a/worlds/sm/Options.py b/worlds/sm/Options.py index 223179529c..3dad16ad3a 100644 --- a/worlds/sm/Options.py +++ b/worlds/sm/Options.py @@ -1,6 +1,7 @@ import typing -from Options import Choice, Range, OptionDict, OptionList, OptionSet, Option, Toggle, DefaultOnToggle +from Options import Choice, PerGameCommonOptions, Range, OptionDict, OptionList, OptionSet, Option, Toggle, DefaultOnToggle from .variaRandomizer.utils.objectives import _goals +from dataclasses import dataclass class StartItemsRemovesFromPool(Toggle): """Remove items in starting inventory from pool.""" @@ -372,62 +373,62 @@ class RelaxedRoundRobinCF(Toggle): """ display_name = "Relaxed round robin Crystal Flash" -sm_options: typing.Dict[str, type(Option)] = { - "start_inventory_removes_from_pool": StartItemsRemovesFromPool, - "preset": Preset, - "start_location": StartLocation, - "remote_items": RemoteItems, - "death_link": DeathLink, - #"majors_split": "Full", - #"scav_num_locs": "10", - #"scav_randomized": "off", - #"scav_escape": "off", - "max_difficulty": MaxDifficulty, - #"progression_speed": "medium", - #"progression_difficulty": "normal", - "morph_placement": MorphPlacement, - #"suits_restriction": SuitsRestriction, - "hide_items": HideItems, - "strict_minors": StrictMinors, - "missile_qty": MissileQty, - "super_qty": SuperQty, - "power_bomb_qty": PowerBombQty, - "minor_qty": MinorQty, - "energy_qty": EnergyQty, - "area_randomization": AreaRandomization, - "area_layout": AreaLayout, - "doors_colors_rando": DoorsColorsRando, - "allow_grey_doors": AllowGreyDoors, - "boss_randomization": BossRandomization, - #"minimizer": "off", - #"minimizer_qty": "45", - #"minimizer_tourian": "off", - "escape_rando": EscapeRando, - "remove_escape_enemies": RemoveEscapeEnemies, - "fun_combat": FunCombat, - "fun_movement": FunMovement, - "fun_suits": FunSuits, - "layout_patches": LayoutPatches, - "varia_tweaks": VariaTweaks, - "nerfed_charge": NerfedCharge, - "gravity_behaviour": GravityBehaviour, - #"item_sounds": "on", - "elevators_speed": ElevatorsSpeed, - "fast_doors": DoorsSpeed, - "spin_jump_restart": SpinJumpRestart, - "rando_speed": SpeedKeep, - "infinite_space_jump": InfiniteSpaceJump, - "refill_before_save": RefillBeforeSave, - "hud": Hud, - "animals": Animals, - "no_music": NoMusic, - "random_music": RandomMusic, - "custom_preset": CustomPreset, - "varia_custom_preset": VariaCustomPreset, - "tourian": Tourian, - "custom_objective": CustomObjective, - "custom_objective_list": CustomObjectiveList, - "custom_objective_count": CustomObjectiveCount, - "objective": Objective, - "relaxed_round_robin_cf": RelaxedRoundRobinCF, - } +@dataclass +class SMOptions(PerGameCommonOptions): + start_inventory_removes_from_pool: StartItemsRemovesFromPool + preset: Preset + start_location: StartLocation + remote_items: RemoteItems + death_link: DeathLink + #majors_split: "Full" + #scav_num_locs: "10" + #scav_randomized: "off" + #scav_escape: "off" + max_difficulty: MaxDifficulty + #progression_speed": "medium" + #progression_difficulty": "normal" + morph_placement: MorphPlacement + #suits_restriction": SuitsRestriction + hide_items: HideItems + strict_minors: StrictMinors + missile_qty: MissileQty + super_qty: SuperQty + power_bomb_qty: PowerBombQty + minor_qty: MinorQty + energy_qty: EnergyQty + area_randomization: AreaRandomization + area_layout: AreaLayout + doors_colors_rando: DoorsColorsRando + allow_grey_doors: AllowGreyDoors + boss_randomization: BossRandomization + #minimizer: "off" + #minimizer_qty: "45" + #minimizer_tourian: "off" + escape_rando: EscapeRando + remove_escape_enemies: RemoveEscapeEnemies + fun_combat: FunCombat + fun_movement: FunMovement + fun_suits: FunSuits + layout_patches: LayoutPatches + varia_tweaks: VariaTweaks + nerfed_charge: NerfedCharge + gravity_behaviour: GravityBehaviour + #item_sounds: "on" + elevators_speed: ElevatorsSpeed + fast_doors: DoorsSpeed + spin_jump_restart: SpinJumpRestart + rando_speed: SpeedKeep + infinite_space_jump: InfiniteSpaceJump + refill_before_save: RefillBeforeSave + hud: Hud + animals: Animals + no_music: NoMusic + random_music: RandomMusic + custom_preset: CustomPreset + varia_custom_preset: VariaCustomPreset + tourian: Tourian + custom_objective: CustomObjective + custom_objective_list: CustomObjectiveList + custom_objective_count: CustomObjectiveCount + objective: Objective + relaxed_round_robin_cf: RelaxedRoundRobinCF diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 826b144779..bf9d6d087e 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -15,7 +15,7 @@ from worlds.generic.Rules import add_rule, set_rule logger = logging.getLogger("Super Metroid") -from .Options import sm_options +from .Options import SMOptions from .Client import SMSNIClient from .Rom import get_base_rom_path, SM_ROM_MAX_PLAYERID, SM_ROM_PLAYERDATA_COUNT, SMDeltaPatch, get_sm_symbols import Utils @@ -96,10 +96,11 @@ class SMWorld(World): a wide range of options to randomize Item locations, required skills and even the connections between the main Areas! """ - game: str = "Super Metroid" topology_present = True - option_definitions = sm_options + options_dataclass = SMOptions + options: SMOptions + settings: typing.ClassVar[SMSettings] item_name_to_id = {value.Name: items_start_id + value.Id for key, value in ItemManager.Items.items() if value.Id != None} @@ -129,27 +130,27 @@ class SMWorld(World): Logic.factory('vanilla') dummy_rom_file = Utils.user_path(SMSettings.RomFile.copy_to) # actual rom set in generate_output - self.variaRando = VariaRandomizer(self.multiworld, dummy_rom_file, self.player) + self.variaRando = VariaRandomizer(self.options, dummy_rom_file, self.player) self.multiworld.state.smbm[self.player] = SMBoolManager(self.player, self.variaRando.maxDifficulty) # keeps Nothing items local so no player will ever pickup Nothing # doing so reduces contribution of this world to the Multiworld the more Nothing there is though - self.multiworld.local_items[self.player].value.add('Nothing') - self.multiworld.local_items[self.player].value.add('No Energy') + self.options.local_items.value.add('Nothing') + self.options.local_items.value.add('No Energy') if (self.variaRando.args.morphPlacement == "early"): self.multiworld.local_early_items[self.player]['Morph Ball'] = 1 - self.remote_items = self.multiworld.remote_items[self.player] + self.remote_items = self.options.remote_items if (len(self.variaRando.randoExec.setup.restrictedLocs) > 0): - self.multiworld.accessibility[self.player].value = Accessibility.option_minimal + self.options.accessibility.value = Accessibility.option_minimal logger.warning(f"accessibility forced to 'minimal' for player {self.multiworld.get_player_name(self.player)} because of 'fun' settings") def create_items(self): itemPool = self.variaRando.container.itemPool self.startItems = [variaItem for item in self.multiworld.precollected_items[self.player] for variaItem in ItemManager.Items.values() if variaItem.Name == item.name] - if self.multiworld.start_inventory_removes_from_pool[self.player]: + if self.options.start_inventory_removes_from_pool: for item in self.startItems: if (item in itemPool): itemPool.remove(item) @@ -317,10 +318,10 @@ class SMWorld(World): player=self.player) def get_filler_item_name(self) -> str: - if self.multiworld.random.randint(0, 100) < self.multiworld.minor_qty[self.player].value: - power_bombs = self.multiworld.power_bomb_qty[self.player].value - missiles = self.multiworld.missile_qty[self.player].value - super_missiles = self.multiworld.super_qty[self.player].value + if self.multiworld.random.randint(0, 100) < self.options.minor_qty.value: + power_bombs = self.options.power_bomb_qty.value + missiles = self.options.missile_qty.value + super_missiles = self.options.super_qty.value roll = self.multiworld.random.randint(1, power_bombs + missiles + super_missiles) if roll <= power_bombs: return "Power Bomb" @@ -633,7 +634,7 @@ class SMWorld(World): deathLink: List[ByteEdit] = [{ "sym": symbols["config_deathlink"], "offset": 0, - "values": [self.multiworld.death_link[self.player].value] + "values": [self.options.death_link.value] }] remoteItem: List[ByteEdit] = [{ "sym": symbols["config_remote_items"], @@ -859,10 +860,7 @@ class SMWorld(World): def fill_slot_data(self): slot_data = {} if not self.multiworld.is_race: - for option_name in self.option_definitions: - option = getattr(self.multiworld, option_name)[self.player] - slot_data[option_name] = option.value - + slot_data = self.options.as_dict(*self.options_dataclass.type_hints) slot_data["Preset"] = { "Knows": {}, "Settings": {"hardRooms": Settings.SettingsDict[self.player].hardRooms, "bossesDifficulty": Settings.SettingsDict[self.player].bossesDifficulty, @@ -887,14 +885,14 @@ class SMWorld(World): return slot_data def write_spoiler(self, spoiler_handle: TextIO): - if self.multiworld.area_randomization[self.player].value != 0: + if self.options.area_randomization.value != 0: spoiler_handle.write('\n\nArea Transitions:\n\n') spoiler_handle.write('\n'.join(['%s%s %s %s' % (f'{self.multiworld.get_player_name(self.player)}: ' if self.multiworld.players > 1 else '', src.Name, '<=>', dest.Name) for src, dest in self.variaRando.randoExec.areaGraph.InterAreaTransitions if not src.Boss])) - if self.multiworld.boss_randomization[self.player].value != 0: + if self.options.boss_randomization.value != 0: spoiler_handle.write('\n\nBoss Transitions:\n\n') spoiler_handle.write('\n'.join(['%s%s %s %s' % (f'{self.multiworld.get_player_name(self.player)}: ' if self.multiworld.players > 1 else '', src.Name, diff --git a/worlds/sm/variaRandomizer/randomizer.py b/worlds/sm/variaRandomizer/randomizer.py index dab078598e..8a7a2ea0e2 100644 --- a/worlds/sm/variaRandomizer/randomizer.py +++ b/worlds/sm/variaRandomizer/randomizer.py @@ -250,13 +250,13 @@ class VariaRandomizer: parser.add_argument('--tourianList', help="list to choose from when random", dest='tourianList', nargs='?', default=None) - def __init__(self, world, rom, player): + def __init__(self, options, rom, player): # parse args self.args = copy.deepcopy(VariaRandomizer.parser.parse_args(["--logic", "varia"])) #dummy custom args to skip parsing _sys.argv while still get default values self.player = player args = self.args args.rom = rom - # args.startLocation = to_pascal_case_with_space(world.startLocation[player].current_key) + # args.startLocation = to_pascal_case_with_space(options.startLocation.current_key) if args.output is None and args.rom is None: raise Exception("Need --output or --rom parameter") @@ -288,7 +288,7 @@ class VariaRandomizer: # print(msg) # optErrMsgs.append(msg) - preset = loadRandoPreset(world, self.player, args) + preset = loadRandoPreset(options, args) # use the skill preset from the rando preset if preset is not None and preset != 'custom' and preset != 'varia_custom' and args.paramsFileName is None: args.paramsFileName = "/".join((appDir, getPresetDir(preset), preset+".json")) @@ -302,12 +302,12 @@ class VariaRandomizer: preset = args.preset else: if preset == 'custom': - PresetLoader.factory(world.custom_preset[player].value).load(self.player) + PresetLoader.factory(options.custom_preset.value).load(self.player) elif preset == 'varia_custom': - if len(world.varia_custom_preset[player].value) == 0: + if len(options.varia_custom_preset.value) == 0: raise Exception("varia_custom was chosen but varia_custom_preset is missing.") url = 'https://randommetroidsolver.pythonanywhere.com/presetWebService' - preset_name = next(iter(world.varia_custom_preset[player].value)) + preset_name = next(iter(options.varia_custom_preset.value)) payload = '{{"preset": "{}"}}'.format(preset_name) headers = {'content-type': 'application/json', 'Accept-Charset': 'UTF-8'} response = requests.post(url, data=payload, headers=headers) @@ -463,7 +463,7 @@ class VariaRandomizer: args.startLocation = random.choice(possibleStartAPs) elif args.startLocation not in possibleStartAPs: args.startLocation = 'Landing Site' - world.start_location[player] = StartLocation(StartLocation.default) + options.start_location = StartLocation(StartLocation.default) #optErrMsgs.append('Invalid start location: {}. {}'.format(args.startLocation, reasons[args.startLocation])) #optErrMsgs.append('Possible start locations with these settings: {}'.format(possibleStartAPs)) #dumpErrorMsgs(args.output, optErrMsgs) diff --git a/worlds/sm/variaRandomizer/utils/utils.py b/worlds/sm/variaRandomizer/utils/utils.py index 01029f2f60..f7d699b665 100644 --- a/worlds/sm/variaRandomizer/utils/utils.py +++ b/worlds/sm/variaRandomizer/utils/utils.py @@ -358,35 +358,35 @@ def convertParam(randoParams, param, inverse=False): return "random" raise Exception("invalid value for parameter {}".format(param)) -def loadRandoPreset(world, player, args): +def loadRandoPreset(options, args): defaultMultiValues = getDefaultMultiValues() diffs = ["easy", "medium", "hard", "harder", "hardcore", "mania", "infinity"] presetValues = getPresetValues() - args.animals = world.animals[player].value - args.noVariaTweaks = not world.varia_tweaks[player].value - args.maxDifficulty = diffs[world.max_difficulty[player].value] - #args.suitsRestriction = world.suits_restriction[player].value - args.hideItems = world.hide_items[player].value - args.strictMinors = world.strict_minors[player].value - args.noLayout = not world.layout_patches[player].value - args.gravityBehaviour = defaultMultiValues["gravityBehaviour"][world.gravity_behaviour[player].value] - args.nerfedCharge = world.nerfed_charge[player].value - args.area = world.area_randomization[player].current_key + args.animals = options.animals.value + args.noVariaTweaks = not options.varia_tweaks.value + args.maxDifficulty = diffs[options.max_difficulty.value] + #args.suitsRestriction = options.suits_restriction.value + args.hideItems = options.hide_items.value + args.strictMinors = options.strict_minors.value + args.noLayout = not options.layout_patches.value + args.gravityBehaviour = defaultMultiValues["gravityBehaviour"][options.gravity_behaviour.value] + args.nerfedCharge = options.nerfed_charge.value + args.area = options.area_randomization.current_key if (args.area == "true"): args.area = "full" if args.area != "off": - args.areaLayoutBase = not world.area_layout[player].value - args.escapeRando = world.escape_rando[player].value - args.noRemoveEscapeEnemies = not world.remove_escape_enemies[player].value - args.doorsColorsRando = world.doors_colors_rando[player].value - args.allowGreyDoors = world.allow_grey_doors[player].value - args.bosses = world.boss_randomization[player].value - if world.fun_combat[player].value: + args.areaLayoutBase = not options.area_layout.value + args.escapeRando = options.escape_rando.value + args.noRemoveEscapeEnemies = not options.remove_escape_enemies.value + args.doorsColorsRando = options.doors_colors_rando.value + args.allowGreyDoors = options.allow_grey_doors.value + args.bosses = options.boss_randomization.value + if options.fun_combat.value: args.superFun.append("Combat") - if world.fun_movement[player].value: + if options.fun_movement.value: args.superFun.append("Movement") - if world.fun_suits[player].value: + if options.fun_suits.value: args.superFun.append("Suits") ipsPatches = { "spin_jump_restart":"spinjumprestart", @@ -396,36 +396,36 @@ def loadRandoPreset(world, player, args): "refill_before_save":"refill_before_save", "relaxed_round_robin_cf":"relaxed_round_robin_cf"} for settingName, patchName in ipsPatches.items(): - if hasattr(world, settingName) and getattr(world, settingName)[player].value: + if hasattr(options, settingName) and getattr(options, settingName).value: args.patches.append(patchName + '.ips') patches = {"no_music":"No_Music", "infinite_space_jump":"Infinite_Space_Jump"} for settingName, patchName in patches.items(): - if hasattr(world, settingName) and getattr(world, settingName)[player].value: + if hasattr(options, settingName) and getattr(options, settingName).value: args.patches.append(patchName) - args.hud = world.hud[player].value - args.morphPlacement = defaultMultiValues["morphPlacement"][world.morph_placement[player].value] + args.hud = options.hud.value + args.morphPlacement = defaultMultiValues["morphPlacement"][options.morph_placement.value] #args.majorsSplit #args.scavNumLocs #args.scavRandomized - args.startLocation = defaultMultiValues["startLocation"][world.start_location[player].value] + args.startLocation = defaultMultiValues["startLocation"][options.start_location.value] #args.progressionDifficulty #args.progressionSpeed - args.missileQty = world.missile_qty[player].value / float(10) - args.superQty = world.super_qty[player].value / float(10) - args.powerBombQty = world.power_bomb_qty[player].value / float(10) - args.minorQty = world.minor_qty[player].value - args.energyQty = defaultMultiValues["energyQty"][world.energy_qty[player].value] - args.objectiveRandom = world.custom_objective[player].value - args.objectiveList = list(world.custom_objective_list[player].value) - args.nbObjective = world.custom_objective_count[player].value - args.objective = list(world.objective[player].value) - args.tourian = defaultMultiValues["tourian"][world.tourian[player].value] + args.missileQty = options.missile_qty.value / float(10) + args.superQty = options.super_qty.value / float(10) + args.powerBombQty = options.power_bomb_qty.value / float(10) + args.minorQty = options.minor_qty.value + args.energyQty = defaultMultiValues["energyQty"][options.energy_qty.value] + args.objectiveRandom = options.custom_objective.value + args.objectiveList = list(options.custom_objective_list.value) + args.nbObjective = options.custom_objective_count.value + args.objective = list(options.objective.value) + args.tourian = defaultMultiValues["tourian"][options.tourian.value] #args.minimizerN #args.minimizerTourian - return presetValues[world.preset[player].value] + return presetValues[options.preset.value] def getRandomizerDefaultParameters(): defaultParams = {} diff --git a/worlds/smz3/Options.py b/worlds/smz3/Options.py index 8c5efc431f..7df01f8710 100644 --- a/worlds/smz3/Options.py +++ b/worlds/smz3/Options.py @@ -1,5 +1,6 @@ import typing -from Options import Choice, Option, Toggle, DefaultOnToggle, Range, ItemsAccessibility +from Options import Choice, Option, PerGameCommonOptions, Toggle, DefaultOnToggle, Range, ItemsAccessibility +from dataclasses import dataclass class SMLogic(Choice): """This option selects what kind of logic to use for item placement inside @@ -126,20 +127,19 @@ class EnergyBeep(DefaultOnToggle): """Toggles the low health energy beep in Super Metroid.""" display_name = "Energy Beep" - -smz3_options: typing.Dict[str, type(Option)] = { - "accessibility": ItemsAccessibility, - "sm_logic": SMLogic, - "sword_location": SwordLocation, - "morph_location": MorphLocation, - "goal": Goal, - "key_shuffle": KeyShuffle, - "open_tower": OpenTower, - "ganon_vulnerable": GanonVulnerable, - "open_tourian": OpenTourian, - "spin_jumps_animation": SpinJumpsAnimation, - "heart_beep_speed": HeartBeepSpeed, - "heart_color": HeartColor, - "quick_swap": QuickSwap, - "energy_beep": EnergyBeep - } +@dataclass +class SMZ3Options(PerGameCommonOptions): + accessibility: ItemsAccessibility + sm_logic: SMLogic + sword_location: SwordLocation + morph_location: MorphLocation + goal: Goal + key_shuffle: KeyShuffle + open_tower: OpenTower + ganon_vulnerable: GanonVulnerable + open_tourian: OpenTourian + spin_jumps_animation: SpinJumpsAnimation + heart_beep_speed: HeartBeepSpeed + heart_color: HeartColor + quick_swap: QuickSwap + energy_beep: EnergyBeep diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index 690e5172a2..5e6a6ac609 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -22,8 +22,8 @@ from worlds.AutoWorld import World, AutoLogicRegister, WebWorld from .Client import SMZ3SNIClient from .Rom import get_base_rom_bytes, SMZ3DeltaPatch from .ips import IPS_Patch -from .Options import smz3_options -from Options import Accessibility +from .Options import SMZ3Options +from Options import Accessibility, ItemsAccessibility world_folder = os.path.dirname(__file__) logger = logging.getLogger("SMZ3") @@ -68,7 +68,9 @@ class SMZ3World(World): """ game: str = "SMZ3" topology_present = False - option_definitions = smz3_options + options_dataclass = SMZ3Options + options: SMZ3Options + item_names: Set[str] = frozenset(TotalSMZ3Item.lookup_name_to_id) location_names: Set[str] item_name_to_id = TotalSMZ3Item.lookup_name_to_id @@ -189,14 +191,14 @@ class SMZ3World(World): self.config = Config() self.config.GameMode = GameMode.Multiworld self.config.Z3Logic = Z3Logic.Normal - self.config.SMLogic = SMLogic(self.multiworld.sm_logic[self.player].value) - self.config.SwordLocation = SwordLocation(self.multiworld.sword_location[self.player].value) - self.config.MorphLocation = MorphLocation(self.multiworld.morph_location[self.player].value) - self.config.Goal = Goal(self.multiworld.goal[self.player].value) - self.config.KeyShuffle = KeyShuffle(self.multiworld.key_shuffle[self.player].value) - self.config.OpenTower = OpenTower(self.multiworld.open_tower[self.player].value) - self.config.GanonVulnerable = GanonVulnerable(self.multiworld.ganon_vulnerable[self.player].value) - self.config.OpenTourian = OpenTourian(self.multiworld.open_tourian[self.player].value) + self.config.SMLogic = SMLogic(self.options.sm_logic.value) + self.config.SwordLocation = SwordLocation(self.options.sword_location.value) + self.config.MorphLocation = MorphLocation(self.options.morph_location.value) + self.config.Goal = Goal(self.options.goal.value) + self.config.KeyShuffle = KeyShuffle(self.options.key_shuffle.value) + self.config.OpenTower = OpenTower(self.options.open_tower.value) + self.config.GanonVulnerable = GanonVulnerable(self.options.ganon_vulnerable.value) + self.config.OpenTourian = OpenTourian(self.options.open_tourian.value) self.local_random = random.Random(self.multiworld.random.randint(0, 1000)) self.smz3World = TotalSMZ3World(self.config, self.multiworld.get_player_name(self.player), self.player, self.multiworld.seed_name) @@ -222,7 +224,7 @@ class SMZ3World(World): else: progressionItems = self.progression # Dungeons items here are not in the itempool and will be prefilled locally so they must stay local - self.multiworld.non_local_items[self.player].value -= frozenset(item_name for item_name in self.item_names if TotalSMZ3Item.Item.IsNameDungeonItem(item_name)) + self.options.non_local_items.value -= frozenset(item_name for item_name in self.item_names if TotalSMZ3Item.Item.IsNameDungeonItem(item_name)) for item in self.keyCardsItems: self.multiworld.push_precollected(SMZ3Item(item.Type.name, ItemClassification.filler, item.Type, self.item_name_to_id[item.Type.name], self.player, item)) @@ -244,7 +246,7 @@ class SMZ3World(World): set_rule(entrance, lambda state, region=region: region.CanEnter(state.smz3state[self.player])) for loc in region.Locations: l = self.locations[loc.Name] - if self.multiworld.accessibility[self.player] != 'full': + if self.options.accessibility.value != ItemsAccessibility.option_full: l.always_allow = lambda state, item, loc=loc: \ item.game == "SMZ3" and \ loc.alwaysAllow(item.item, state.smz3state[self.player]) @@ -405,12 +407,12 @@ class SMZ3World(World): patch = {} # smSpinjumps - if (self.multiworld.spin_jumps_animation[self.player].value == 1): + if (self.options.spin_jumps_animation.value == 1): patch[self.SnesCustomization(0x9B93FE)] = bytearray([0x01]) # z3HeartBeep values = [ 0x00, 0x80, 0x40, 0x20, 0x10] - index = self.multiworld.heart_beep_speed[self.player].value + index = self.options.heart_beep_speed.value patch[0x400033] = bytearray([values[index if index < len(values) else 2]]) # z3HeartColor @@ -420,17 +422,17 @@ class SMZ3World(World): [0x2C, [0xC9, 0x69]], [0x28, [0xBC, 0x02]] ] - index = self.multiworld.heart_color[self.player].value + index = self.options.heart_color.value (hud, fileSelect) = values[index if index < len(values) else 0] for i in range(0, 20, 2): patch[self.SnesCustomization(0xDFA1E + i)] = bytearray([hud]) patch[self.SnesCustomization(0x1BD6AA)] = bytearray(fileSelect) # z3QuickSwap - patch[0x40004B] = bytearray([0x01 if self.multiworld.quick_swap[self.player].value else 0x00]) + patch[0x40004B] = bytearray([0x01 if self.options.quick_swap.value else 0x00]) # smEnergyBeepOff - if (self.multiworld.energy_beep[self.player].value == 0): + if (self.options.energy_beep.value == 0): for ([addr, value]) in [ [0x90EA9B, 0x80], [0x90F337, 0x80], @@ -551,7 +553,7 @@ class SMZ3World(World): # some small or big keys (those always_allow) can be unreachable in-game # while logic still collects some of them (probably to simulate the player collecting pot keys in the logic), some others don't # so we need to remove those exceptions as progression items - if self.multiworld.accessibility[self.player] == 'items': + if self.options.accessibility.value == ItemsAccessibility.option_items: state = CollectionState(self.multiworld) locs = [self.multiworld.get_location("Swamp Palace - Big Chest", self.player), self.multiworld.get_location("Skull Woods - Big Chest", self.player), From 8a809be67a02ac44ebdbe748869585e703e40f6f Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Sat, 31 Aug 2024 21:57:43 +0300 Subject: [PATCH 06/13] Stardew Valley - Prize Ticket and Mystery Box grinding requires the abilty to redeem them #3728 --- worlds/stardew_valley/logic/grind_logic.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/worlds/stardew_valley/logic/grind_logic.py b/worlds/stardew_valley/logic/grind_logic.py index ccd8c5dacc..e0ac84639d 100644 --- a/worlds/stardew_valley/logic/grind_logic.py +++ b/worlds/stardew_valley/logic/grind_logic.py @@ -5,6 +5,7 @@ from .base_logic import BaseLogic, BaseLogicMixin from .book_logic import BookLogicMixin from .has_logic import HasLogicMixin from .received_logic import ReceivedLogicMixin +from .region_logic import RegionLogicMixin from .time_logic import TimeLogicMixin from ..options import Booksanity from ..stardew_rule import StardewRule, HasProgressionPercent @@ -13,6 +14,7 @@ from ..strings.craftable_names import Consumable from ..strings.currency_names import Currency from ..strings.fish_names import WaterChest from ..strings.geode_names import Geode +from ..strings.region_names import Region from ..strings.tool_names import Tool if TYPE_CHECKING: @@ -31,26 +33,28 @@ class GrindLogicMixin(BaseLogicMixin): self.grind = GrindLogic(*args, **kwargs) -class GrindLogic(BaseLogic[Union[GrindLogicMixin, HasLogicMixin, ReceivedLogicMixin, BookLogicMixin, TimeLogicMixin, ToolLogicMixin]]): +class GrindLogic(BaseLogic[Union[GrindLogicMixin, HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, BookLogicMixin, TimeLogicMixin, ToolLogicMixin]]): def can_grind_mystery_boxes(self, quantity: int) -> StardewRule: + opening_rule = self.logic.region.can_reach(Region.blacksmith) mystery_box_rule = self.logic.has(Consumable.mystery_box) book_of_mysteries_rule = self.logic.true_ \ if self.options.booksanity == Booksanity.option_none \ else self.logic.book.has_book_power(Book.book_of_mysteries) # Assuming one box per day, but halved because we don't know how many months have passed before Mr. Qi's Plane Ride. time_rule = self.logic.time.has_lived_months(quantity // 14) - return self.logic.and_(mystery_box_rule, - book_of_mysteries_rule, - time_rule) + return self.logic.and_(opening_rule, mystery_box_rule, + book_of_mysteries_rule, time_rule,) def can_grind_artifact_troves(self, quantity: int) -> StardewRule: - return self.logic.and_(self.logic.has(Geode.artifact_trove), + opening_rule = self.logic.region.can_reach(Region.blacksmith) + return self.logic.and_(opening_rule, self.logic.has(Geode.artifact_trove), # Assuming one per month if the player does not grind it. self.logic.time.has_lived_months(quantity)) def can_grind_prize_tickets(self, quantity: int) -> StardewRule: - return self.logic.and_(self.logic.has(Currency.prize_ticket), + claiming_rule = self.logic.region.can_reach(Region.mayor_house) + return self.logic.and_(claiming_rule, self.logic.has(Currency.prize_ticket), # Assuming two per month if the player does not grind it. self.logic.time.has_lived_months(quantity // 2)) From 499dad53b1a3943019f1bf57897e48edf563150e Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Sat, 31 Aug 2024 20:00:19 +0100 Subject: [PATCH 07/13] AHIT: Fix thug shops having 0 items after the first shop rolls 0 items (#3799) Once a thug shop rolled 0 as the number of items it should have, all remaining iterations would do nothing because neither the `count == -1` condition nor the `count >= 1` condition would be met. This caused all remaining thug shops to have zero items. This also caused the item counts of remaining thug shops to be absent from slot data, which was how this issue was found. I found the old code confusing and, rather than try to figure out how to fix it, I opted to rewrite it. With the new code, a local variable dictionary tracks the number of created locations for each thug and no more locations are created for a thug once their number of locations equals the number of shop items that thug rolled. --- worlds/ahit/Regions.py | 49 +++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 8cb3782bde..c70f08b475 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -968,40 +968,35 @@ def get_act_by_number(world: "HatInTimeWorld", chapter_name: str, num: int) -> R def create_thug_shops(world: "HatInTimeWorld"): min_items: int = world.options.NyakuzaThugMinShopItems.value max_items: int = world.options.NyakuzaThugMaxShopItems.value - count = -1 - step = 0 - old_name = "" + + thug_location_counts: Dict[str, int] = {} for key, data in shop_locations.items(): - if data.nyakuza_thug == "": + thug_name = data.nyakuza_thug + if thug_name == "": + # Different shop type. continue - if old_name != "" and old_name == data.nyakuza_thug: + if thug_name not in world.nyakuza_thug_items: + shop_item_count = world.random.randint(min_items, max_items) + world.nyakuza_thug_items[thug_name] = shop_item_count + else: + shop_item_count = world.nyakuza_thug_items[thug_name] + + if shop_item_count <= 0: continue - try: - if world.nyakuza_thug_items[data.nyakuza_thug] <= 0: - continue - except KeyError: - pass + location_count = thug_location_counts.setdefault(thug_name, 0) + if location_count >= shop_item_count: + # Already created all the locations for this thug. + continue - if count == -1: - count = world.random.randint(min_items, max_items) - world.nyakuza_thug_items.setdefault(data.nyakuza_thug, count) - if count <= 0: - continue - - if count >= 1: - region = world.multiworld.get_region(data.region, world.player) - loc = HatInTimeLocation(world.player, key, data.id, region) - region.locations.append(loc) - world.shop_locs.append(loc.name) - - step += 1 - if step >= count: - old_name = data.nyakuza_thug - step = 0 - count = -1 + # Create the shop location. + region = world.multiworld.get_region(data.region, world.player) + loc = HatInTimeLocation(world.player, key, data.id, region) + region.locations.append(loc) + world.shop_locs.append(loc.name) + thug_location_counts[thug_name] = location_count + 1 def create_events(world: "HatInTimeWorld") -> int: From fc8462f4e9f782bd123ce6123efab91c6850228a Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Sat, 31 Aug 2024 22:51:41 +0200 Subject: [PATCH 08/13] The Witness: Add Beginner Mode option preset #3691 --- worlds/witness/presets.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/worlds/witness/presets.py b/worlds/witness/presets.py index 105514c91e..8993048065 100644 --- a/worlds/witness/presets.py +++ b/worlds/witness/presets.py @@ -3,6 +3,13 @@ from typing import Any, Dict from .options import * witness_option_presets: Dict[str, Dict[str, Any]] = { + # Best for beginners. This is just default options, but with a much easier goal that skips the Mountain puzzles. + "Beginner Mode": { + "victory_condition": VictoryCondition.option_mountain_box_short, + + "puzzle_skip_amount": 15, + }, + # Great for short syncs & scratching that "speedrun with light routing elements" itch. "Short & Dense": { "progression_balancing": 30, From 456b4adaa177ed13fda00b66b93cec1ef9c7333f Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Sat, 31 Aug 2024 17:36:29 -0400 Subject: [PATCH 09/13] ALttP/Docs: Correcting the plando docs (#3835) * Correcting some text * Reword sentence --- worlds/alttp/docs/plando_en.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/worlds/alttp/docs/plando_en.md b/worlds/alttp/docs/plando_en.md index af8cbfe1b0..13224cb4d5 100644 --- a/worlds/alttp/docs/plando_en.md +++ b/worlds/alttp/docs/plando_en.md @@ -2,8 +2,8 @@ ## Configuration -1. Plando features have to be enabled first, before they can be used (opt-in). -2. To do so, go to your installation directory (Windows default: `C:\ProgramData\Archipelago`), then open the host.yaml +1. All plando options are enabled by default, except for "items plando" which has to be enabled before it can be used (opt-in). +2. To enable it, go to your installation directory (Windows default: `C:\ProgramData\Archipelago`), then open the host.yaml file with a text editor. 3. In it, you're looking for the option key `plando_options`. To enable all plando modules you can set the value to `bosses, items, texts, connections` @@ -66,6 +66,7 @@ boss_shuffle: - ignored if only one world is generated - can be a number, to target that slot in the multiworld - can be a name, to target that player's world + - can be a list of names, to target those players' worlds - can be true, to target any other player's world - can be false, to target own world and is the default - can be null, to target a random world @@ -132,17 +133,15 @@ plando_items: ### Texts -- This module is disabled by default. - Has the options `text`, `at`, and `percentage` +- All of these options support subweights - percentage is the percentage chance for this text to be placed, can be omitted entirely for 100% - text is the text to be placed. - - can be weighted. - `\n` is a newline. - `@` is the entered player's name. - Warning: Text Mapper does not support full unicode. - [Alphabet](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Text.py#L758) - at is the location within the game to attach the text to. - - can be weighted. - [List of targets](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Text.py#L1499) #### Example @@ -162,7 +161,6 @@ and `uncle_dying_sewer`, then places the text "This is a plando. You've been war ### Connections -- This module is disabled by default. - Has the options `percentage`, `entrance`, `exit` and `direction`. - All options support subweights - percentage is the percentage chance for this to be connected, can be omitted entirely for 100% From 34a3b5f058766c650499eb48c2eced7e06c14c9b Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Sat, 31 Aug 2024 17:37:18 -0400 Subject: [PATCH 10/13] TUNIC: Add alias for Ladders in Overworld Town #3862 --- worlds/tunic/items.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/tunic/items.py b/worlds/tunic/items.py index 3e7f2c1a43..e0ee17831a 100644 --- a/worlds/tunic/items.py +++ b/worlds/tunic/items.py @@ -235,9 +235,10 @@ extra_groups: Dict[str, Set[str]] = { "Questagons": {"Red Questagon", "Green Questagon", "Blue Questagon", "Gold Questagon"}, "Ladder to Atoll": {"Ladder to Ruined Atoll"}, # fuzzy matching made it hint Ladders in Well, now it won't "Ladders to Bell": {"Ladders to West Bell"}, - "Ladders to Well": {"Ladders in Well"}, # fuzzy matching decided ladders in well was ladders to west bell + "Ladders to Well": {"Ladders in Well"}, # fuzzy matching decided Ladders in Well was Ladders to West Bell "Ladders in Atoll": {"Ladders in South Atoll"}, "Ladders in Ruined Atoll": {"Ladders in South Atoll"}, + "Ladders in Town": {"Ladders in Overworld Town"}, # fuzzy matching decided this was Ladders in South Atoll } item_name_groups.update(extra_groups) From 1a41e1acc8417d2791cc50c2f8082f57ef076ea1 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 1 Sep 2024 20:34:50 +0200 Subject: [PATCH 11/13] customserver: fix memory leak (#3864) --- MultiServer.py | 18 ++++++++++++++++++ WebHostLib/customserver.py | 12 +++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/MultiServer.py b/MultiServer.py index b7c0e0f745..fb539f5671 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -67,6 +67,21 @@ def update_dict(dictionary, entries): return dictionary +def queue_gc(): + import gc + from threading import Thread + + gc_thread: typing.Optional[Thread] = getattr(queue_gc, "_thread", None) + def async_collect(): + time.sleep(2) + setattr(queue_gc, "_thread", None) + gc.collect() + if not gc_thread: + gc_thread = Thread(target=async_collect) + setattr(queue_gc, "_thread", gc_thread) + gc_thread.start() + + # functions callable on storable data on the server by clients modify_functions = { # generic: @@ -551,6 +566,9 @@ class Context: self.logger.info(f"Saving failed. Retry in {self.auto_save_interval} seconds.") else: self.save_dirty = False + if not atexit_save: # if atexit is used, that keeps a reference anyway + queue_gc() + self.auto_saver_thread = threading.Thread(target=save_regularly, daemon=True) self.auto_saver_thread.start() diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index ccffc40b38..a2eef108b0 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -72,6 +72,14 @@ class WebHostContext(Context): self.video = {} self.tags = ["AP", "WebHost"] + def __del__(self): + try: + import psutil + from Utils import format_SI_prefix + self.logger.debug(f"Context destroyed, Mem: {format_SI_prefix(psutil.Process().memory_info().rss, 1024)}iB") + except ImportError: + self.logger.debug("Context destroyed") + def _load_game_data(self): for key, value in self.static_server_data.items(): # NOTE: attributes are mutable and shared, so they will have to be copied before being modified @@ -249,6 +257,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict, ctx = WebHostContext(static_server_data, logger) ctx.load(room_id) ctx.init_save() + assert ctx.server is None try: ctx.server = websockets.serve( functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context) @@ -279,6 +288,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict, ctx.auto_shutdown = Room.get(id=room_id).timeout if ctx.saving: setattr(asyncio.current_task(), "save", lambda: ctx._save(True)) + assert ctx.shutdown_task is None ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, [])) await ctx.shutdown_task @@ -325,7 +335,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict, def run(self): while 1: next_room = rooms_to_run.get(block=True, timeout=None) - gc.collect(0) + gc.collect() task = asyncio.run_coroutine_threadsafe(start_room(next_room), loop) self._tasks.append(task) task.add_done_callback(self._done) From 6f46397185ea945ed4db7d1404980c8f2d92253d Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Sun, 1 Sep 2024 21:41:55 +0200 Subject: [PATCH 12/13] Rogue Legacy: Crash generation when there are overlapping IDs (#3865) Client literally does not work when there are overlapping IDs. Phar is not currently intending to fix it. https://discord.com/channels/731205301247803413/929585237695029268/1269684436853723156 --- worlds/rogue_legacy/__init__.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/worlds/rogue_legacy/__init__.py b/worlds/rogue_legacy/__init__.py index eb65769954..78e56a794c 100644 --- a/worlds/rogue_legacy/__init__.py +++ b/worlds/rogue_legacy/__init__.py @@ -49,6 +49,30 @@ class RLWorld(World): return {option_name: self.get_setting(option_name).value for option_name in rl_options} def generate_early(self): + location_ids_used_per_game = { + world.game: set(world.location_id_to_name) for world in self.multiworld.worlds.values() + } + item_ids_used_per_game = { + world.game: set(world.item_id_to_name) for world in self.multiworld.worlds.values() + } + overlapping_games = set() + + for id_lookup in (location_ids_used_per_game, item_ids_used_per_game): + for game_1, ids_1 in id_lookup.items(): + for game_2, ids_2 in id_lookup.items(): + if game_1 == game_2: + continue + + if ids_1 & ids_2: + overlapping_games.add(tuple(sorted([game_1, game_2]))) + + if overlapping_games: + raise RuntimeError( + "In this multiworld, there are games with overlapping item/location IDs.\n" + "The current Rogue Legacy does not support these and a fix is not currently planned.\n" + f"The overlapping games are: {overlapping_games}" + ) + # Check validation of names. additional_lady_names = len(self.get_setting("additional_lady_names").value) additional_sir_names = len(self.get_setting("additional_sir_names").value) From 3ab71daa8d14bfc4d83836c185a48692cbeaf518 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 1 Sep 2024 21:59:37 +0200 Subject: [PATCH 13/13] MultiServer: put some limits in place (#3858) Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- MultiServer.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/MultiServer.py b/MultiServer.py index fb539f5671..e0b137fd68 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -1221,6 +1221,10 @@ class CommonCommandProcessor(CommandProcessor): timer = int(seconds, 10) except ValueError: timer = 10 + else: + if timer > 60 * 60: + raise ValueError(f"{timer} is invalid. Maximum is 1 hour.") + async_start(countdown(self.ctx, timer)) return True @@ -2057,6 +2061,8 @@ class ServerCommandProcessor(CommonCommandProcessor): item_name, usable, response = get_intended_text(item_name, names) if usable: amount: int = int(amount) + if amount > 100: + raise ValueError(f"{amount} is invalid. Maximum is 100.") new_items = [NetworkItem(names[item_name], -1, 0) for _ in range(int(amount))] send_items_to(self.ctx, team, slot, *new_items)