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 <beauxq@yahoo.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
This commit is contained in:
Silvris 2024-08-31 06:15:00 -05:00 committed by GitHub
parent b1be597451
commit 920cffda2d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 2420 additions and 2056 deletions

View File

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

View File

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

View File

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

View File

@ -1,25 +1,25 @@
import logging import logging
import typing
from BaseClasses import Tutorial, ItemClassification, MultiWorld from BaseClasses import Tutorial, ItemClassification, MultiWorld, CollectionState, Item
from Fill import fill_restrictive from Fill import fill_restrictive
from Options import PerGameCommonOptions from Options import PerGameCommonOptions
from worlds.AutoWorld import World, WebWorld from worlds.AutoWorld import World, WebWorld
from .Items import item_table, item_names, copy_ability_table, animal_friend_table, filler_item_weights, KDL3Item, \ 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 trap_item_table, copy_ability_access_table, star_item_weights, total_filler_weights, animal_friend_spawn_table,\
from .Locations import location_table, KDL3Location, level_consumables, consumable_locations, star_locations lookup_item_to_id
from .Names.AnimalFriendSpawns import animal_friend_spawns from .locations import location_table, KDL3Location, level_consumables, consumable_locations, star_locations
from .Names.EnemyAbilities import vanilla_enemies, enemy_mapping, enemy_restrictive from .names.animal_friend_spawns import animal_friend_spawns, problematic_sets
from .Regions import create_levels, default_levels from .names.enemy_abilities import vanilla_enemies, enemy_mapping, enemy_restrictive
from .Options import KDL3Options from .regions import create_levels, default_levels
from .Presets import kdl3_options_presets from .options import KDL3Options, kdl3_option_groups
from .Names import LocationName from .presets import kdl3_options_presets
from .Room import KDL3Room from .names import location_name
from .Rules import set_rules from .room import KDL3Room
from .Rom import KDL3DeltaPatch, get_base_rom_path, RomData, patch_rom, KDL3JHASH, KDL3UHASH from .rules import set_rules
from .Client import KDL3SNIClient 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 os
import math import math
import threading import threading
@ -53,6 +53,7 @@ class KDL3WebWorld(WebWorld):
) )
] ]
options_presets = kdl3_options_presets options_presets = kdl3_options_presets
option_groups = kdl3_option_groups
class KDL3World(World): class KDL3World(World):
@ -61,35 +62,35 @@ class KDL3World(World):
""" """
game = "Kirby's Dream Land 3" game = "Kirby's Dream Land 3"
options_dataclass: typing.ClassVar[typing.Type[PerGameCommonOptions]] = KDL3Options options_dataclass: ClassVar[Type[PerGameCommonOptions]] = KDL3Options
options: 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} location_name_to_id = {location_table[location]: location for location in location_table}
item_name_groups = item_names item_name_groups = item_names
web = KDL3WebWorld() web = KDL3WebWorld()
settings: typing.ClassVar[KDL3Settings] settings: ClassVar[KDL3Settings]
def __init__(self, multiworld: MultiWorld, player: int): def __init__(self, multiworld: MultiWorld, player: int):
self.rom_name = None self.rom_name: bytes = bytes()
self.rom_name_available_event = threading.Event() self.rom_name_available_event = threading.Event()
super().__init__(multiworld, player) super().__init__(multiworld, player)
self.copy_abilities: Dict[str, str] = vanilla_enemies.copy() self.copy_abilities: Dict[str, str] = vanilla_enemies.copy()
self.required_heart_stars: int = 0 # we fill this during create_items 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.player_levels = default_levels.copy()
self.stage_shuffle_enabled = False self.stage_shuffle_enabled = False
self.boss_butch_bosses: List[Optional[bool]] = list() self.boss_butch_bosses: List[Optional[bool]] = []
self.rooms: Optional[List[KDL3Room]] = None self.rooms: List[KDL3Room] = []
@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}")
create_regions = create_levels 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] item = item_table[name]
classification = ItemClassification.filler classification = ItemClassification.filler
if item.progression and not force_non_progression: if item.progression and not force_non_progression:
@ -99,7 +100,7 @@ class KDL3World(World):
classification = ItemClassification.trap classification = ItemClassification.trap
return KDL3Item(name, classification, item.code, self.player) 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: if include_stars:
return self.random.choices(list(total_filler_weights.keys()), return self.random.choices(list(total_filler_weights.keys()),
weights=list(total_filler_weights.values()))[0] weights=list(total_filler_weights.values()))[0]
@ -112,8 +113,8 @@ class KDL3World(World):
self.options.slow_trap_weight.value, self.options.slow_trap_weight.value,
self.options.ability_trap_weight.value])[0] self.options.ability_trap_weight.value])[0]
def get_restrictive_copy_ability_placement(self, copy_ability: str, enemies_to_set: typing.List[str], def get_restrictive_copy_ability_placement(self, copy_ability: str, enemies_to_set: List[str],
level: int, stage: int): level: int, stage: int) -> Optional[str]:
valid_rooms = [room for room in self.rooms if (room.level < level) 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 or (room.level == level and room.stage < stage)] # leave out the stage in question to avoid edge
valid_enemies = set() valid_enemies = set()
@ -124,6 +125,10 @@ class KDL3World(World):
return None # a valid enemy got placed by a more restrictive placement 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])) 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: def pre_fill(self) -> None:
if self.options.copy_ability_randomization: if self.options.copy_ability_randomization:
# randomize copy abilities # 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 # If Kine is ever the last animal friend placed, he will cause fill errors on closed world
animal_pool.sort() animal_pool.sort()
locations = [self.multiworld.get_location(spawn, self.player) for spawn in spawns] locations = [self.multiworld.get_location(spawn, self.player) for spawn in spawns]
items = [self.create_item(animal) for animal in animal_pool] items: List[Item] = [self.create_item(animal) for animal in animal_pool]
allstate = self.multiworld.get_all_state(False) 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) self.random.shuffle(locations)
fill_restrictive(self.multiworld, allstate, locations, items, True, True) 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: else:
animal_friends = animal_friend_spawns.copy() animal_friends = animal_friend_spawns.copy()
for animal in animal_friends: for animal in animal_friends:
@ -225,21 +252,20 @@ class KDL3World(World):
remaining_items = len(location_table) - len(itempool) remaining_items = len(location_table) - len(itempool)
if not self.options.consumables: if not self.options.consumables:
remaining_items -= len(consumable_locations) remaining_items -= len(consumable_locations)
remaining_items -= len(star_locations) if not self.options.starsanity:
if self.options.starsanity: remaining_items -= len(star_locations)
# star fill, keep consumable pool locked to consumable and fill 767 stars specifically max_heart_stars = self.options.max_heart_stars.value
star_items = list(star_item_weights.keys()) if max_heart_stars > remaining_items:
star_weights = list(star_item_weights.values()) max_heart_stars = remaining_items
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
# ensure at least 1 heart star required per world # ensure at least 1 heart star required per world
required_heart_stars = max(int(total_heart_stars * required_percentage), 5) required_heart_stars = min(max(int(max_heart_stars * required_percentage), 5), 99)
filler_items = total_heart_stars - required_heart_stars filler_items = remaining_items - required_heart_stars
filler_amount = math.floor(filler_items * (self.options.filler_percentage / 100.0)) converted_heart_stars = math.floor((max_heart_stars - required_heart_stars) * (self.options.filler_percentage / 100.0))
trap_amount = math.floor(filler_amount * (self.options.trap_percentage / 100.0)) non_required_heart_stars = max_heart_stars - converted_heart_stars - required_heart_stars
filler_amount -= trap_amount filler_items -= non_required_heart_stars
non_required_heart_stars = filler_items - filler_amount - trap_amount trap_amount = math.floor(filler_items * (self.options.trap_percentage / 100.0))
filler_items -= trap_amount
self.required_heart_stars = required_heart_stars self.required_heart_stars = required_heart_stars
# handle boss requirements here # handle boss requirements here
requirements = [required_heart_stars] requirements = [required_heart_stars]
@ -261,8 +287,8 @@ class KDL3World(World):
requirements.insert(i - 1, quotient * i) requirements.insert(i - 1, quotient * i)
self.boss_requirements = requirements self.boss_requirements = requirements
itempool.extend([self.create_item("Heart Star") for _ in range(required_heart_stars)]) itempool.extend([self.create_item("Heart Star") for _ in range(required_heart_stars)])
itempool.extend([self.create_item(self.get_filler_item_name(False)) itempool.extend([self.create_item(self.get_filler_item_name(bool(self.options.starsanity.value)))
for _ in range(filler_amount + (remaining_items - total_heart_stars))]) for _ in range(filler_items)])
itempool.extend([self.create_item(self.get_trap_item_name()) itempool.extend([self.create_item(self.get_trap_item_name())
for _ in range(trap_amount)]) for _ in range(trap_amount)])
itempool.extend([self.create_item("Heart Star", True) for _ in range(non_required_heart_stars)]) 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]] self.multiworld.get_location(location_table[self.player_levels[level][stage]]
.replace("Complete", "Stage Completion"), self.player) \ .replace("Complete", "Stage Completion"), self.player) \
.place_locked_item(KDL3Item( .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)) ItemClassification.progression, None, self.player))
set_rules = set_rules set_rules = set_rules
def generate_basic(self) -> None: def generate_basic(self) -> None:
self.stage_shuffle_enabled = self.options.stage_shuffle > 0 self.stage_shuffle_enabled = self.options.stage_shuffle > 0
goal = self.options.goal goal = self.options.goal.value
goal_location = self.multiworld.get_location(LocationName.goals[goal], self.player) 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)) goal_location.place_locked_item(KDL3Item("Love-Love Rod", ItemClassification.progression, None, self.player))
for level in range(1, 6): for level in range(1, 6):
self.multiworld.get_location(f"Level {level} Boss - Defeated", self.player) \ self.multiworld.get_location(f"Level {level} Boss - Defeated", self.player) \
@ -300,60 +326,65 @@ class KDL3World(World):
else: else:
self.boss_butch_bosses = [False for _ in range(6)] self.boss_butch_bosses = [False for _ in range(6)]
def generate_output(self, output_directory: str): def generate_output(self, output_directory: str) -> None:
rom_path = ""
try: try:
rom = RomData(get_base_rom_path()) patch = KDL3ProcedurePatch()
patch_rom(self, rom) patch_rom(self, patch)
rom_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.sfc") self.rom_name = patch.name
rom.write_to_file(rom_path)
self.rom_name = rom.name
patch = KDL3DeltaPatch(os.path.splitext(rom_path)[0] + KDL3DeltaPatch.patch_file_ending, player=self.player, patch.write(os.path.join(output_directory,
player_name=self.multiworld.player_name[self.player], patched_path=rom_path) f"{self.multiworld.get_out_file_name_base(self.player)}{patch.patch_file_ending}"))
patch.write()
except Exception: except Exception:
raise raise
finally: finally:
self.rom_name_available_event.set() # make sure threading continues and errors are collected 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. # wait for self.rom_name to be available.
self.rom_name_available_event.wait() self.rom_name_available_event.wait()
assert isinstance(self.rom_name, bytes)
rom_name = getattr(self, "rom_name", None) 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 # we skip in case of error, so that the original error in the output thread is the one that gets raised
if rom_name: 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]] 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: def write_spoiler(self, spoiler_handle: TextIO) -> None:
if self.stage_shuffle_enabled: if self.stage_shuffle_enabled:
spoiler_handle.write(f"\nLevel Layout ({self.multiworld.get_player_name(self.player)}):\n") spoiler_handle.write(f"\nLevel Layout ({self.multiworld.get_player_name(self.player)}):\n")
for level in LocationName.level_names: for level in location_name.level_names:
for stage, i in zip(self.player_levels[LocationName.level_names[level]], range(1, 7)): 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") spoiler_handle.write(f"{level} {i}: {location_table[stage].replace(' - Complete', '')}\n")
if self.options.animal_randomization: if self.options.animal_randomization:
spoiler_handle.write(f"\nAnimal Friends ({self.multiworld.get_player_name(self.player)}):\n") 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): 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 = [] animals = []
for room in rooms: for room in rooms:
animals.extend([location.item.name.replace(" Spawn", "") animals.extend([location.item.name.replace(" Spawn", "")
for location in room.locations if "Animal" in location.name]) for location in room.locations if "Animal" in location.name
spoiler_handle.write(f"{location_table[self.player_levels[level][stage]].replace(' - Complete','')}" and location.item is not None])
spoiler_handle.write(f"{location_table[self.player_levels[lvl][stage]].replace(' - Complete','')}"
f": {', '.join(animals)}\n") f": {', '.join(animals)}\n")
if self.options.copy_ability_randomization: if self.options.copy_ability_randomization:
spoiler_handle.write(f"\nCopy Abilities ({self.multiworld.get_player_name(self.player)}):\n") spoiler_handle.write(f"\nCopy Abilities ({self.multiworld.get_player_name(self.player)}):\n")
for enemy in self.copy_abilities: for enemy in self.copy_abilities:
spoiler_handle.write(f"{enemy}: {self.copy_abilities[enemy].replace('No Ability', 'None').replace(' Ability', '')}\n") 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: 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 = {} level_hint_data = {}
for level in regions: for level in regions:
for stage in range(7): for stage in range(7):
@ -361,6 +392,6 @@ class KDL3World(World):
self.player).name.replace(" - Complete", "") self.player).name.replace(" - Complete", "")
stage_regions = [room for room in self.rooms if stage_name in room.name] stage_regions = [room for room in self.rooms if stage_name in room.name]
for region in stage_regions: 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'}" level_hint_data[location.address] = f"{regions[level]} {stage + 1 if stage < 6 else 'Boss'}"
hint_data[self.player] = level_hint_data hint_data[self.player] = level_hint_data

View File

@ -1,5 +1,9 @@
import struct 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 = { kirby_flavor_presets = {
1: { 1: {
@ -223,6 +227,23 @@ kirby_flavor_presets = {
"14": "E6E6FA", "14": "E6E6FA",
"15": "976FBD", "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 = { 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 palette = world.options.kirby_flavor_preset.value
if palette == KirbyFlavorPreset.option_custom: if palette == KirbyFlavorPreset.option_custom:
return world.options.kirby_flavor.value return world.options.kirby_flavor.value
return kirby_flavor_presets.get(palette, None) 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 palette = world.options.gooey_flavor_preset.value
if palette == GooeyFlavorPreset.option_custom: if palette == GooeyFlavorPreset.option_custom:
return world.options.gooey_flavor.value return world.options.gooey_flavor.value
return gooey_flavor_presets.get(palette, None) 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 red = red >> 3
green = green >> 3 green = green >> 3
blue = blue >> 3 blue = blue >> 3
@ -420,15 +441,15 @@ def rgb888_to_bgr555(red, green, blue) -> bytes:
return struct.pack("H", outcol) 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() output_data = bytearray()
for color in target: for color in target:
hexcol = palette[color] hexcol = palette[color]
if hexcol.startswith("#"): if hexcol.startswith("#"):
hexcol = hexcol.replace("#", "") hexcol = hexcol.replace("#", "")
colint = int(hexcol, 16) 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) col = tuple(int(int(factor*x) + offset) for x in col)
byte_data = rgb888_to_bgr555(col[0], col[1], col[2]) byte_data = rgb888_to_bgr555(col[0], col[1], col[2])
output_data.extend(bytearray(byte_data)) output_data.extend(bytearray(byte_data))
return output_data return bytes(output_data)

View File

@ -11,13 +11,13 @@ from MultiServer import mark_raw
from NetUtils import ClientStatus, color from NetUtils import ClientStatus, color
from Utils import async_start from Utils import async_start
from worlds.AutoSNIClient import SNIClient from worlds.AutoSNIClient import SNIClient
from .Locations import boss_locations from .locations import boss_locations
from .Gifting import kdl3_gifting_options, kdl3_trap_gifts, kdl3_gifts, update_object, pop_object, initialize_giftboxes 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 .client_addrs import consumable_addrs, star_addrs
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from SNIClient import SNIClientCommandProcessor from SNIClient import SNIClientCommandProcessor, SNIContext
snes_logger = logging.getLogger("SNES") snes_logger = logging.getLogger("SNES")
@ -81,17 +81,16 @@ deathlink_messages = defaultdict(lambda: " was defeated.", {
@mark_raw @mark_raw
def cmd_gift(self: "SNIClientCommandProcessor"): def cmd_gift(self: "SNIClientCommandProcessor") -> None:
"""Toggles gifting for the current game.""" """Toggles gifting for the current game."""
if not getattr(self.ctx, "gifting", None): handler = self.ctx.client_handler
self.ctx.gifting = True assert isinstance(handler, KDL3SNIClient)
else: handler.gifting = not handler.gifting
self.ctx.gifting = not self.ctx.gifting self.output(f"Gifting set to {handler.gifting}")
self.output(f"Gifting set to {self.ctx.gifting}")
async_start(update_object(self.ctx, f"Giftboxes;{self.ctx.team}", { async_start(update_object(self.ctx, f"Giftboxes;{self.ctx.team}", {
f"{self.ctx.slot}": f"{self.ctx.slot}":
{ {
"IsOpen": self.ctx.gifting, "IsOpen": handler.gifting,
**kdl3_gifting_options **kdl3_gifting_options
} }
})) }))
@ -100,16 +99,17 @@ def cmd_gift(self: "SNIClientCommandProcessor"):
class KDL3SNIClient(SNIClient): class KDL3SNIClient(SNIClient):
game = "Kirby's Dream Land 3" game = "Kirby's Dream Land 3"
patch_suffix = ".apkdl3" patch_suffix = ".apkdl3"
levels = None levels: typing.Dict[int, typing.List[int]] = {}
consumables = None consumables: typing.Optional[bool] = None
stars = None stars: typing.Optional[bool] = None
item_queue: typing.List = [] item_queue: typing.List[int] = []
initialize_gifting = False initialize_gifting: bool = False
gifting: bool = False
giftbox_key: str = "" giftbox_key: str = ""
motherbox_key: str = "" motherbox_key: str = ""
client_random: random.Random = random.Random() 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 from SNIClient import DeathState, snes_buffered_write, snes_flush_writes, snes_read
game_state = await snes_read(ctx, KDL3_GAME_STATE, 1) game_state = await snes_read(ctx, KDL3_GAME_STATE, 1)
if game_state[0] == 0xFF: if game_state[0] == 0xFF:
@ -131,7 +131,7 @@ class KDL3SNIClient(SNIClient):
ctx.death_state = DeathState.dead ctx.death_state = DeathState.dead
ctx.last_death_link = time.time() 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 from SNIClient import snes_read
rom_name = await snes_read(ctx, KDL3_ROMNAME, 0x15) 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": 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.game = self.game
ctx.rom = rom_name 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 ctx.allow_collect = True
if "gift" not in ctx.command_processor.commands: if "gift" not in ctx.command_processor.commands:
ctx.command_processor.commands["gift"] = cmd_gift 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) death_link = await snes_read(ctx, KDL3_DEATH_LINK_ADDR, 1)
if death_link: if death_link:
await ctx.update_death_link(bool(death_link[0] & 0b1)) await ctx.update_death_link(bool(death_link[0] & 0b1))
ctx.items_handling |= (death_link[0] & 0b10) # set local items if enabled
return True 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 from SNIClient import snes_buffered_write, snes_read
if len(self.item_queue) > 0: if len(self.item_queue) > 0:
item = self.item_queue.pop() item = self.item_queue.pop()
@ -168,8 +169,8 @@ class KDL3SNIClient(SNIClient):
else: else:
self.item_queue.append(item) # no more slots, get it next go around self.item_queue.append(item) # no more slots, get it next go around
async def pop_gift(self, ctx): async def pop_gift(self, ctx: "SNIContext") -> None:
if ctx.stored_data[self.giftbox_key]: if self.giftbox_key in ctx.stored_data and ctx.stored_data[self.giftbox_key]:
from SNIClient import snes_read, snes_buffered_write from SNIClient import snes_read, snes_buffered_write
key, gift = ctx.stored_data[self.giftbox_key].popitem() key, gift = ctx.stored_data[self.giftbox_key].popitem()
await pop_object(ctx, self.giftbox_key, key) await pop_object(ctx, self.giftbox_key, key)
@ -214,7 +215,7 @@ class KDL3SNIClient(SNIClient):
quality = min(10, quality * 2) quality = min(10, quality * 2)
else: else:
# it's not really edible, but he'll eat it anyway # 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) kirby_hp = await snes_read(ctx, KDL3_KIRBY_HP, 1)
gooey_hp = await snes_read(ctx, KDL3_KIRBY_HP + 2, 1) gooey_hp = await snes_read(ctx, KDL3_KIRBY_HP + 2, 1)
snes_buffered_write(ctx, KDL3_SOUND_FX, bytes([0x26])) snes_buffered_write(ctx, KDL3_SOUND_FX, bytes([0x26]))
@ -224,7 +225,8 @@ class KDL3SNIClient(SNIClient):
else: else:
snes_buffered_write(ctx, KDL3_KIRBY_HP, struct.pack("H", min(kirby_hp[0] + quality, 10))) 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: if gift != 4:
gift_base = kdl3_gifts[gift] gift_base = kdl3_gifts[gift]
else: else:
@ -238,7 +240,7 @@ class KDL3SNIClient(SNIClient):
if desire > most_applicable: if desire > most_applicable:
most_applicable = desire most_applicable = desire
most_applicable_slot = int(slot) 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 # only send to ourselves if no one else will take it
most_applicable_slot = int(slot) most_applicable_slot = int(slot)
# print(most_applicable, most_applicable_slot) # print(most_applicable, most_applicable_slot)
@ -257,7 +259,7 @@ class KDL3SNIClient(SNIClient):
item_uuid: item, item_uuid: item,
}) })
async def game_watcher(self, ctx) -> None: async def game_watcher(self, ctx: "SNIContext") -> None:
try: try:
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
rom = await snes_read(ctx, KDL3_ROMNAME, 0x15) 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])) await initialize_giftboxes(ctx, self.giftbox_key, self.motherbox_key, bool(enable_gifting[0]))
self.initialize_gifting = True self.initialize_gifting = True
# can't check debug anymore, without going and copying the value. might be important later. # 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() self.levels = dict()
for i in range(5): for i in range(5):
level_data = await snes_read(ctx, KDL3_LEVEL_ADDR + (14 * i), 14) 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 self.levels[5] = [0x0205, # Hyper Zone
0, # MG-5, can't send from here 0, # MG-5, can't send from here
0x0300, # Boss Butch 0x0300, # Boss Butch
@ -371,7 +374,7 @@ class KDL3SNIClient(SNIClient):
stages_raw = await snes_read(ctx, KDL3_COMPLETED_STAGES, 60) stages_raw = await snes_read(ctx, KDL3_COMPLETED_STAGES, 60)
stages = struct.unpack("HHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", stages_raw) stages = struct.unpack("HHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", stages_raw)
for i in range(30): 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: if stages[i] == 1 and loc_id not in ctx.checked_locations:
new_checks.append(loc_id) new_checks.append(loc_id)
elif loc_id in ctx.checked_locations: elif loc_id in ctx.checked_locations:
@ -381,8 +384,8 @@ class KDL3SNIClient(SNIClient):
heart_stars = await snes_read(ctx, KDL3_HEART_STARS, 35) heart_stars = await snes_read(ctx, KDL3_HEART_STARS, 35)
for i in range(5): for i in range(5):
start_ind = i * 7 start_ind = i * 7
for j in range(1, 7): for j in range(6):
level_ind = start_ind + j - 1 level_ind = start_ind + j
loc_id = 0x770100 + (6 * i) + j loc_id = 0x770100 + (6 * i) + j
if heart_stars[level_ind] and loc_id not in ctx.checked_locations: if heart_stars[level_ind] and loc_id not in ctx.checked_locations:
new_checks.append(loc_id) 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: if star not in ctx.checked_locations and stars[star_addrs[star]] == 0x01:
new_checks.append(star) new_checks.append(star)
if not game_state:
return
if game_state[0] != 0xFF: if game_state[0] != 0xFF:
await self.pop_gift(ctx) await self.pop_gift(ctx)
await self.pop_item(ctx, game_state[0] != 0xFF) await self.pop_item(ctx, game_state[0] != 0xFF)
@ -408,7 +414,7 @@ class KDL3SNIClient(SNIClient):
# boss status # boss status
boss_flag_bytes = await snes_read(ctx, KDL3_BOSS_STATUS, 2) 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()): 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: if boss_flag & (1 << bitmask) > 0 and boss not in ctx.checked_locations:
new_checks.append(boss) new_checks.append(boss)

View File

@ -1,8 +1,11 @@
# Small subfile to handle gifting info such as desired traits and giftbox management # Small subfile to handle gifting info such as desired traits and giftbox management
import typing 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([ await ctx.send_msgs([
{ {
"cmd": "Set", "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([ await ctx.send_msgs([
{ {
"cmd": "Set", "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) ctx.set_notify(motherbox_key, giftbox_key)
await update_object(ctx, f"Giftboxes;{ctx.team}", {f"{ctx.slot}": await update_object(ctx, f"Giftboxes;{ctx.team}", {f"{ctx.slot}":
{ {
"IsOpen": is_open, "IsOpen": is_open,
**kdl3_gifting_options **kdl3_gifting_options
}}) }})
ctx.gifting = is_open ctx.client_handler.gifting = is_open
kdl3_gifting_options = { kdl3_gifting_options = {

View File

@ -77,9 +77,9 @@ filler_item_weights = {
} }
star_item_weights = { star_item_weights = {
"Little Star": 4, "Little Star": 16,
"Medium Star": 2, "Medium Star": 8,
"Big Star": 1 "Big Star": 4
} }
total_filler_weights = { total_filler_weights = {
@ -102,4 +102,4 @@ item_names = {
"Animal Friend": set(animal_friend_table), "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}

940
worlds/kdl3/locations.py Normal file
View File

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

View File

@ -1,3 +1,5 @@
from typing import List
grass_land_1_a1 = "Grass Land 1 - Animal 1" # Nago grass_land_1_a1 = "Grass Land 1 - Animal 1" # Nago
grass_land_1_a2 = "Grass Land 1 - Animal 2" # Rick grass_land_1_a2 = "Grass Land 1 - Animal 2" # Rick
grass_land_2_a1 = "Grass Land 2 - Animal 1" # ChuChu grass_land_2_a1 = "Grass Land 2 - Animal 1" # ChuChu
@ -197,3 +199,12 @@ animal_friend_spawns = {
iceberg_6_a5: "ChuChu Spawn", iceberg_6_a5: "ChuChu Spawn",
iceberg_6_a6: "Nago 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]
]

View File

@ -809,7 +809,7 @@ vanilla_enemies = {'Waddle Dee': 'No Ability',
enemy_restrictive: List[Tuple[List[str], List[str]]] = [ enemy_restrictive: List[Tuple[List[str], List[str]]] = [
# abilities, enemies, set_all (False to set any) # 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 # Sand Canyon 6
(["Parasol Ability", "Cutter Ability"], ['Bukiset (Parasol)', 'Bukiset (Cutter)']), (["Parasol Ability", "Cutter Ability"], ['Bukiset (Parasol)', 'Bukiset (Cutter)']),
(["Spark Ability", "Clean Ability"], ['Bukiset (Spark)', 'Bukiset (Clean)']), (["Spark Ability", "Clean Ability"], ['Bukiset (Spark)', 'Bukiset (Clean)']),

View File

@ -1,13 +1,21 @@
import random import random
from dataclasses import dataclass from dataclasses import dataclass
from typing import List
from Options import DeathLink, Choice, Toggle, OptionDict, Range, PlandoBosses, DefaultOnToggle, \ from Options import DeathLinkMixin, Choice, Toggle, OptionDict, Range, PlandoBosses, DefaultOnToggle, \
PerGameCommonOptions, PlandoConnections PerGameCommonOptions, Visibility, NamedRange, OptionGroup, PlandoConnections
from .Names import LocationName 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): 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): class Goal(Choice):
@ -30,6 +38,7 @@ class Goal(Choice):
return cls.name_lookup[value].upper() return cls.name_lookup[value].upper()
return super().get_option_name(value) return super().get_option_name(value)
class GoalSpeed(Choice): class GoalSpeed(Choice):
""" """
Normal: the goal is unlocked after purifying the five bosses Normal: the goal is unlocked after purifying the five bosses
@ -40,13 +49,14 @@ class GoalSpeed(Choice):
option_fast = 1 option_fast = 1
class TotalHeartStars(Range): class MaxHeartStars(Range):
""" """
Maximum number of heart stars to include in the pool of items. 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" display_name = "Max Heart Stars"
range_start = 5 # set to 5 so strict bosses does not degrade 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 default = 30
@ -84,9 +94,9 @@ class BossShuffle(PlandoBosses):
Singularity: All (non-Zero) bosses will be replaced with a single boss Singularity: All (non-Zero) bosses will be replaced with a single boss
Supports plando placement. 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 duplicate_bosses = True
@ -278,7 +288,8 @@ class KirbyFlavorPreset(Choice):
option_orange = 11 option_orange = 11
option_lime = 12 option_lime = 12
option_lavender = 13 option_lavender = 13
option_custom = 14 option_miku = 14
option_custom = 15
default = 0 default = 0
@classmethod @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 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. "15", with their values being an HTML hex color.
""" """
display_name = "Custom Kirby Flavor"
default = { default = {
"1": "B01810", "1": "B01810",
"2": "F0E0E8", "2": "F0E0E8",
@ -313,6 +325,7 @@ class KirbyFlavor(OptionDict):
"14": "F8F8F8", "14": "F8F8F8",
"15": "B03830", "15": "B03830",
} }
visibility = Visibility.template | Visibility.spoiler # likely never supported on guis
class GooeyFlavorPreset(Choice): 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 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. "15", with their values being an HTML hex color.
""" """
display_name = "Custom Gooey Flavor"
default = { default = {
"1": "000808", "1": "000808",
"2": "102838", "2": "102838",
@ -363,6 +377,7 @@ class GooeyFlavor(OptionDict):
"8": "D0C0C0", "8": "D0C0C0",
"9": "F8F8F8", "9": "F8F8F8",
} }
visibility = Visibility.template | Visibility.spoiler # likely never supported on guis
class MusicShuffle(Choice): class MusicShuffle(Choice):
@ -402,14 +417,27 @@ class Gifting(Toggle):
display_name = "Gifting" 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 @dataclass
class KDL3Options(PerGameCommonOptions): class KDL3Options(PerGameCommonOptions, DeathLinkMixin):
remote_items: RemoteItems
plando_connections: KDL3PlandoConnections plando_connections: KDL3PlandoConnections
death_link: DeathLink
game_language: GameLanguage game_language: GameLanguage
goal: Goal goal: Goal
goal_speed: GoalSpeed goal_speed: GoalSpeed
total_heart_stars: TotalHeartStars max_heart_stars: MaxHeartStars
heart_stars_required: HeartStarsRequired heart_stars_required: HeartStarsRequired
filler_percentage: FillerPercentage filler_percentage: FillerPercentage
trap_percentage: TrapPercentage trap_percentage: TrapPercentage
@ -435,3 +463,17 @@ class KDL3Options(PerGameCommonOptions):
gooey_flavor: GooeyFlavor gooey_flavor: GooeyFlavor
music_shuffle: MusicShuffle music_shuffle: MusicShuffle
virtual_console: VirtualConsoleChanges 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, ]),
]

View File

@ -25,6 +25,7 @@ all_random = {
"ow_boss_requirement": "random", "ow_boss_requirement": "random",
"boss_requirement_random": "random", "boss_requirement_random": "random",
"consumables": "random", "consumables": "random",
"starsanity": "random",
"kirby_flavor_preset": "random", "kirby_flavor_preset": "random",
"gooey_flavor_preset": "random", "gooey_flavor_preset": "random",
"music_shuffle": "random", "music_shuffle": "random",

View File

@ -1,60 +1,62 @@
import orjson import orjson
import os import os
from pkgutil import get_data from pkgutil import get_data
from copy import deepcopy
from typing import TYPE_CHECKING, List, Dict, Optional, Union from typing import TYPE_CHECKING, List, Dict, Optional, Union, Callable
from BaseClasses import Region from BaseClasses import Region, CollectionState
from worlds.generic.Rules import add_item_rule from worlds.generic.Rules import add_item_rule
from .Locations import KDL3Location from .locations import KDL3Location
from .Names import LocationName from .names import location_name
from .Options import BossShuffle from .options import BossShuffle
from .Room import KDL3Room from .room import KDL3Room
if TYPE_CHECKING: if TYPE_CHECKING:
from . import KDL3World from . import KDL3World
default_levels = { default_levels = {
1: [0x770001, 0x770002, 0x770003, 0x770004, 0x770005, 0x770006, 0x770200], 1: [0x770000, 0x770001, 0x770002, 0x770003, 0x770004, 0x770005, 0x770200],
2: [0x770007, 0x770008, 0x770009, 0x77000A, 0x77000B, 0x77000C, 0x770201], 2: [0x770006, 0x770007, 0x770008, 0x770009, 0x77000A, 0x77000B, 0x770201],
3: [0x77000D, 0x77000E, 0x77000F, 0x770010, 0x770011, 0x770012, 0x770202], 3: [0x77000C, 0x77000D, 0x77000E, 0x77000F, 0x770010, 0x770011, 0x770202],
4: [0x770013, 0x770014, 0x770015, 0x770016, 0x770017, 0x770018, 0x770203], 4: [0x770012, 0x770013, 0x770014, 0x770015, 0x770016, 0x770017, 0x770203],
5: [0x770019, 0x77001A, 0x77001B, 0x77001C, 0x77001D, 0x77001E, 0x770204], 5: [0x770018, 0x770019, 0x77001A, 0x77001B, 0x77001C, 0x77001D, 0x770204],
} }
first_stage_blacklist = { first_stage_blacklist = {
# We want to confirm that the first stage can be completed without any items # We want to confirm that the first stage can be completed without any items
0x77000B, # 2-5 needs Kine 0x77000A, # 2-5 needs Kine
0x770011, # 3-5 needs Cutter 0x770010, # 3-5 needs Cutter
0x77001C, # 5-4 needs Burning 0x77001B, # 5-4 needs Burning
} }
first_world_limit = { first_world_limit = {
# We need to limit the number of very restrictive stages in level 1 on solo gens # 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 *first_stage_blacklist, # all three of the blacklist stages need 2+ items for both checks
0x770006,
0x770007, 0x770007,
0x770008, 0x770012,
0x770013, 0x77001D,
0x77001E,
} }
def generate_valid_level(world: "KDL3World", level: int, stage: int, 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) new_stage = world.random.choice(possible_stages)
if level == 1: if level == 1:
if stage == 0 and new_stage in first_stage_blacklist: 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) 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 elif (not (world.multiworld.players > 1 or world.options.consumables or world.options.starsanity) and
new_stage in first_world_limit and new_stage in first_world_limit and
sum(p_stage in first_world_limit for p_stage in placed_stages) sum(p_stage in first_world_limit for p_stage in placed_stages)
>= (2 if world.options.open_world else 1)): >= (2 if world.options.open_world else 1)):
return generate_valid_level(world, level, stage, possible_stages, placed_stages) return generate_valid_level(world, level, stage, possible_stages, placed_stages)
return new_stage return new_stage
def generate_rooms(world: "KDL3World", level_regions: Dict[int, Region]): def generate_rooms(world: "KDL3World", level_regions: Dict[int, Region]) -> None:
level_names = {LocationName.level_names[level]: level for level in LocationName.level_names} 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"))) room_data = orjson.loads(get_data(__name__, os.path.join("data", "Rooms.json")))
rooms: Dict[str, KDL3Room] = dict() rooms: Dict[str, KDL3Room] = dict()
for room_entry in room_data: 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["default_exits"], room_entry["animal_pointers"], room_entry["enemies"],
room_entry["entity_load"], room_entry["consumables"], room_entry["consumables_pointer"]) 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 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 if (not any(x in location for x in ["1-Up", "Maxim"]) or
world.options.consumables.value) and ("Star" not in location world.options.consumables.value) and ("Star" not in location
or world.options.starsanity.value)}, or world.options.starsanity.value)},
@ -83,8 +85,8 @@ def generate_rooms(world: "KDL3World", level_regions: Dict[int, Region]):
if room.stage == 7: if room.stage == 7:
first_rooms[0x770200 + room.level - 1] = room first_rooms[0x770200 + room.level - 1] = room
else: else:
first_rooms[0x770000 + ((room.level - 1) * 6) + room.stage] = room first_rooms[0x770000 + ((room.level - 1) * 6) + room.stage - 1] = room
exits = dict() exits: Dict[str, Callable[[CollectionState], bool]] = dict()
for def_exit in room.default_exits: for def_exit in room.default_exits:
target = f"{level_names[room.level]} {room.stage} - {def_exit['room']}" target = f"{level_names[room.level]} {room.stage} - {def_exit['room']}"
access_rule = tuple(def_exit["access_rule"]) 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: if world.options.open_world:
level_regions[level].add_exits([first_rooms[0x770200 + level - 1].name]) level_regions[level].add_exits([first_rooms[0x770200 + level - 1].name])
else: 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]) .parent_region.add_exits([first_rooms[0x770200 + level - 1].name])
def generate_valid_levels(world: "KDL3World", enforce_world: bool, enforce_pattern: bool) -> dict: def generate_valid_levels(world: "KDL3World", shuffle_mode: int) -> Dict[int, List[int]]:
levels: Dict[int, List[Optional[int]]] = { if shuffle_mode:
1: [None] * 7, levels: Dict[int, List[Optional[int]]] = {
2: [None] * 7, 1: [None] * 7,
3: [None] * 7, 2: [None] * 7,
4: [None] * 7, 3: [None] * 7,
5: [None] * 7, 4: [None] * 7,
} 5: [None] * 7,
}
possible_stages = [default_levels[level][stage] for level in default_levels for stage in range(6)] possible_stages = [default_levels[level][stage] for level in default_levels for stage in range(6)]
if world.options.plando_connections: if world.options.plando_connections:
for connection in world.options.plando_connections: for connection in world.options.plando_connections:
try: try:
entrance_world, entrance_stage = connection.entrance.rsplit(" ", 1) entrance_world, entrance_stage = connection.entrance.rsplit(" ", 1)
stage_world, stage_stage = connection.exit.rsplit(" ", 1) stage_world, stage_stage = connection.exit.rsplit(" ", 1)
new_stage = default_levels[LocationName.level_names[stage_world.strip()]][int(stage_stage) - 1] new_stage = default_levels[location_name.level_names[stage_world.strip()]][int(stage_stage) - 1]
levels[LocationName.level_names[entrance_world.strip()]][int(entrance_stage) - 1] = new_stage levels[location_name.level_names[entrance_world.strip()]][int(entrance_stage) - 1] = new_stage
possible_stages.remove(new_stage) possible_stages.remove(new_stage)
except Exception: except Exception:
raise Exception( raise Exception(
f"Invalid connection: {connection.entrance} =>" f"Invalid connection: {connection.entrance} =>"
f" {connection.exit} for player {world.player} ({world.player_name})") f" {connection.exit} for player {world.player} ({world.player_name})")
for level in range(1, 6): for level in range(1, 6):
for stage in range(6): for stage in range(6):
# Randomize bosses separately # Randomize bosses separately
try:
if levels[level][stage] is None: if levels[level][stage] is None:
stage_candidates = [candidate for candidate in possible_stages stage_candidates = [candidate for candidate in possible_stages
if (enforce_world and candidate in default_levels[level]) if (shuffle_mode == 1 and candidate in default_levels[level])
or (enforce_pattern and ((candidate - 1) & 0x00FFFF) % 6 == stage) or (shuffle_mode == 2 and (candidate & 0x00FFFF) % 6 == stage)
or (enforce_pattern == enforce_world) 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]) new_stage = generate_valid_level(world, level, stage, stage_candidates, levels[level])
possible_stages.remove(new_stage) possible_stages.remove(new_stage)
levels[level][stage] = new_stage levels[level][stage] = new_stage
except Exception: else:
raise Exception(f"Failed to find valid stage for {level}-{stage}. Remaining Stages:{possible_stages}") levels = deepcopy(default_levels)
for level in levels:
levels[level][6] = None
# now handle bosses # now handle bosses
boss_shuffle: Union[int, str] = world.options.boss_shuffle.value boss_shuffle: Union[int, str] = world.options.boss_shuffle.value
plando_bosses = [] plando_bosses = []
@ -168,17 +174,17 @@ def generate_valid_levels(world: "KDL3World", enforce_world: bool, enforce_patte
boss_shuffle = BossShuffle.options[options.pop()] boss_shuffle = BossShuffle.options[options.pop()]
for option in options: for option in options:
if "-" in option: if "-" in option:
loc, boss = option.split("-") loc, plando_boss = option.split("-")
loc = loc.title() loc = loc.title()
boss = boss.title() plando_boss = plando_boss.title()
levels[LocationName.level_names[loc]][6] = LocationName.boss_names[boss] levels[location_name.level_names[loc]][6] = location_name.boss_names[plando_boss]
plando_bosses.append(LocationName.boss_names[boss]) plando_bosses.append(location_name.boss_names[plando_boss])
else: else:
option = option.title() option = option.title()
for level in levels: for level in levels:
if levels[level][6] is None: if levels[level][6] is None:
levels[level][6] = LocationName.boss_names[option] levels[level][6] = location_name.boss_names[option]
plando_bosses.append(LocationName.boss_names[option]) plando_bosses.append(location_name.boss_names[option])
if boss_shuffle > 0: if boss_shuffle > 0:
if boss_shuffle == BossShuffle.option_full: if boss_shuffle == BossShuffle.option_full:
@ -223,15 +229,14 @@ def create_levels(world: "KDL3World") -> None:
5: level5, 5: level5,
} }
level_shuffle = world.options.stage_shuffle.value level_shuffle = world.options.stage_shuffle.value
if level_shuffle != 0: if hasattr(world.multiworld, "re_gen_passthrough"):
world.player_levels = generate_valid_levels( world.player_levels = getattr(world.multiworld, "re_gen_passthrough")["Kirby's Dream Land 3"]["player_levels"]
world, else:
level_shuffle == 1, world.player_levels = generate_valid_levels(world, level_shuffle)
level_shuffle == 2)
generate_rooms(world, levels) 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") menu.connect(level1, "Start Game")
level1.connect(level2, "To Level 2") level1.connect(level2, "To Level 2")

602
worlds/kdl3/rom.py Normal file
View File

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

133
worlds/kdl3/room.py Normal file
View File

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

View File

@ -1,7 +1,7 @@
from worlds.generic.Rules import set_rule, add_rule from worlds.generic.Rules import set_rule, add_rule
from .Names import LocationName, EnemyAbilities from .names import location_name, enemy_abilities, animal_friend_spawns
from .Locations import location_table from .locations import location_table
from .Options import GoalSpeed from .options import GoalSpeed
import typing import typing
if typing.TYPE_CHECKING: 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, 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: 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: else:
return state.can_reach(location_table[player_levels[level][5]], "Location", player) 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 # check animal requirements
if not (can_reach_coo(state, player) and can_reach_kine(state, player)): if not (can_reach_coo(state, player) and can_reach_kine(state, player)):
return False 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) iterator = iter(x for x in bukisets if copy_abilities[x] in abilities)
target_bukiset = next(iterator, None) target_bukiset = next(iterator, None)
can_reach = False 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) 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 can_reach = True
for enemy in {"Sparky", "Blocky", "Jumper Shoot", "Yuki", "Sir Kibble", "Haboki", "Boboo", "Captain Stitch"}: 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) 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: def set_rules(world: "KDL3World") -> None:
# Level 1 # 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)) 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)) 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)) lambda state: can_reach_kine(state, world.player))
# Level 2 # 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)) 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)) 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)) 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)) 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 lambda state: (can_reach_pitch(state, world.player) and
can_reach_kine(state, world.player) and can_reach_kine(state, world.player) and
can_reach_burning(state, world.player) and can_reach_burning(state, world.player) and
can_reach_stone(state, world.player))) can_reach_stone(state, world.player)))
# Level 3 # 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)) 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)) 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)) 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) lambda state: can_assemble_rob(state, world.player, world.copy_abilities)
) )
# Level 4 # 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)) 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)) 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)) 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)) lambda state: can_reach_rick(state, world.player))
# Level 5 # 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)) 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)) 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)) 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 lambda state: (can_reach_coo(state, world.player) and
can_reach_burning(state, world.player) and can_reach_burning(state, world.player) and
can_reach_chuchu(state, world.player))) can_reach_chuchu(state, world.player)))
# ChuChu is guaranteed here, but we use this for consistency # 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)) 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)) lambda state: can_fix_angel_wings(state, world.player, world.copy_abilities))
# Consumables # Consumables
if world.options.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)) 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)) 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)) 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)) 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)) 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)) 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)) 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)) 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)) 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 lambda state: (can_reach_kine(state, world.player) and
can_reach_burning(state, world.player) and can_reach_burning(state, world.player) and
can_reach_stone(state, world.player))) 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 lambda state: (can_reach_kine(state, world.player) and
can_reach_burning(state, world.player) and can_reach_burning(state, world.player) and
can_reach_stone(state, world.player))) 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)) 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)) 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 lambda state: can_reach_ice(state, world.player) and
(can_reach_rick(state, world.player) or can_reach_coo(state, world.player) (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_chuchu(state, world.player) or can_reach_pitch(state, world.player)
or can_reach_nago(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 lambda state: can_reach_ice(state, world.player) and
(can_reach_rick(state, world.player) or can_reach_coo(state, world.player) (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_chuchu(state, world.player) or can_reach_pitch(state, world.player)
or can_reach_nago(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 lambda state: can_reach_ice(state, world.player) and
(can_reach_rick(state, world.player) or can_reach_coo(state, world.player) (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_chuchu(state, world.player) or can_reach_pitch(state, world.player)
or can_reach_nago(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)) lambda state: can_reach_cutter(state, world.player))
if world.options.starsanity: if world.options.starsanity:
@ -274,50 +274,57 @@ def set_rules(world: "KDL3World") -> None:
# copy ability access edge cases # 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 # 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 # 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)) 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)) 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 # 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)) 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)) 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)) 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)) 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)) 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)) 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)) 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)) 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)) 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)) 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)) 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", for boss_flag, purification, i in zip(["Level 1 Boss - Purified", "Level 2 Boss - Purified",
"Level 3 Boss - Purified", "Level 4 Boss - Purified", "Level 3 Boss - Purified", "Level 4 Boss - Purified",
"Level 5 Boss - Purified"], "Level 5 Boss - Purified"],
[LocationName.grass_land_whispy, LocationName.ripple_field_acro, [location_name.grass_land_whispy, location_name.ripple_field_acro,
LocationName.sand_canyon_poncon, LocationName.cloudy_park_ado, location_name.sand_canyon_poncon, location_name.cloudy_park_ado,
LocationName.iceberg_dedede], location_name.iceberg_dedede],
range(1, 6)): range(1, 6)):
set_rule(world.multiworld.get_location(boss_flag, world.player), 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]) lambda state, x=i: (state.has("Heart Star", world.player, world.boss_requirements[x - 1])
and can_reach_boss(state, world.player, i, and can_reach_boss(state, world.player, x,
world.options.open_world.value, world.options.open_world.value,
world.options.ow_boss_requirement.value, world.options.ow_boss_requirement.value,
world.player_levels))) world.player_levels)))
set_rule(world.multiworld.get_location(purification, world.player), set_rule(world.multiworld.get_location(purification, world.player),
lambda state, i=i: (state.has("Heart Star", world.player, world.boss_requirements[i - 1]) lambda state, x=i: (state.has("Heart Star", world.player, world.boss_requirements[x - 1])
and can_reach_boss(state, world.player, i, and can_reach_boss(state, world.player, x,
world.options.open_world.value, world.options.open_world.value,
world.options.ow_boss_requirement.value, world.options.ow_boss_requirement.value,
world.player_levels))) world.player_levels)))
@ -327,12 +334,12 @@ def set_rules(world: "KDL3World") -> None:
for level in range(2, 6): for level in range(2, 6):
set_rule(world.multiworld.get_entrance(f"To Level {level}", world.player), 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: if world.options.strict_bosses:
for level in range(2, 6): for level in range(2, 6):
add_rule(world.multiworld.get_entrance(f"To Level {level}", world.player), 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: if world.options.goal_speed == GoalSpeed.option_normal:
add_rule(world.multiworld.get_entrance("To Level 6", world.player), add_rule(world.multiworld.get_entrance("To Level 6", world.player),

View File

@ -58,6 +58,10 @@ org $01AFC8
org $01B013 org $01B013
SEC ; Remove Dedede Bad Ending SEC ; Remove Dedede Bad Ending
org $01B050
JSL HookBossPurify
NOP
org $02B7B0 ; Zero unlock org $02B7B0 ; Zero unlock
LDA $80A0 LDA $80A0
CMP #$0001 CMP #$0001
@ -160,7 +164,6 @@ CopyAbilityAnimalOverride:
STA $39DF, X STA $39DF, X
RTL RTL
org $079A00
HeartStarCheck: HeartStarCheck:
TXA TXA
CMP #$0000 ; is this level 1 CMP #$0000 ; is this level 1
@ -201,7 +204,6 @@ HeartStarCheck:
SEC SEC
RTL RTL
org $079A80
OpenWorldUnlock: OpenWorldUnlock:
PHX PHX
LDX $900E ; Are we on open world? LDX $900E ; Are we on open world?
@ -224,7 +226,6 @@ OpenWorldUnlock:
PLX PLX
RTL RTL
org $079B00
MainLoopHook: MainLoopHook:
STA $D4 STA $D4
INC $3524 INC $3524
@ -239,16 +240,18 @@ MainLoopHook:
BEQ .Return ; return if we are BEQ .Return ; return if we are
LDA $5541 ; gooey status LDA $5541 ; gooey status
BPL .Slowness ; gooey is already spawned BPL .Slowness ; gooey is already spawned
LDA $39D1 ; is kirby alive?
BEQ .Slowness ; branch if he isn't
; maybe BMI here too?
LDA $8080 LDA $8080
CMP #$0000 ; did we get a gooey trap CMP #$0000 ; did we get a gooey trap
BEQ .Slowness ; branch if we did not BEQ .Slowness ; branch if we did not
JSL GooeySpawn JSL GooeySpawn
STZ $8080 DEC $8080
.Slowness: .Slowness:
LDA $8082 ; slowness LDA $8082 ; slowness
BEQ .Eject ; are we under the effects of a slowness trap BEQ .Eject ; are we under the effects of a slowness trap
DEC DEC $8082 ; dec by 1 each frame
STA $8082 ; dec by 1 each frame
.Eject: .Eject:
PHX PHX
PHY PHY
@ -258,14 +261,13 @@ MainLoopHook:
BEQ .PullVars ; branch if we haven't received eject BEQ .PullVars ; branch if we haven't received eject
LDA #$2000 ; select button press LDA #$2000 ; select button press
STA $60C1 ; write to controller mirror STA $60C1 ; write to controller mirror
STZ $8084 DEC $8084
.PullVars: .PullVars:
PLY PLY
PLX PLX
.Return: .Return:
RTL RTL
org $079B80
HeartStarGraphicFix: HeartStarGraphicFix:
LDA #$0000 LDA #$0000
PHX PHX
@ -288,7 +290,7 @@ HeartStarGraphicFix:
ASL ASL
TAX TAX
LDA $07D080, X ; table of original stage number 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 BEQ .ReturnTrue ; branch if so
CLC CLC
BRA .Return BRA .Return
@ -299,7 +301,6 @@ HeartStarGraphicFix:
PLX PLX
RTL RTL
org $079BF0
ParseItemQueue: ParseItemQueue:
; Local item queue parsing ; Local item queue parsing
NOP NOP
@ -336,8 +337,6 @@ ParseItemQueue:
AND #$000F AND #$000F
ASL ASL
TAY TAY
LDA $8080,Y
BNE .LoopCheck
JSL .ApplyNegative JSL .ApplyNegative
RTL RTL
.ApplyAbility: .ApplyAbility:
@ -418,35 +417,73 @@ ParseItemQueue:
CPY #$0005 CPY #$0005
BCS .PlayNone BCS .PlayNone
LDA $8080,Y LDA $8080,Y
BNE .Return CPY #$0002
BNE .Increment
CLC
LDA #$0384 LDA #$0384
ADC $8080, Y
BVC .PlayNegative
LDA #$FFFF
.PlayNegative:
STA $8080,Y STA $8080,Y
LDA #$00A7 LDA #$00A7
BRA .PlaySFXLong BRA .PlaySFXLong
.Increment:
INC
STA $8080, Y
BRA .PlayNegative
.PlayNone: .PlayNone:
LDA #$0000 LDA #$0000
BRA .PlaySFXLong BRA .PlaySFXLong
org $079D00
AnimalFriendSpawn: AnimalFriendSpawn:
PHA PHA
CPX #$0002 ; is this an animal friend? CPX #$0002 ; is this an animal friend?
BNE .Return BNE .Return
XBA XBA
PHA 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 ASL
TAY TAY
PLA PLA
INC INC
CMP $8000, Y ; do we have this animal friend CMP $8000, Y ; do we have this animal friend
BEQ .Return ; we have this animal friend BEQ .Return ; we have this animal friend
.False:
INX INX
.Return: .Return:
PLY PLY
LDA #$9999 LDA #$9999
RTL RTL
.AlreadySpawned:
PLA
PLX
ASL
TAY
PLA
BRA .False
org $079E00
WriteBWRAM: WriteBWRAM:
LDY #$6001 ;starting addr LDY #$6001 ;starting addr
LDA #$1FFE ;bytes to write LDA #$1FFE ;bytes to write
@ -479,7 +516,6 @@ WriteBWRAM:
.Return: .Return:
RTL RTL
org $079E80
ConsumableSet: ConsumableSet:
PHA PHA
PHX PHX
@ -507,7 +543,6 @@ ConsumableSet:
ASL ASL
TAX TAX
LDA $07D020, X ; current stage LDA $07D020, X ; current stage
DEC
ASL #6 ASL #6
TAX TAX
PLA PLA
@ -519,8 +554,16 @@ ConsumableSet:
BRA .LoopHead ; return to loop head BRA .LoopHead ; return to loop head
.ApplyCheck: .ApplyCheck:
LDA $A000, X ; consumables index LDA $A000, X ; consumables index
PHA
ORA #$0001 ORA #$0001
STA $A000, X STA $A000, X
PLA
AND #$00FF
BNE .Return
TXA
ORA #$1000
JSL ApplyLocalCheck
.Return:
PLY PLY
PLX PLX
PLA PLA
@ -528,7 +571,6 @@ ConsumableSet:
AND #$00FF AND #$00FF
RTL RTL
org $079F00
NormalGoalSet: NormalGoalSet:
PHX PHX
LDA $07D012 LDA $07D012
@ -549,7 +591,6 @@ NormalGoalSet:
STA $5AC1 ; cutscene STA $5AC1 ; cutscene
RTL RTL
org $079F80
FinalIcebergFix: FinalIcebergFix:
PHX PHX
PHY PHY
@ -572,7 +613,7 @@ FinalIcebergFix:
ASL ASL
TAX TAX
LDA $07D020, X LDA $07D020, X
CMP #$001E CMP #$001D
BEQ .ReturnTrue BEQ .ReturnTrue
CLC CLC
BRA .Return BRA .Return
@ -583,7 +624,6 @@ FinalIcebergFix:
PLX PLX
RTL RTL
org $07A000
StrictBosses: StrictBosses:
PHX PHX
LDA $901E ; Do we have strict bosses enabled? LDA $901E ; Do we have strict bosses enabled?
@ -610,7 +650,6 @@ StrictBosses:
LDA $53CD LDA $53CD
RTL RTL
org $07A030
NintenHalken: NintenHalken:
LDX #$0005 LDX #$0005
.Halken: .Halken:
@ -628,7 +667,6 @@ NintenHalken:
LDA #$0001 LDA #$0001
RTL RTL
org $07A080
StageCompleteSet: StageCompleteSet:
PHX PHX
LDA $5AC1 ; completed stage cutscene LDA $5AC1 ; completed stage cutscene
@ -656,9 +694,17 @@ StageCompleteSet:
ASL ASL
TAX TAX
LDA $9020, X ; load the stage we completed LDA $9020, X ; load the stage we completed
DEC
ASL ASL
TAX TAX
PHX
LDA $8200, X
AND #$00FF
BNE .ApplyClear
TXA
LSR
JSL ApplyLocalCheck
.ApplyClear:
PLX
LDA #$0001 LDA #$0001
ORA $8200, X ORA $8200, X
STA $8200, X STA $8200, X
@ -668,7 +714,6 @@ StageCompleteSet:
CMP $53CB CMP $53CB
RTL RTL
org $07A100
OpenWorldBossUnlock: OpenWorldBossUnlock:
PHX PHX
PHY PHY
@ -699,7 +744,6 @@ OpenWorldBossUnlock:
.LoopStage: .LoopStage:
PLX PLX
LDY $9020, X ; get stage id LDY $9020, X ; get stage id
DEY
INX INX
INX INX
PHA PHA
@ -732,7 +776,6 @@ OpenWorldBossUnlock:
PLX PLX
RTL RTL
org $07A180
GooeySpawn: GooeySpawn:
PHY PHY
PHX PHX
@ -768,7 +811,6 @@ GooeySpawn:
PLY PLY
RTL RTL
org $07A200
SpeedTrap: SpeedTrap:
PHX PHX
LDX $8082 ; do we have slowness LDX $8082 ; do we have slowness
@ -780,7 +822,6 @@ SpeedTrap:
EOR #$FFFF EOR #$FFFF
RTL RTL
org $07A280
HeartStarVisual: HeartStarVisual:
CPX #$0000 CPX #$0000
BEQ .SkipInx BEQ .SkipInx
@ -844,7 +885,6 @@ HeartStarVisual:
.Return: .Return:
RTL RTL
org $07A300
LoadFont: LoadFont:
JSL $00D29F ; play sfx JSL $00D29F ; play sfx
PHX PHX
@ -915,7 +955,6 @@ LoadFont:
PLX PLX
RTL RTL
org $07A380
HeartStarVisual2: HeartStarVisual2:
LDA #$2C80 LDA #$2C80
STA $0000, Y STA $0000, Y
@ -1029,14 +1068,12 @@ HeartStarVisual2:
STA $0000, Y STA $0000, Y
RTL RTL
org $07A480
HeartStarSelectFix: HeartStarSelectFix:
PHX PHX
TXA TXA
ASL ASL
TAX TAX
LDA $9020, X LDA $9020, X
DEC
TAX TAX
.LoopHead: .LoopHead:
CMP #$0006 CMP #$0006
@ -1051,15 +1088,31 @@ HeartStarSelectFix:
AND #$00FF AND #$00FF
RTL RTL
org $07A500
HeartStarCutsceneFix: HeartStarCutsceneFix:
TAX TAX
LDA $53D3 LDA $53D3
DEC DEC
STA $5AC3 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 RTL
org $07A510
GiftGiving: GiftGiving:
CMP #$0008 CMP #$0008
.This: .This:
@ -1075,7 +1128,6 @@ GiftGiving:
PLX PLX
JML $CABC18 JML $CABC18
org $07A550
PauseMenu: PauseMenu:
JSL $00D29F JSL $00D29F
PHX PHX
@ -1136,7 +1188,6 @@ PauseMenu:
PLX PLX
RTL RTL
org $07A600
StarsSet: StarsSet:
PHA PHA
PHX PHX
@ -1166,7 +1217,6 @@ StarsSet:
ASL ASL
TAX TAX
LDA $07D020, X LDA $07D020, X
DEC
ASL ASL
ASL ASL
ASL ASL
@ -1183,8 +1233,15 @@ StarsSet:
BRA .2LoopHead BRA .2LoopHead
.2LoopEnd: .2LoopEnd:
LDA $B000, X LDA $B000, X
PHA
ORA #$0001 ORA #$0001
STA $B000, X STA $B000, X
PLA
AND #$00FF
BNE .Return
TXA
ORA #$2000
JSL ApplyLocalCheck
.Return: .Return:
PLY PLY
PLX PLX
@ -1199,6 +1256,48 @@ StarsSet:
STA $39D7 STA $39D7
BRA .Return 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 org $07C000
db "KDL3_BASEPATCH_ARCHI" db "KDL3_BASEPATCH_ARCHI"
@ -1234,4 +1333,7 @@ org $07E040
db $3A, $01 db $3A, $01
db $3B, $05 db $3B, $05
db $3C, $05 db $3C, $05
db $3D, $05 db $3D, $05
org $07F000
incbin "APPauseIcons.dat"

View File

@ -6,6 +6,8 @@ from test.bases import WorldTestBase
from test.general import gen_steps from test.general import gen_steps
from worlds import AutoWorld from worlds import AutoWorld
from worlds.AutoWorld import call_all 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): class KDL3TestBase(WorldTestBase):

View File

@ -5,12 +5,12 @@ class TestFastGoal(KDL3TestBase):
options = { options = {
"open_world": False, "open_world": False,
"goal_speed": "fast", "goal_speed": "fast",
"total_heart_stars": 30, "max_heart_stars": 30,
"heart_stars_required": 50, "heart_stars_required": 50,
"filler_percentage": 0, "filler_percentage": 0,
} }
def test_goal(self): def test_goal(self) -> None:
self.assertBeatable(False) self.assertBeatable(False)
heart_stars = self.get_items_by_name("Heart Star") heart_stars = self.get_items_by_name("Heart Star")
self.collect(heart_stars[0:14]) self.collect(heart_stars[0:14])
@ -30,12 +30,12 @@ class TestNormalGoal(KDL3TestBase):
options = { options = {
"open_world": False, "open_world": False,
"goal_speed": "normal", "goal_speed": "normal",
"total_heart_stars": 30, "max_heart_stars": 30,
"heart_stars_required": 50, "heart_stars_required": 50,
"filler_percentage": 0, "filler_percentage": 0,
} }
def test_goal(self): def test_goal(self) -> None:
self.assertBeatable(False) self.assertBeatable(False)
heart_stars = self.get_items_by_name("Heart Star") heart_stars = self.get_items_by_name("Heart Star")
self.collect(heart_stars[0:14]) 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.assertEqual(self.count("Heart Star"), 30, str(self.multiworld.seed))
self.assertBeatable(True) self.assertBeatable(True)
def test_kine(self): def test_kine(self) -> None:
self.collect_by_name(["Cutter", "Burning", "Heart Star"]) self.collect_by_name(["Cutter", "Burning", "Heart Star"])
self.assertBeatable(False) self.assertBeatable(False)
def test_cutter(self): def test_cutter(self) -> None:
self.collect_by_name(["Kine", "Burning", "Heart Star"]) self.collect_by_name(["Kine", "Burning", "Heart Star"])
self.assertBeatable(False) self.assertBeatable(False)
def test_burning(self): def test_burning(self) -> None:
self.collect_by_name(["Cutter", "Kine", "Heart Star"]) self.collect_by_name(["Cutter", "Kine", "Heart Star"])
self.assertBeatable(False) self.assertBeatable(False)

View File

@ -1,6 +1,6 @@
from . import KDL3TestBase from . import KDL3TestBase
from ..names import location_name
from Options import PlandoConnection from Options import PlandoConnection
from ..Names import LocationName
import typing import typing
@ -12,31 +12,31 @@ class TestLocations(KDL3TestBase):
# these ensure we can always reach all stages physically # these ensure we can always reach all stages physically
} }
def test_simple_heart_stars(self): def test_simple_heart_stars(self) -> None:
self.run_location_test(LocationName.grass_land_muchi, ["ChuChu"]) self.run_location_test(location_name.grass_land_muchi, ["ChuChu"])
self.run_location_test(LocationName.grass_land_chao, ["Stone"]) self.run_location_test(location_name.grass_land_chao, ["Stone"])
self.run_location_test(LocationName.grass_land_mine, ["Kine"]) self.run_location_test(location_name.grass_land_mine, ["Kine"])
self.run_location_test(LocationName.ripple_field_kamuribana, ["Pitch", "Clean"]) self.run_location_test(location_name.ripple_field_kamuribana, ["Pitch", "Clean"])
self.run_location_test(LocationName.ripple_field_bakasa, ["Kine", "Parasol"]) self.run_location_test(location_name.ripple_field_bakasa, ["Kine", "Parasol"])
self.run_location_test(LocationName.ripple_field_toad, ["Needle"]) self.run_location_test(location_name.ripple_field_toad, ["Needle"])
self.run_location_test(LocationName.ripple_field_mama_pitch, ["Pitch", "Kine", "Burning", "Stone"]) self.run_location_test(location_name.ripple_field_mama_pitch, ["Pitch", "Kine", "Burning", "Stone"])
self.run_location_test(LocationName.sand_canyon_auntie, ["Clean"]) self.run_location_test(location_name.sand_canyon_auntie, ["Clean"])
self.run_location_test(LocationName.sand_canyon_nyupun, ["ChuChu", "Cutter"]) self.run_location_test(location_name.sand_canyon_nyupun, ["ChuChu", "Cutter"])
self.run_location_test(LocationName.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Spark", "Ice"]) self.run_location_test(location_name.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(location_name.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(location_name.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(location_name.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Clean", "Needle"])
self.run_location_test(LocationName.cloudy_park_hibanamodoki, ["Coo", "Clean"]) self.run_location_test(location_name.cloudy_park_hibanamodoki, ["Coo", "Clean"])
self.run_location_test(LocationName.cloudy_park_piyokeko, ["Needle"]) self.run_location_test(location_name.cloudy_park_piyokeko, ["Needle"])
self.run_location_test(LocationName.cloudy_park_mikarin, ["Coo"]) self.run_location_test(location_name.cloudy_park_mikarin, ["Coo"])
self.run_location_test(LocationName.cloudy_park_pick, ["Rick"]) self.run_location_test(location_name.cloudy_park_pick, ["Rick"])
self.run_location_test(LocationName.iceberg_kogoesou, ["Burning"]) self.run_location_test(location_name.iceberg_kogoesou, ["Burning"])
self.run_location_test(LocationName.iceberg_samus, ["Ice"]) self.run_location_test(location_name.iceberg_samus, ["Ice"])
self.run_location_test(LocationName.iceberg_name, ["Burning", "Coo", "ChuChu"]) self.run_location_test(location_name.iceberg_name, ["Burning", "Coo", "ChuChu"])
self.run_location_test(LocationName.iceberg_angel, ["Cutter", "Burning", "Spark", "Parasol", "Needle", "Clean", self.run_location_test(location_name.iceberg_angel, ["Cutter", "Burning", "Spark", "Parasol", "Needle", "Clean",
"Stone", "Ice"]) "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() items = itempool.copy()
while len(itempool) > 0: while len(itempool) > 0:
self.assertFalse(self.can_reach_location(location), str(self.multiworld.seed)) self.assertFalse(self.can_reach_location(location), str(self.multiworld.seed))
@ -57,7 +57,7 @@ class TestShiro(KDL3TestBase):
"plando_options": "connections" "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.assertFalse(self.can_reach_location("Iceberg 5 - Shiro"), str(self.multiworld.seed))
self.collect_by_name("Nago") self.collect_by_name("Nago")
self.assertFalse(self.can_reach_location("Iceberg 5 - Shiro"), str(self.multiworld.seed)) self.assertFalse(self.can_reach_location("Iceberg 5 - Shiro"), str(self.multiworld.seed))

View File

@ -1,47 +1,61 @@
from typing import List, Tuple from typing import List, Tuple, Optional
from . import KDL3TestBase from . import KDL3TestBase
from ..Room import KDL3Room from ..room import KDL3Room
from ..names import animal_friend_spawns
class TestCopyAbilityShuffle(KDL3TestBase): class TestCopyAbilityShuffle(KDL3TestBase):
options = { options = {
"open_world": False, "open_world": False,
"goal_speed": "normal", "goal_speed": "normal",
"total_heart_stars": 30, "max_heart_stars": 30,
"heart_stars_required": 50, "heart_stars_required": 50,
"filler_percentage": 0, "filler_percentage": 0,
"copy_ability_randomization": "enabled", "copy_ability_randomization": "enabled",
} }
def test_goal(self): def test_goal(self) -> None:
self.assertBeatable(False) try:
heart_stars = self.get_items_by_name("Heart Star") self.assertBeatable(False)
self.collect(heart_stars[0:14]) heart_stars = self.get_items_by_name("Heart Star")
self.assertEqual(self.count("Heart Star"), 14, str(self.multiworld.seed)) self.collect(heart_stars[0:14])
self.assertBeatable(False) self.assertEqual(self.count("Heart Star"), 14, str(self.multiworld.seed))
self.collect(heart_stars[14:15]) self.assertBeatable(False)
self.assertEqual(self.count("Heart Star"), 15, str(self.multiworld.seed)) self.collect(heart_stars[14:15])
self.assertBeatable(False) self.assertEqual(self.count("Heart Star"), 15, str(self.multiworld.seed))
self.collect_by_name(["Burning", "Cutter", "Kine"]) self.assertBeatable(False)
self.assertBeatable(True) self.collect_by_name(["Burning", "Cutter", "Kine"])
self.remove([self.get_item_by_name("Love-Love Rod")]) self.assertBeatable(True)
self.collect(heart_stars) self.remove([self.get_item_by_name("Love-Love Rod")])
self.assertEqual(self.count("Heart Star"), 30, str(self.multiworld.seed)) self.collect(heart_stars)
self.assertBeatable(True) 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): def test_kine(self) -> None:
self.collect_by_name(["Cutter", "Burning", "Heart Star"]) try:
self.assertBeatable(False) 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): def test_cutter(self) -> None:
self.collect_by_name(["Kine", "Burning", "Heart Star"]) try:
self.assertBeatable(False) 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): def test_burning(self) -> None:
self.collect_by_name(["Cutter", "Kine", "Heart Star"]) try:
self.assertBeatable(False) 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 rooms = self.multiworld.worlds[1].rooms
copy_abilities = self.multiworld.worlds[1].copy_abilities copy_abilities = self.multiworld.worlds[1].copy_abilities
sand_canyon_5 = self.multiworld.get_region("Sand Canyon 5 - 9", 1) sand_canyon_5 = self.multiworld.get_region("Sand Canyon 5 - 9", 1)
@ -63,7 +77,7 @@ class TestCopyAbilityShuffle(KDL3TestBase):
else: else:
self.fail("Could not reach Burning Ability before Iceberg 4!") 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 # 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 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 # first we need to identify our bukiset requirements
@ -74,13 +88,13 @@ class TestCopyAbilityShuffle(KDL3TestBase):
({"Stone Ability", "Burning Ability"}, {'Bukiset (Stone)', 'Bukiset (Burning)'}), ({"Stone Ability", "Burning Ability"}, {'Bukiset (Stone)', 'Bukiset (Burning)'}),
] ]
copy_abilities = self.multiworld.worlds[1].copy_abilities copy_abilities = self.multiworld.worlds[1].copy_abilities
required_abilities: List[Tuple[str]] = [] required_abilities: List[List[str]] = []
for abilities, bukisets in groups: for abilities, bukisets in groups:
potential_abilities: List[str] = list() potential_abilities: List[str] = list()
for bukiset in bukisets: for bukiset in bukisets:
if copy_abilities[bukiset] in abilities: if copy_abilities[bukiset] in abilities:
potential_abilities.append(copy_abilities[bukiset]) potential_abilities.append(copy_abilities[bukiset])
required_abilities.append(tuple(potential_abilities)) required_abilities.append(potential_abilities)
collected_abilities = list() collected_abilities = list()
for group in required_abilities: for group in required_abilities:
self.assertFalse(len(group) == 0, str(self.multiworld.seed)) self.assertFalse(len(group) == 0, str(self.multiworld.seed))
@ -103,91 +117,147 @@ class TestAnimalShuffle(KDL3TestBase):
options = { options = {
"open_world": False, "open_world": False,
"goal_speed": "normal", "goal_speed": "normal",
"total_heart_stars": 30, "max_heart_stars": 30,
"heart_stars_required": 50, "heart_stars_required": 50,
"filler_percentage": 0, "filler_percentage": 0,
"animal_randomization": "full", "animal_randomization": "full",
} }
def test_goal(self): def test_goal(self) -> None:
self.assertBeatable(False) try:
heart_stars = self.get_items_by_name("Heart Star") self.assertBeatable(False)
self.collect(heart_stars[0:14]) heart_stars = self.get_items_by_name("Heart Star")
self.assertEqual(self.count("Heart Star"), 14, str(self.multiworld.seed)) self.collect(heart_stars[0:14])
self.assertBeatable(False) self.assertEqual(self.count("Heart Star"), 14, str(self.multiworld.seed))
self.collect(heart_stars[14:15]) self.assertBeatable(False)
self.assertEqual(self.count("Heart Star"), 15, str(self.multiworld.seed)) self.collect(heart_stars[14:15])
self.assertBeatable(False) self.assertEqual(self.count("Heart Star"), 15, str(self.multiworld.seed))
self.collect_by_name(["Burning", "Cutter", "Kine"]) self.assertBeatable(False)
self.assertBeatable(True) self.collect_by_name(["Burning", "Cutter", "Kine"])
self.remove([self.get_item_by_name("Love-Love Rod")]) self.assertBeatable(True)
self.collect(heart_stars) self.remove([self.get_item_by_name("Love-Love Rod")])
self.assertEqual(self.count("Heart Star"), 30, str(self.multiworld.seed)) self.collect(heart_stars)
self.assertBeatable(True) 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): def test_kine(self) -> None:
self.collect_by_name(["Cutter", "Burning", "Heart Star"]) try:
self.assertBeatable(False) 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): def test_cutter(self) -> None:
self.collect_by_name(["Kine", "Burning", "Heart Star"]) try:
self.assertBeatable(False) 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): def test_burning(self) -> None:
self.collect_by_name(["Cutter", "Kine", "Heart Star"]) try:
self.assertBeatable(False) 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): def test_locked_animals(self) -> None:
self.assertTrue(self.multiworld.get_location("Ripple Field 5 - Animal 2", 1).item.name == "Pitch Spawn") ripple_field_5 = self.multiworld.get_location("Ripple Field 5 - Animal 2", 1)
self.assertTrue(self.multiworld.get_location("Iceberg 4 - Animal 1", 1).item.name == "ChuChu Spawn") self.assertTrue(ripple_field_5.item is not None and ripple_field_5.item.name == "Pitch Spawn",
self.assertTrue(self.multiworld.get_location("Sand Canyon 6 - Animal 1", 1).item.name in {"Kine Spawn", "Coo 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): class TestAllShuffle(KDL3TestBase):
options = { options = {
"open_world": False, "open_world": False,
"goal_speed": "normal", "goal_speed": "normal",
"total_heart_stars": 30, "max_heart_stars": 30,
"heart_stars_required": 50, "heart_stars_required": 50,
"filler_percentage": 0, "filler_percentage": 0,
"animal_randomization": "full", "animal_randomization": "full",
"copy_ability_randomization": "enabled", "copy_ability_randomization": "enabled",
} }
def test_goal(self): def test_goal(self) -> None:
self.assertBeatable(False) try:
heart_stars = self.get_items_by_name("Heart Star") self.assertBeatable(False)
self.collect(heart_stars[0:14]) heart_stars = self.get_items_by_name("Heart Star")
self.assertEqual(self.count("Heart Star"), 14, str(self.multiworld.seed)) self.collect(heart_stars[0:14])
self.assertBeatable(False) self.assertEqual(self.count("Heart Star"), 14, str(self.multiworld.seed))
self.collect(heart_stars[14:15]) self.assertBeatable(False)
self.assertEqual(self.count("Heart Star"), 15, str(self.multiworld.seed)) self.collect(heart_stars[14:15])
self.assertBeatable(False) self.assertEqual(self.count("Heart Star"), 15, str(self.multiworld.seed))
self.collect_by_name(["Burning", "Cutter", "Kine"]) self.assertBeatable(False)
self.assertBeatable(True) self.collect_by_name(["Burning", "Cutter", "Kine"])
self.remove([self.get_item_by_name("Love-Love Rod")]) self.assertBeatable(True)
self.collect(heart_stars) self.remove([self.get_item_by_name("Love-Love Rod")])
self.assertEqual(self.count("Heart Star"), 30, str(self.multiworld.seed)) self.collect(heart_stars)
self.assertBeatable(True) 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): def test_kine(self) -> None:
self.collect_by_name(["Cutter", "Burning", "Heart Star"]) try:
self.assertBeatable(False) 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): def test_cutter(self) -> None:
self.collect_by_name(["Kine", "Burning", "Heart Star"]) try:
self.assertBeatable(False) 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): def test_burning(self) -> None:
self.collect_by_name(["Cutter", "Kine", "Heart Star"]) try:
self.assertBeatable(False) 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): def test_locked_animals(self) -> None:
self.assertTrue(self.multiworld.get_location("Ripple Field 5 - Animal 2", 1).item.name == "Pitch Spawn") ripple_field_5 = self.multiworld.get_location("Ripple Field 5 - Animal 2", 1)
self.assertTrue(self.multiworld.get_location("Iceberg 4 - Animal 1", 1).item.name == "ChuChu Spawn") self.assertTrue(ripple_field_5.item is not None and ripple_field_5.item.name == "Pitch Spawn",
self.assertTrue(self.multiworld.get_location("Sand Canyon 6 - Animal 1", 1).item.name in {"Kine Spawn", "Coo 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 rooms = self.multiworld.worlds[1].rooms
copy_abilities = self.multiworld.worlds[1].copy_abilities copy_abilities = self.multiworld.worlds[1].copy_abilities
sand_canyon_5 = self.multiworld.get_region("Sand Canyon 5 - 9", 1) sand_canyon_5 = self.multiworld.get_region("Sand Canyon 5 - 9", 1)
@ -209,7 +279,7 @@ class TestAllShuffle(KDL3TestBase):
else: else:
self.fail("Could not reach Burning Ability before Iceberg 4!") 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 # 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 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 # first we need to identify our bukiset requirements
@ -220,13 +290,13 @@ class TestAllShuffle(KDL3TestBase):
({"Stone Ability", "Burning Ability"}, {'Bukiset (Stone)', 'Bukiset (Burning)'}), ({"Stone Ability", "Burning Ability"}, {'Bukiset (Stone)', 'Bukiset (Burning)'}),
] ]
copy_abilities = self.multiworld.worlds[1].copy_abilities copy_abilities = self.multiworld.worlds[1].copy_abilities
required_abilities: List[Tuple[str]] = [] required_abilities: List[List[str]] = []
for abilities, bukisets in groups: for abilities, bukisets in groups:
potential_abilities: List[str] = list() potential_abilities: List[str] = list()
for bukiset in bukisets: for bukiset in bukisets:
if copy_abilities[bukiset] in abilities: if copy_abilities[bukiset] in abilities:
potential_abilities.append(copy_abilities[bukiset]) potential_abilities.append(copy_abilities[bukiset])
required_abilities.append(tuple(potential_abilities)) required_abilities.append(potential_abilities)
collected_abilities = list() collected_abilities = list()
for group in required_abilities: for group in required_abilities:
self.assertFalse(len(group) == 0, str(self.multiworld.seed)) self.assertFalse(len(group) == 0, str(self.multiworld.seed))
@ -242,4 +312,4 @@ class TestAllShuffle(KDL3TestBase):
self.collect_by_name(["Cutter"]) self.collect_by_name(["Cutter"])
self.assertTrue(self.can_reach_location("Sand Canyon 6 - Professor Hector & R.O.B"), 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}")