Merge remote-tracking branch 'codemann/OverworldShuffle' into codemann_OverworldShuffle

This commit is contained in:
2026-01-25 21:39:53 -06:00
39 changed files with 2714 additions and 418 deletions

View File

@@ -90,7 +90,8 @@ class CustomSettings(object):
args.mystery = True
else:
settings = defaultdict(lambda: None, player_setting)
args.ow_shuffle[p] = get_setting(settings['ow_shuffle'], args.ow_shuffle[p])
args.ow_layout[p] = get_setting(settings['ow_layout'], args.ow_layout[p])
args.ow_parallel[p] = get_setting(settings['ow_parallel'], args.ow_parallel[p])
args.ow_terrain[p] = get_setting(settings['ow_terrain'], args.ow_terrain[p])
args.ow_crossed[p] = get_setting(settings['ow_crossed'], args.ow_crossed[p])
if args.ow_crossed[p] == 'chaos':
@@ -101,6 +102,7 @@ class CustomSettings(object):
args.ow_mixed[p] = get_setting(settings['ow_mixed'], args.ow_mixed[p])
args.ow_whirlpool[p] = get_setting(settings['ow_whirlpool'], args.ow_whirlpool[p])
args.ow_fluteshuffle[p] = get_setting(settings['ow_fluteshuffle'], args.ow_fluteshuffle[p])
args.ow_fog[p] = get_setting(settings['ow_fog'], args.ow_fog[p])
args.shuffle_followers[p] = get_setting(settings['shuffle_followers'], args.shuffle_followers[p])
args.bonk_drops[p] = get_setting(settings['bonk_drops'], args.bonk_drops[p])
args.shuffle[p] = get_setting(settings['shuffle'], args.shuffle[p])
@@ -136,6 +138,14 @@ class CustomSettings(object):
args.take_any[p] = 'random' if args.take_any[p] == 'none' else args.take_any[p]
args.keyshuffle[p] = 'universal'
ow_shuffle = get_setting(settings['ow_shuffle'], args.ow_shuffle[p])
if ow_shuffle == 'parallel':
args.ow_layout = 'wild'
args.ow_parallel = True
elif ow_shuffle == 'full':
args.ow_layout = 'wild'
args.ow_parallel = False
args.mixed_travel[p] = get_setting(settings['mixed_travel'], args.mixed_travel[p])
args.standardize_palettes[p] = get_setting(settings['standardize_palettes'],
args.standardize_palettes[p])
@@ -254,6 +264,11 @@ class CustomSettings(object):
return self.file_source['ow-edges']
return None
def get_owgrid(self):
if 'ow-grid' in self.file_source:
return self.file_source['ow-grid']
return None
def get_owcrossed(self):
if 'ow-crossed' in self.file_source:
return self.file_source['ow-crossed']
@@ -357,13 +372,15 @@ class CustomSettings(object):
self.world_rep['start_inventory'] = start_inv = {}
for p in self.player_range:
settings_dict[p] = {}
settings_dict[p]['ow_shuffle'] = world.owShuffle[p]
settings_dict[p]['ow_layout'] = world.owLayout[p]
settings_dict[p]['ow_parallel'] = world.owParallel[p]
settings_dict[p]['ow_terrain'] = world.owTerrain[p]
settings_dict[p]['ow_crossed'] = world.owCrossed[p]
settings_dict[p]['ow_keepsimilar'] = world.owKeepSimilar[p]
settings_dict[p]['ow_mixed'] = world.owMixed[p]
settings_dict[p]['ow_whirlpool'] = world.owWhirlpoolShuffle[p]
settings_dict[p]['ow_fluteshuffle'] = world.owFluteShuffle[p]
settings_dict[p]['ow_fog'] = world.owFog[p]
settings_dict[p]['shuffle_followers'] = world.shuffle_followers[p]
settings_dict[p]['bonk_drops'] = world.shuffle_bonk_drops[p]
settings_dict[p]['shuffle'] = world.shuffle[p]

View File

@@ -92,13 +92,15 @@ SETTINGSTOPROCESS = {
"bombbag": "bombbag"
},
"overworld": {
"overworldshuffle": "ow_shuffle",
"layout": "ow_layout",
"parallel": "ow_parallel",
"terrain": "ow_terrain",
"crossed": "ow_crossed",
"keepsimilar": "ow_keepsimilar",
"mixed": "ow_mixed",
"whirlpool": "ow_whirlpool",
"overworldflute": "ow_fluteshuffle"
"overworldflute": "ow_fluteshuffle",
"fog": "ow_fog"
},
"entrance": {
"entranceshuffle": "shuffle",

View File

@@ -2116,8 +2116,13 @@ class EnemyTable:
self.room_map = defaultdict(list)
self.special_bitmasks = None
def write_sprite_data_to_rom(self, rom):
pointer_address = snes_to_pc(0x09D62E)
def write_sprite_data_to_rom(self, rom, pointer_table):
ow_data_end = pointer_table['ow_sprites'][0] + pointer_table['ow_sprites'][1]
if ow_data_end > pointer_table['uw_sprites'][2]:
# moving pointer table down
pointer_table['uw_sprites'][2] = ow_data_end
rom.write_bytes(snes_to_pc(pointer_table['uw_sprites'][3]), int16_as_bytes(ow_data_end & 0xFFFF))
pointer_address = snes_to_pc(pointer_table['uw_sprites'][2])
data_pointer = snes_to_pc(0x288000)
empty_pointer = pc_to_snes(data_pointer) & 0xFFFF
rom.write_bytes(data_pointer, [0x00, 0xff])

View File

@@ -183,9 +183,9 @@ def init_sprite_requirements():
SpriteRequirement(EnemySprite.Vulture).no_drop().sub_group(2, 0x12).exclude(NoFlyingRooms),
SpriteRequirement(EnemySprite.CorrectPullSwitch).affix().sub_group(3, [0x52, 0x53]),
SpriteRequirement(EnemySprite.WrongPullSwitch).affix().sub_group(3, [0x52, 0x53]),
SpriteRequirement(EnemySprite.Octorok).aquaphobia().sub_group(2, [0xc, 0x18]),
SpriteRequirement(EnemySprite.Octorok).sub_group(2, [0xc, 0x18]),
SpriteRequirement(EnemySprite.Moldorm).exalt().sub_group(2, 0x30),
SpriteRequirement(EnemySprite.Octorok4Way).aquaphobia().sub_group(2, 0xc),
SpriteRequirement(EnemySprite.Octorok4Way).sub_group(2, 0xc),
SpriteRequirement(EnemySprite.Cucco).immune().sub_group(3, [0x15, 0x50]).exclude(NoFlyingRooms),
SpriteRequirement(EnemySprite.Buzzblob).sub_group(3, 0x11),
SpriteRequirement(EnemySprite.Snapdragon).sub_group(0, 0x16).sub_group(2, 0x17),
@@ -197,7 +197,7 @@ def init_sprite_requirements():
.exclude(NoFlyingRooms).exclude({0x40}), # no anti-fairies in aga tower bridge room
SpriteRequirement(EnemySprite.Wiseman).affix().sub_group(2, 0x4c),
SpriteRequirement(EnemySprite.Hoarder).sub_group(3, 0x11).exclude({0x10c}),
SpriteRequirement(EnemySprite.MiniMoldorm).aquaphobia().sub_group(1, 0x1e),
SpriteRequirement(EnemySprite.MiniMoldorm).sub_group(1, 0x1e),
SpriteRequirement(EnemySprite.Poe).no_drop().sub_group(3, 0x15).exclude(NoFlyingRooms),
SpriteRequirement(EnemySprite.Smithy).affix().sub_group(1, 0x1d).sub_group(3, 0x15),
SpriteRequirement(EnemySprite.Statue).stasis().immune().sub_group(3, [0x52, 0x53]),
@@ -231,12 +231,12 @@ def init_sprite_requirements():
SpriteRequirement(EnemySprite.Hoarder2).sub_group(3, 0x11).exclude({0x10c}),
SpriteRequirement(EnemySprite.TutorialGuard).affix(),
SpriteRequirement(EnemySprite.LightningGate).affix().sub_group(3, 0x3f),
SpriteRequirement(EnemySprite.BlueGuard).aquaphobia().sub_group(1, [0xd, 0x49]).exclude(PitRooms),
SpriteRequirement(EnemySprite.BlueGuard).aquaphobia().sub_group(1, [0xd, 0x49]).sub_group(2, [0x29, 0x13]),
SpriteRequirement(EnemySprite.GreenGuard).aquaphobia().sub_group(1, 0x49).exclude(PitRooms),
SpriteRequirement(EnemySprite.GreenGuard).aquaphobia().sub_group(1, 0x49).sub_group(2, 0x13),
SpriteRequirement(EnemySprite.RedSpearGuard).aquaphobia().sub_group(1, [0xd, 0x49]).exclude(PitRooms),
SpriteRequirement(EnemySprite.RedSpearGuard).aquaphobia().sub_group(1, [0xd, 0x49]).sub_group(2, [0x29, 0x13]),
SpriteRequirement(EnemySprite.BlueGuard).sub_group(1, [0xd, 0x49]).exclude(PitRooms),
SpriteRequirement(EnemySprite.BlueGuard).sub_group(1, [0xd, 0x49]).sub_group(2, [0x29, 0x13]),
SpriteRequirement(EnemySprite.GreenGuard).sub_group(1, 0x49).exclude(PitRooms),
SpriteRequirement(EnemySprite.GreenGuard).sub_group(1, 0x49).sub_group(2, 0x13),
SpriteRequirement(EnemySprite.RedSpearGuard).sub_group(1, [0xd, 0x49]).exclude(PitRooms),
SpriteRequirement(EnemySprite.RedSpearGuard).sub_group(1, [0xd, 0x49]).sub_group(2, [0x29, 0x13]),
SpriteRequirement(EnemySprite.BluesainBolt).aquaphobia().sub_group(0, 0x46).sub_group(1, [0xd, 0x49]),
SpriteRequirement(EnemySprite.UsainBolt).aquaphobia().sub_group(1, [0xd, 0x49]),
SpriteRequirement(EnemySprite.BlueArcher).sub_group(0, 0x48).sub_group(1, 0x49),

View File

@@ -147,139 +147,149 @@ door_addresses = {'Links House': (0x00, (0x0104, 0x2c
'Lake Hylia Fortune Teller': (0x72, (0x0122, 0x35, 0x0380, 0x0c6a, 0x0a00, 0x0cb8, 0x0a58, 0x0cd7, 0x0a85, 0x06, 0xfa, 0x0000, 0x0000), 0x00),
'Kakariko Gamble Game': (0x66, (0x0118, 0x29, 0x069e, 0x0ac4, 0x02ea, 0x0b18, 0x0368, 0x0b33, 0x036f, 0x0a, 0xf6, 0x09AC, 0x0000), 0x00)}
ow_prize_table = {'Links House': (0x8b1, 0xb2d),
'Desert Palace Entrance (South)': (0x108, 0xd70), 'Desert Palace Entrance (West)': (0x031, 0xca0),
'Desert Palace Entrance (North)': (0x0e1, 0xba0), 'Desert Palace Entrance (East)': (0x191, 0xca0),
'Eastern Palace': (0xf31, 0x620), 'Tower of Hera': (0x8D0, 0x080),
'Hyrule Castle Entrance (South)': (0x820, 0x730), 'Hyrule Castle Entrance (West)': (0x740, 0x5D0),
'Hyrule Castle Entrance (East)': (0x8f0, 0x5D0),
'Agahnims Tower': (0x820, 0x5D0),
'Thieves Town': (0x1d0, 0x780), 'Skull Woods First Section Door': (0x2e0, 0x280),
'Skull Woods Second Section Door (East)': (0x200, 0x240),
'Skull Woods Second Section Door (West)': (0x0c0, 0x1c0),
'Skull Woods Final Section': (0x082, 0x0b0),
'Skull Woods First Section Hole (West)': (0x200, 0x2b0),
'Skull Woods First Section Hole (East)': (0x340, 0x2e0),
'Skull Woods First Section Hole (North)': (0x320, 0x1e0),
'Skull Woods Second Section Hole': (0x0f0, 0x0b0),
'Ice Palace': (0xca0, 0xda0),
'Misery Mire': (0x100, 0xca0),
'Palace of Darkness': (0xf40, 0x620), 'Swamp Palace': (0x759, 0xED0),
'Turtle Rock': (0xf11, 0x103),
'Dark Death Mountain Ledge (West)': (0xb80, 0x180),
'Dark Death Mountain Ledge (East)': (0xc80, 0x180),
'Turtle Rock Isolated Ledge Entrance': (0xc00, 0x240),
ow_prize_table = {'Lost Woods Gamble': (0x2A0, 0x080),
'Lost Woods Hideout Drop': (0x338, 0x218),
'Lost Woods Hideout Stump': (0x2A0, 0x2E0),
'Lumberjack House': (0x538, 0x158),
'Lumberjack Tree Tree': (0x4A0, 0x160),
'Lumberjack Tree Cave': (0x550, 0x004),
'Tower of Hera': (0x900, 0x0E0),
'Spectacle Rock Cave Peak': (0x758, 0x1B7),
'Spectacle Rock Cave (Bottom)': (0x6F0, 0x257),
'Spectacle Rock Cave': (0x7C8, 0x257),
'Death Mountain Return Cave (East)': (0x648, 0x278),
'Old Man Cave (East)': (0x658, 0x387),
'Old Man House (Top)': (0x8C8, 0x2D8),
'Old Man House (Bottom)': (0x728, 0x3A7),
'Paradox Cave (Top)': (0xDE0, 0x100),
'Paradox Cave (Bottom)': (0xDC8, 0x358),
'Paradox Cave (Middle)': (0xDDF, 0x3FF),
'Fairy Ascension Cave (Top)': (0xCD8, 0x2B8),
'Fairy Ascension Cave (Bottom)': (0xC78, 0x358),
'Spiral Cave': (0xC78, 0x207),
'Spiral Cave (Bottom)': (0xBC8, 0x2F7),
'Mimic Cave': (0xD38, 0x207),
'Hookshot Fairy': (0xD38, 0x358),
'Death Mountain Return Cave (West)': (0x5B8, 0x247),
'Old Man Cave (West)': (0x5B8, 0x348),
'Waterfall of Wishing': (0xE80, 0x2C7),
'Fortune Teller (Light)': (0x308, 0x5A8),
'Bonk Rock Cave': (0x600, 0x517),
'Sanctuary': (0x748, 0x4F4),
'Sanctuary Grave': (0x860, 0x550),
'Graveyard Cave': (0x8F8, 0x4A6),
'Kings Grave': (0x978, 0x566),
'North Fairy Cave Drop': (0xA40, 0x580),
'North Fairy Cave': (0xAA8, 0x4A8),
'Potion Shop': (0xCB8, 0x598),
'Kakariko Well Drop': (0x030, 0x6C0),
'Kakariko Well Cave': (0x0D8, 0x6F7),
'Blinds Hideout': (0x1B8, 0x6F7),
'Elder House (West)': (0x258, 0x6F7),
'Elder House (East)': (0x2E8, 0x6F7),
'Snitch Lady (West)': (0x0C8, 0x7E8),
'Snitch Lady (East)': (0x338, 0x7E8),
'Chicken House': (0x188, 0x8E8),
'Sick Kids House': (0x278, 0x8E8),
'Bush Covered House': (0x338, 0x8E8),
'Light World Bomb Hut': (0x068, 0x9D7),
'Kakariko Shop': (0x1C8, 0x9D7),
'Tavern North': (0x2B8, 0x977),
'Tavern (Front)': (0x2B8, 0x9F7),
'Agahnims Tower': (0x7F0, 0x630),
'Hyrule Castle Entrance (South)': (0x7F0, 0x750),
'Hyrule Castle Entrance (West)': (0x758, 0x644),
'Hyrule Castle Entrance (East)': (0x8E8, 0x644),
'Hyrule Castle Secret Entrance Drop': (0x9D0, 0x680),
'Hyrule Castle Secret Entrance Stairs': (0x8D0, 0x700),
'Kakariko Well Drop': (0x030, 0x680),
'Kakariko Well Cave': (0x060, 0x680),
'Bat Cave Drop': (0x520, 0x8f0),
'Bat Cave Cave': (0x560, 0x940),
'Elder House (East)': (0x2b0, 0x6a0),
'Elder House (West)': (0x230, 0x6a0),
'North Fairy Cave Drop': (0xa40, 0x500),
'North Fairy Cave': (0xa80, 0x440),
'Lost Woods Hideout Drop': (0x290, 0x200),
'Lost Woods Hideout Stump': (0x240, 0x280),
'Lumberjack Tree Tree': (0x4e0, 0x140),
'Lumberjack Tree Cave': (0x560, 0x004),
'Two Brothers House (East)': (0x200, 0x0b60),
'Two Brothers House (West)': (0x180, 0x0b60),
'Sanctuary Grave': (0x820, 0x4c0),
'Sanctuary': (0x720, 0x4a0),
'Old Man Cave (West)': (0x580, 0x2c0),
'Old Man Cave (East)': (0x620, 0x2c0),
'Old Man House (Bottom)': (0x720, 0x320),
'Old Man House (Top)': (0x820, 0x220),
'Death Mountain Return Cave (East)': (0x600, 0x220),
'Death Mountain Return Cave (West)': (0x500, 0x1c0),
'Spectacle Rock Cave Peak': (0x720, 0x0a0),
'Spectacle Rock Cave': (0x790, 0x1a0),
'Spectacle Rock Cave (Bottom)': (0x710, 0x0a0),
'Paradox Cave (Bottom)': (0xd80, 0x180),
'Paradox Cave (Middle)': (0xd80, 0x380),
'Paradox Cave (Top)': (0xd80, 0x020),
'Fairy Ascension Cave (Bottom)': (0xcc8, 0x2a0),
'Fairy Ascension Cave (Top)': (0xc00, 0x240),
'Spiral Cave': (0xb80, 0x180),
'Spiral Cave (Bottom)': (0xb80, 0x2c0),
'Bumper Cave (Bottom)': (0x580, 0x2c0),
'Bumper Cave (Top)': (0x500, 0x1c0),
'Superbunny Cave (Top)': (0xd80, 0x020),
'Superbunny Cave (Bottom)': (0xd00, 0x180),
'Hookshot Cave': (0xc80, 0x0c0),
'Hookshot Cave Back Entrance': (0xcf0, 0x004),
'Ganons Tower': (0x8D0, 0x080),
'Pyramid Hole': (0x820, 0x680),
'Inverted Pyramid Hole': (0x820, 0x680),
'Pyramid Entrance': (0x640, 0x7c0),
'Inverted Pyramid Entrance': (0x6C0, 0x5D0),
'Waterfall of Wishing': (0xe80, 0x280),
'Dam': (0x759, 0xED0),
'Blinds Hideout': (0x190, 0x6c0),
'Bonk Fairy (Light)': (0x740, 0xa80),
'Lake Hylia Fairy': (0xd40, 0x9f0),
'Light Hype Fairy': (0x940, 0xc80),
'Desert Fairy': (0x420, 0xe00),
'Kings Grave': (0x920, 0x520),
'Tavern North': (0x270, 0x900),
'Chicken House': (0x120, 0x880),
'Aginahs Cave': (0x2e0, 0xd00),
'Sahasrahlas Hut': (0xcf0, 0x6c0),
'Lake Hylia Shop': (0xbc0, 0xc00),
'Capacity Upgrade': (0xca0, 0xda0),
'Blacksmiths Hut': (0x4a0, 0x880),
'Sick Kids House': (0x220, 0x880),
'Lost Woods Gamble': (0x240, 0x080),
'Fortune Teller (Light)': (0x2c0, 0x4c0),
'Snitch Lady (East)': (0x310, 0x7a0),
'Snitch Lady (West)': (0x080, 0x7a0),
'Bush Covered House': (0x2e0, 0x880),
'Tavern (Front)': (0x270, 0x980),
'Light World Bomb Hut': (0x070, 0x980),
'Kakariko Shop': (0x170, 0x980),
'Cave 45': (0x440, 0xca0), 'Graveyard Cave': (0x8f0, 0x430),
'Checkerboard Cave': (0x260, 0xc00),
'Mini Moldorm Cave': (0xa40, 0xe80),
'Long Fairy Cave': (0xf60, 0xb00),
'Good Bee Cave': (0xec0, 0xc00),
'20 Rupee Cave': (0xe80, 0xca0),
'50 Rupee Cave': (0x4d0, 0xed0),
'Ice Rod Cave': (0xe00, 0xc00),
'Bonk Rock Cave': (0x5f0, 0x460),
'Library': (0x270, 0xaa0),
'Potion Shop': (0xc80, 0x4c0),
'Hookshot Fairy': (0xd00, 0x180),
'Pyramid Fairy': (0x740, 0x740),
'East Dark World Hint': (0xf60, 0xb00),
'Palace of Darkness Hint': (0xd60, 0x7c0),
'Dark Lake Hylia Fairy': (0xd40, 0x9f0),
'Dark Lake Hylia Ledge Fairy': (0xe00, 0xc00),
'Dark Lake Hylia Ledge Spike Cave': (0xe80, 0xca0),
'Dark Lake Hylia Ledge Hint': (0xec0, 0xc00),
'Hype Cave': (0x940, 0xc80),
'Bonk Fairy (Dark)': (0x740, 0xa80),
'Brewery': (0x170, 0x980), 'C-Shaped House': (0x310, 0x7a0), 'Chest Game': (0x080, 0x7a0),
'Hammer Peg Cave': (0x4c0, 0x940),
'Red Shield Shop': (0x500, 0x680),
'Dark Sanctuary Hint': (0x720, 0x4a0),
'Fortune Teller (Dark)': (0x2c0, 0x4c0),
'Dark World Shop': (0x2e0, 0x880),
'Dark Lumberjack Shop': (0x4e0, 0x0d0),
'Dark Potion Shop': (0xc80, 0x4c0),
'Archery Game': (0x2f0, 0xaf0),
'Mire Shed': (0x060, 0xc90),
'Mire Hint': (0x2e0, 0xd00),
'Mire Fairy': (0x1c0, 0xc90),
'Spike Cave': (0x860, 0x180),
'Dark Death Mountain Shop': (0xd80, 0x180),
'Dark Death Mountain Fairy': (0x620, 0x2c0),
'Mimic Cave': (0xc80, 0x180),
'Big Bomb Shop': (0x8b1, 0xb2d),
'Dark Lake Hylia Shop': (0xa40, 0xc40),
'Lumberjack House': (0x580, 0x100),
'Lake Hylia Fortune Teller': (0xa40, 0xc40),
'Kakariko Gamble Game': (0x2f0, 0xaf0)}
'Hyrule Castle Secret Entrance Stairs': (0x8C8, 0x718),
'Inverted Pyramid Hole': (0x7F0, 0x6B0),
'Inverted Pyramid Entrance': (0x6E8, 0x644),
'Sahasrahlas Hut': (0xCF0, 0x747),
'Eastern Palace': (0xF40, 0x680),
'Blacksmiths Hut': (0x4E8, 0x8C8),
'Bat Cave Drop': (0x538, 0x9A8),
'Bat Cave Cave': (0x510, 0x930),
'Two Brothers House (West)': (0x1B8, 0xB88),
'Two Brothers House (East)': (0x238, 0xB88),
'Library': (0x2B8, 0xAA7),
'Kakariko Gamble Game': (0x348, 0xB78),
'Bonk Fairy (Light)': (0x788, 0xA87),
'Links House': (0x8B8, 0xB58),
'Lake Hylia Fairy': (0xD58, 0xA57),
'Long Fairy Cave': (0xF68, 0xB58),
'Desert Palace Entrance (North)': (0x148, 0xC28),
'Desert Palace Entrance (West)': (0x088, 0xCF8),
'Desert Palace Entrance (East)': (0x1E8, 0xCF8),
'Desert Palace Entrance (South)': (0x138, 0xD18),
'Checkerboard Cave': (0x2E8, 0xCF7),
'Aginahs Cave': (0x388, 0xDE8),
'Cave 45': (0x448, 0xD86),
'Light Hype Fairy': (0x988, 0xCF8),
'Lake Hylia Fortune Teller': (0xA68, 0xCC8),
'Lake Hylia Shop': (0xBF8, 0xC66),
'Capacity Upgrade': (0xCC8, 0xDE8),
'Mini Moldorm Cave': (0xA88, 0xEF8),
'Ice Rod Cave': (0xE68, 0xC37),
'Good Bee Cave': (0xEF8, 0xC36),
'20 Rupee Cave': (0xEB8, 0xCC6),
'Desert Fairy': (0x488, 0xE76),
'50 Rupee Cave': (0x4F8, 0xF47),
'Dam': (0x778, 0xF68),
'Skull Woods Final Section': (0x078, 0x0A8),
'Skull Woods Second Section Door (West)': (0x0E0, 0x1C0),
'Skull Woods Second Section Hole': (0x150, 0x120),
'Skull Woods Second Section Door (East)': (0x208, 0x208),
'Skull Woods First Section Door': (0x2C8, 0x2A8),
'Skull Woods First Section Hole (North)': (0x320, 0x1e0),
'Skull Woods First Section Hole (West)': (0x240, 0x380),
'Skull Woods First Section Hole (East)': (0x340, 0x380),
'Dark Lumberjack Shop': (0x538, 0x177),
'Ganons Tower': (0x900, 0x0E0),
'Dark Death Mountain Fairy': (0x658, 0x387),
'Spike Cave': (0x9F8, 0x268),
'Hookshot Cave Back Entrance': (0xD38, 0x038),
'Hookshot Cave': (0xD48, 0x107),
'Superbunny Cave (Top)': (0xDE0, 0x100),
'Superbunny Cave (Bottom)': (0xD38, 0x358),
'Dark Death Mountain Ledge (West)': (0xC78, 0x207),
'Dark Death Mountain Ledge (East)': (0xD38, 0x207),
'Turtle Rock Isolated Ledge Entrance': (0xCD8, 0x2B8),
'Dark Death Mountain Shop': (0xDC8, 0x358),
'Turtle Rock': (0xF48, 0x108),
'Bumper Cave (Top)': (0x5B8, 0x247),
'Bumper Cave (Bottom)': (0x5B8, 0x348),
'Fortune Teller (Dark)': (0x308, 0x5A8),
'Dark Sanctuary Hint': (0x748, 0x4F4),
'Dark Potion Shop': (0xCB8, 0x598),
'Chest Game': (0x0C8, 0x7E8),
'Thieves Town': (0x1F0, 0x7E8),
'C-Shaped House': (0x338, 0x7E8),
'Brewery': (0x1C8, 0x9D7),
'Dark World Shop': (0x338, 0x8E8),
'Red Shield Shop': (0x538, 0x778),
'Pyramid Hole': (0x800, 0x680),
'Pyramid Entrance': (0x718, 0x7A8),
'Pyramid Fairy': (0x7B8, 0x7A8),
'Palace of Darkness': (0xF40, 0x680),
'Palace of Darkness Hint': (0xD70, 0x878),
'Hammer Peg Cave': (0x528, 0x9B6),
'Archery Game': (0x348, 0xB78),
'Bonk Fairy (Dark)': (0x788, 0xA87),
'Big Bomb Shop': (0x8B8, 0xB58),
'Dark Lake Hylia Fairy': (0xD58, 0xA57),
'East Dark World Hint': (0xF68, 0xB58),
'Mire Shed': (0x0A8, 0xCC7),
'Misery Mire': (0x148, 0xCC7),
'Mire Fairy': (0x1E8, 0xCC7),
'Mire Hint': (0x388, 0xDE8),
'Hype Cave': (0x988, 0xCF8),
'Dark Lake Hylia Shop': (0xA68, 0xCC8),
'Ice Palace': (0xCA8, 0xE28),
'Dark Lake Hylia Ledge Fairy': (0xE68, 0xC37),
'Dark Lake Hylia Ledge Hint': (0xEF8, 0xC36),
'Dark Lake Hylia Ledge Spike Cave': (0xEB8, 0xCC6),
'Swamp Palace': (0x778, 0xF68)}
default_connector_connections = [('Death Mountain Return Cave (West)', 'Death Mountain Return Cave Exit (West)'),
('Death Mountain Return Cave (East)', 'Death Mountain Return Cave Exit (East)'),

View File

@@ -634,7 +634,7 @@ def do_dark_sanc(entrances, exits, avail):
forbidden.append('Links House')
else:
forbidden.append('Big Bomb Shop')
if avail.world.owShuffle[avail.player] == 'vanilla':
if avail.world.owLayout[avail.player] == 'vanilla':
choices = [e for e in avail.world.districts[avail.player]['Northwest Dark World'].entrances if e not in forbidden and e in entrances]
else:
choices = [e for e in get_starting_entrances(avail) if e not in forbidden and e in entrances]
@@ -678,7 +678,7 @@ def do_links_house(entrances, exits, avail, cross_world):
forbidden.append(links_house_vanilla)
forbidden.extend(Forbidden_Swap_Entrances)
shuffle_mode = avail.world.shuffle[avail.player]
if avail.world.owShuffle[avail.player] == 'vanilla':
if avail.world.owLayout[avail.player] == 'vanilla':
# simple shuffle -
if shuffle_mode == 'simple':
avail.links_on_mountain = True # taken care of by the logic below
@@ -732,7 +732,7 @@ def do_links_house(entrances, exits, avail, cross_world):
# links on dm
dm_spots = LH_DM_Connector_List.union(LH_DM_Exit_Forbidden)
if links_house in dm_spots and avail.world.owShuffle[avail.player] == 'vanilla':
if links_house in dm_spots and avail.world.owLayout[avail.player] == 'vanilla':
if avail.links_on_mountain:
return # connector is fine
logging.getLogger('').warning(f'Links House is placed in tight area and is now unhandled. Report any errors that occur from here.')

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,473 @@
import logging
import os
from datetime import datetime
from typing import Dict, List
from PIL import Image, ImageDraw
from BaseClasses import Direction, OWEdge
from source.overworld.LayoutGenerator import Screen
def get_edge_lists(grid: List[List[List[int]]],
overworld_screens: Dict[int, Screen],
large_screen_quadrant_info: Dict[int, Dict]) -> Dict:
"""
Get list of edges for each cell and direction.
Args:
grid: 3D list [world][row][col] containing screen IDs
overworld_screens: Dict of screen_id -> Screen objects
large_screen_quadrant_info: Dict of screen_id -> quadrant info for large screens
Returns:
Dict mapping (world, row, col, direction) -> list of edges
Each edge has a .dest property (None if unconnected)
"""
GRID_SIZE = 8
edge_lists = {}
# Large screen base IDs
large_screen_base_ids = [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35,
0x40, 0x43, 0x45, 0x58, 0x5B, 0x5E, 0x70, 0x75]
for world_idx in range(2):
# Build a map of screen_id -> list of (row, col) positions for large screens
large_screen_positions = {}
for row in range(GRID_SIZE):
for col in range(GRID_SIZE):
screen_id = grid[world_idx][row][col]
if screen_id != -1 and screen_id in large_screen_base_ids:
if screen_id not in large_screen_positions:
large_screen_positions[screen_id] = []
large_screen_positions[screen_id].append((row, col))
for row in range(GRID_SIZE):
for col in range(GRID_SIZE):
screen_id = grid[world_idx][row][col]
if screen_id == -1:
# Empty cell - no edges
for direction in [Direction.North, Direction.South, Direction.East, Direction.West]:
edge_lists[(world_idx, row, col, direction)] = []
continue
screen = overworld_screens.get(screen_id)
if not screen:
for direction in [Direction.North, Direction.South, Direction.East, Direction.West]:
edge_lists[(world_idx, row, col, direction)] = []
continue
is_large = screen_id in large_screen_base_ids
if is_large:
# For large screens, determine which quadrant this cell is
# Find all positions of this large screen and determine quadrant
positions = large_screen_positions.get(screen_id, [(row, col)])
# Determine quadrant by finding relative position
# The quadrant is determined by which cells are adjacent
quadrant = determine_large_screen_quadrant(row, col, positions, GRID_SIZE)
# Get edges for this quadrant
if screen_id in large_screen_quadrant_info:
quad_info = large_screen_quadrant_info[screen_id]
for direction in [Direction.North, Direction.South, Direction.East, Direction.West]:
edges = quad_info.get(quadrant, {}).get(direction, [])
edge_lists[(world_idx, row, col, direction)] = edges
else:
# No quadrant info - no edges
for direction in [Direction.North, Direction.South, Direction.East, Direction.West]:
edge_lists[(world_idx, row, col, direction)] = []
else:
# Small screen - get edges directly
for direction in [Direction.North, Direction.South, Direction.East, Direction.West]:
edges_in_dir = [e for e in screen.edges.values() if e.direction == direction]
edge_lists[(world_idx, row, col, direction)] = edges_in_dir
return edge_lists
def determine_large_screen_quadrant(row: int, col: int, positions: List[tuple], grid_size: int) -> str:
"""
Determine which quadrant (NW, NE, SW, SE) a cell is in for a large screen.
Handles wrapping correctly by checking adjacency patterns.
Args:
row: Current cell row
col: Current cell column
positions: List of all (row, col) positions for this large screen
grid_size: Size of the grid (8)
Returns:
Quadrant string: "NW", "NE", "SW", or "SE"
"""
positions_set = set(positions)
# Check which adjacent cells also belong to this large screen
has_right = ((row, (col + 1) % grid_size) in positions_set)
has_below = (((row + 1) % grid_size, col) in positions_set)
has_left = ((row, (col - 1) % grid_size) in positions_set)
has_above = (((row - 1) % grid_size, col) in positions_set)
# Determine quadrant based on adjacency
# NW: has right and below neighbors
# NE: has left and below neighbors
# SW: has right and above neighbors
# SE: has left and above neighbors
if has_right and has_below:
return "NW"
elif has_left and has_below:
return "NE"
elif has_right and has_above:
return "SW"
elif has_left and has_above:
return "SE"
else:
raise Exception("?")
def is_crossed_edge(edge: OWEdge, overworld_screens: Dict[int, Screen]) -> bool:
if edge.dest is None:
return False
source_screen = overworld_screens.get(edge.owIndex)
dest_screen = overworld_screens.get(edge.dest.owIndex)
return source_screen.dark_world != dest_screen.dark_world
def visualize_layout(grid: List[List[List[int]]], output_dir: str,
overworld_screens: Dict[int, Screen],
large_screen_quadrant_info: Dict[int, Dict]) -> None:
# Constants
GRID_SIZE = 8
BORDER_WIDTH = 1
OUTPUT_CELL_SIZE = 64 # Each cell in output is always 64x64 pixels
# Load the world images
try:
lightworld_img = Image.open("data/overworld/lightworld.png")
darkworld_img = Image.open("data/overworld/darkworld.png")
except FileNotFoundError as e:
raise FileNotFoundError(f"World image not found: {e}. Ensure lightworld.png and darkworld.png are in the data/overworld directory.")
# Calculate source cell size from the base images
# Each world image is 8x8 screens, so divide by 8 to get source cell size
img_width, _ = lightworld_img.size
SOURCE_CELL_SIZE = img_width // GRID_SIZE # Size of each cell in the source image
# Calculate dimensions for the output (always based on 64x64 cells)
world_width = GRID_SIZE * OUTPUT_CELL_SIZE
world_height = GRID_SIZE * OUTPUT_CELL_SIZE
# Create output image (two worlds side by side with a small gap)
gap = 32
output_width = world_width * 2 + gap
output_height = world_height
output_img = Image.new('RGB', (output_width, output_height), color='black')
# Large screen base IDs (defined once for reuse)
large_screen_base_ids = [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35,
0x40, 0x43, 0x45, 0x58, 0x5B, 0x5E, 0x70, 0x75]
# Process both worlds
for world_idx in range(2):
x_offset = 0 if world_idx == 0 else (world_width + gap)
# Build a map of screen_id -> list of (row, col) positions for large screens
large_screen_positions = {}
for row in range(GRID_SIZE):
for col in range(GRID_SIZE):
screen_id = grid[world_idx][row][col]
if screen_id != -1 and screen_id in large_screen_base_ids:
if screen_id not in large_screen_positions:
large_screen_positions[screen_id] = []
large_screen_positions[screen_id].append((row, col))
# Process each cell in the grid individually
# This handles wrapped large screens correctly by drawing each quadrant separately
for row in range(GRID_SIZE):
for col in range(GRID_SIZE):
screen_id = grid[world_idx][row][col]
if screen_id == -1:
# Empty cell - fill with black (already black from initialization)
continue
is_large = screen_id in large_screen_base_ids
# Calculate source position in the world image
source_row = (screen_id % 0x40) >> 3
source_col = screen_id % 0x08
world_img = lightworld_img if screen_id < 0x40 else darkworld_img
if is_large:
# For large screens, determine which quadrant this cell represents
positions = large_screen_positions.get(screen_id, [(row, col)])
quadrant = determine_large_screen_quadrant(row, col, positions, GRID_SIZE)
# Map quadrant to source offset within the 2x2 large screen
quadrant_offsets = {
"NW": (0, 0),
"NE": (1, 0),
"SW": (0, 1),
"SE": (1, 1)
}
q_col_offset, q_row_offset = quadrant_offsets[quadrant]
# Calculate source position for this quadrant
source_x = (source_col + q_col_offset) * SOURCE_CELL_SIZE
source_y = (source_row + q_row_offset) * SOURCE_CELL_SIZE
# Crop single cell from source (the specific quadrant)
cropped = world_img.crop((
source_x,
source_y,
source_x + SOURCE_CELL_SIZE,
source_y + SOURCE_CELL_SIZE
))
else:
# Small screen (1x1)
source_x = source_col * SOURCE_CELL_SIZE
source_y = source_row * SOURCE_CELL_SIZE
# Crop single cell from source
cropped = world_img.crop((
source_x,
source_y,
source_x + SOURCE_CELL_SIZE,
source_y + SOURCE_CELL_SIZE
))
# Resize to output size (64x64 pixels)
resized = cropped.resize(
(OUTPUT_CELL_SIZE, OUTPUT_CELL_SIZE),
Image.LANCZOS
)
# Paste into output at grid position
dest_x = x_offset + col * OUTPUT_CELL_SIZE
dest_y = row * OUTPUT_CELL_SIZE
output_img.paste(resized, (dest_x, dest_y))
edge_lists = get_edge_lists(grid, overworld_screens, large_screen_quadrant_info)
# Draw borders and edge connection indicators after all screens are placed
draw = ImageDraw.Draw(output_img)
# Size of the indicator squares
INDICATOR_SIZE = 12
for world_idx in range(2):
x_offset = 0 if world_idx == 0 else (world_width + gap)
# Build large screen positions map for this world
large_screen_positions = {}
for row in range(GRID_SIZE):
for col in range(GRID_SIZE):
screen_id = grid[world_idx][row][col]
if screen_id != -1 and screen_id in large_screen_base_ids:
if screen_id not in large_screen_positions:
large_screen_positions[screen_id] = []
large_screen_positions[screen_id].append((row, col))
# Draw borders for each cell
for row in range(GRID_SIZE):
for col in range(GRID_SIZE):
screen_id = grid[world_idx][row][col]
if screen_id == -1:
continue
is_large = screen_id in large_screen_base_ids
dest_x = x_offset + col * OUTPUT_CELL_SIZE
dest_y = row * OUTPUT_CELL_SIZE
if is_large:
# For large screens, determine which quadrant this cell is
positions = large_screen_positions.get(screen_id, [(row, col)])
quadrant = determine_large_screen_quadrant(row, col, positions, GRID_SIZE)
# Draw border only on the outer edges of the large screen
# (not on internal edges between quadrants)
# NW: draw top and left borders
# NE: draw top and right borders
# SW: draw bottom and left borders
# SE: draw bottom and right borders
if quadrant in ["NW", "NE"]:
# Draw top border
draw.line([(dest_x, dest_y), (dest_x + OUTPUT_CELL_SIZE - 1, dest_y)], fill='black', width=BORDER_WIDTH)
if quadrant in ["SW", "SE"]:
# Draw bottom border
draw.line([(dest_x, dest_y + OUTPUT_CELL_SIZE - 1), (dest_x + OUTPUT_CELL_SIZE - 1, dest_y + OUTPUT_CELL_SIZE - 1)], fill='black', width=BORDER_WIDTH)
if quadrant in ["NW", "SW"]:
# Draw left border
draw.line([(dest_x, dest_y), (dest_x, dest_y + OUTPUT_CELL_SIZE - 1)], fill='black', width=BORDER_WIDTH)
if quadrant in ["NE", "SE"]:
# Draw right border
draw.line([(dest_x + OUTPUT_CELL_SIZE - 1, dest_y), (dest_x + OUTPUT_CELL_SIZE - 1, dest_y + OUTPUT_CELL_SIZE - 1)], fill='black', width=BORDER_WIDTH)
else:
# Small screen - draw border around single cell
draw.rectangle(
[dest_x, dest_y, dest_x + OUTPUT_CELL_SIZE - 1, dest_y + OUTPUT_CELL_SIZE - 1],
outline='black',
width=BORDER_WIDTH
)
# Draw edge connection indicators for each cell
for row in range(GRID_SIZE):
for col in range(GRID_SIZE):
screen_id = grid[world_idx][row][col]
if screen_id == -1:
continue
dest_x = x_offset + col * OUTPUT_CELL_SIZE
dest_y = row * OUTPUT_CELL_SIZE
# Draw indicator for each direction (only if edges exist)
# Use bright colors for visibility
GREEN = (0, 255, 0) # Bright green
YELLOW = (255, 255, 0) # Bright yellow
RED = (255, 0, 0) # Bright red
# North indicators - positioned based on edge midpoint
north_edges = edge_lists.get((world_idx, row, col, Direction.North), [])
if north_edges:
north_y = dest_y # Touch the top border
for edge in north_edges:
# For north/south edges, midpoint gives the X coordinate
# Take midpoint modulo 0x0200, range 0-0x01FF maps to full side
midpoint = edge.midpoint % 0x0200
# Map from game coordinate range to pixel position
edge_x_offset = (midpoint * OUTPUT_CELL_SIZE) // 0x0200
edge_x = dest_x + edge_x_offset - INDICATOR_SIZE // 2
edge_color = GREEN if edge.dest is not None else YELLOW if any(e for e in north_edges if e.dest) else RED
draw.rectangle(
[edge_x, north_y, edge_x + INDICATOR_SIZE - 1, north_y + INDICATOR_SIZE - 1],
fill=edge_color,
outline='black'
)
# Draw diagonal cross if edge crosses between worlds
if edge.dest is not None and is_crossed_edge(edge, overworld_screens):
draw.line(
[edge_x, north_y, edge_x + INDICATOR_SIZE - 1, north_y + INDICATOR_SIZE - 1],
fill='black',
width=1
)
draw.line(
[edge_x + INDICATOR_SIZE - 1, north_y, edge_x, north_y + INDICATOR_SIZE - 1],
fill='black',
width=1
)
# South indicators - positioned based on edge midpoint
south_edges = edge_lists.get((world_idx, row, col, Direction.South), [])
if south_edges:
south_y = dest_y + OUTPUT_CELL_SIZE - INDICATOR_SIZE # Touch the bottom border
for edge in south_edges:
# For north/south edges, midpoint gives the X coordinate
# Take midpoint modulo 0x0200, range 0-0x01FF maps to full side
midpoint = edge.midpoint % 0x0200
# Map from game coordinate range to pixel position
edge_x_offset = (midpoint * OUTPUT_CELL_SIZE) // 0x0200
edge_x = dest_x + edge_x_offset - INDICATOR_SIZE // 2
edge_color = GREEN if edge.dest is not None else YELLOW if any(e for e in south_edges if e.dest) else RED
draw.rectangle(
[edge_x, south_y, edge_x + INDICATOR_SIZE - 1, south_y + INDICATOR_SIZE - 1],
fill=edge_color,
outline='black'
)
# Draw diagonal cross if edge crosses between worlds
if edge.dest is not None and is_crossed_edge(edge, overworld_screens):
draw.line(
[edge_x, south_y, edge_x + INDICATOR_SIZE - 1, south_y + INDICATOR_SIZE - 1],
fill='black',
width=1
)
draw.line(
[edge_x + INDICATOR_SIZE - 1, south_y, edge_x, south_y + INDICATOR_SIZE - 1],
fill='black',
width=1
)
# West indicators - positioned based on edge midpoint
west_edges = edge_lists.get((world_idx, row, col, Direction.West), [])
if west_edges:
west_x = dest_x # Touch the left border
for edge in west_edges:
# For west/east edges, midpoint gives the Y coordinate
# Take midpoint modulo 0x0200, range 0-0x01FF maps to full side
midpoint = edge.midpoint % 0x0200
# Map from game coordinate range to pixel position
edge_y_offset = (midpoint * OUTPUT_CELL_SIZE) // 0x0200
edge_y = dest_y + edge_y_offset - INDICATOR_SIZE // 2
edge_color = GREEN if edge.dest is not None else YELLOW if any(e for e in west_edges if e.dest) else RED
draw.rectangle(
[west_x, edge_y, west_x + INDICATOR_SIZE - 1, edge_y + INDICATOR_SIZE - 1],
fill=edge_color,
outline='black'
)
# Draw diagonal cross if edge crosses between worlds
if edge.dest is not None and is_crossed_edge(edge, overworld_screens):
draw.line(
[west_x, edge_y, west_x + INDICATOR_SIZE - 1, edge_y + INDICATOR_SIZE - 1],
fill='black',
width=1
)
draw.line(
[west_x + INDICATOR_SIZE - 1, edge_y, west_x, edge_y + INDICATOR_SIZE - 1],
fill='black',
width=1
)
# East indicators - positioned based on edge midpoint
east_edges = edge_lists.get((world_idx, row, col, Direction.East), [])
if east_edges:
east_x = dest_x + OUTPUT_CELL_SIZE - INDICATOR_SIZE # Touch the right border
for edge in east_edges:
# For west/east edges, midpoint gives the Y coordinate
# Take midpoint modulo 0x0200, range 0-0x01FF maps to full side
midpoint = edge.midpoint % 0x0200
# Map from game coordinate range to pixel position
edge_y_offset = (midpoint * OUTPUT_CELL_SIZE) // 0x0200
edge_y = dest_y + edge_y_offset - INDICATOR_SIZE // 2
edge_color = GREEN if edge.dest is not None else YELLOW if any(e for e in east_edges if e.dest) else RED
draw.rectangle(
[east_x, edge_y, east_x + INDICATOR_SIZE - 1, edge_y + INDICATOR_SIZE - 1],
fill=edge_color,
outline='black'
)
# Draw diagonal cross if edge crosses between worlds
if edge.dest is not None and is_crossed_edge(edge, overworld_screens):
draw.line(
[east_x, edge_y, east_x + INDICATOR_SIZE - 1, edge_y + INDICATOR_SIZE - 1],
fill='black',
width=1
)
draw.line(
[east_x + INDICATOR_SIZE - 1, edge_y, east_x, edge_y + INDICATOR_SIZE - 1],
fill='black',
width=1
)
# Create output directory if it doesn't exist
os.makedirs(output_dir, exist_ok=True)
# Generate filename with timestamp
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"layout_{timestamp}.png"
filepath = os.path.join(output_dir, filename)
# Save the image
output_img.save(filepath, "PNG")
logging.getLogger('').info(f"Layout visualization saved to {filepath}")

View File

@@ -44,6 +44,11 @@ class DataTables:
self.ow_enemy_table = None
self.pot_secret_table = None
self.overworld_sprite_sheets = None
self.pointer_addresses = {
# table: [data_start, data_size, pointer_table, references]
'ow_sprites': [ 0x09CB41, None, (0x09C881, 0x09C901, 0x09CA21), None ],
'uw_sprites': [ 0x09D92E, None, 0x09D62E, 0x09C298 ],
}
# associated data
self.sprite_requirements = None
@@ -92,13 +97,6 @@ class DataTables:
# bank 0A uses 372A bytes
# bank 1F uses 77CE bytes: total is about a bank and a half
# probably should reuse bank 1F if writing all the rooms out
for sheet in self.sprite_sheets.values():
sheet.write_to_rom(rom, snes_to_pc(0x00DB97)) # bank 00, SheetsTable_AA3
if self.uw_enemy_table.size() > 0x2800:
raise Exception('Sprite table is too big for current area')
self.uw_enemy_table.write_sprite_data_to_rom(rom)
self.uw_enemy_table.check_special_bitmasks_size()
self.uw_enemy_table.write_special_bitmask_table(rom)
for area_id, sheet in self.overworld_sprite_sheets.items():
if area_id in [0x80, 0x81]:
offset = area_id - 0x80 # 02E575 for special areas?
@@ -110,7 +108,14 @@ class DataTables:
# _00FAC1 is LW post-aga
# _00FB01 is DW
# _00FA41 is rain state
for sheet in self.sprite_sheets.values():
sheet.write_to_rom(rom, snes_to_pc(0x00DB97)) # bank 00, SheetsTable_AA3
self.write_ow_sprite_data_to_rom(rom)
if self.uw_enemy_table.size() > 0x2800:
raise Exception('Sprite table is too big for current area')
self.uw_enemy_table.write_sprite_data_to_rom(rom, self.pointer_addresses)
self.uw_enemy_table.check_special_bitmasks_size()
self.uw_enemy_table.write_special_bitmask_table(rom)
for sprite, stats in self.enemy_stats.items():
# write health to rom
if stats.health is not None:
@@ -148,13 +153,14 @@ class DataTables:
def write_ow_sprite_data_to_rom(self, rom):
# calculate how big this table is going to be?
# bytes = sum(1+len(x)*3 for x in self.ow_enemy_table.values() if len(x) > 0)+1
bytes = sum(1+len(x)*3 for x in self.ow_enemy_table.values() if len(x) > 0)+1
self.pointer_addresses['ow_sprites'][1] = bytes
# ending_byte = 0x09CB3B + bytes
max_per_state = {0: 0x40, 1: 0x90, 2: 0x8B} # dropped max on state 2 to steal space for a couple extra sprites (Murahdahla, extra tutorial guard)
max_per_state = {0: 0x40, 1: 0x90, 2: 0x82} # dropped max on state 2 to steal space for extra sprites (Murahdahla, extra tutorial guard)
pointer_address = snes_to_pc(0x09C881)
# currently borrowed 10 bytes, used 9 (2xMurah + TutorialGuard)
data_pointer = snes_to_pc(0x09CB38) # was originally 0x09CB41 - stealing space for a couple extra sprites (Murahdahla, extra tutorial guard)
pointer_address = snes_to_pc(self.pointer_addresses['ow_sprites'][2][0])
self.pointer_addresses['ow_sprites'][0] = pointer_address + ((max_per_state[0] + max_per_state[1] + max_per_state[2]) * 2)
data_pointer = self.pointer_addresses['ow_sprites'][0]
empty_pointer = pc_to_snes(data_pointer) & 0xFFFF
rom.write_byte(data_pointer, 0xff)
cached_dark_world = {}
@@ -187,6 +193,10 @@ class DataTables:
data_pointer += len(data)
rom.write_byte(data_pointer, 0xff)
data_pointer += 1
# Check if OW sprite data has overwritten the UW sprite pointer table
max_allowed_address = snes_to_pc(0x09D62E)
if data_pointer > max_allowed_address:
raise Exception(f'OW sprite data will cause the UW sprite pointer table to overwrite the pots pointer table. Data end: {hex(pc_to_snes(data_pointer))}, Max allowed: $09D62E')
special_health_table = {

View File

@@ -121,8 +121,16 @@ def roll_settings(weights):
ret.accessibility = get_choice('accessibility')
ret.restrict_boss_items = get_choice('restrict_boss_items')
overworld_layout = get_choice('overworld_layout')
ret.ow_layout = overworld_layout if overworld_layout != 'none' else 'vanilla'
ret.ow_parallel = get_choice_bool('overworld_parallel')
overworld_shuffle = get_choice('overworld_shuffle')
ret.ow_shuffle = overworld_shuffle if overworld_shuffle != 'none' else 'vanilla'
if overworld_shuffle == 'parallel':
ret.ow_layout = 'wild'
ret.ow_parallel = True
elif overworld_shuffle == 'full':
ret.ow_layout = 'wild'
ret.ow_parallel = False
ret.ow_terrain = get_choice_bool('overworld_terrain')
valid_options = {'none': 'none', 'polar': 'polar', 'grouped': 'polar', 'chaos': 'unrestricted', 'unrestricted': 'unrestricted'}
ret.ow_crossed = get_choice('overworld_crossed')
@@ -132,6 +140,7 @@ def roll_settings(weights):
ret.ow_whirlpool = get_choice_bool('whirlpool_shuffle')
overworld_flute = get_choice('flute_shuffle')
ret.ow_fluteshuffle = overworld_flute if overworld_flute != 'none' else 'vanilla'
ret.ow_fog = get_choice_bool('overworld_fog')
ret.shuffle_followers = get_choice_bool('shuffle_followers')
ret.bonk_drops = get_choice_bool('bonk_drops')
entrance_shuffle = get_choice('entrance_shuffle')
@@ -148,11 +157,7 @@ def roll_settings(weights):
ret.door_self_loops = get_choice_bool('door_self_loops')
ret.experimental = get_choice_bool('experimental')
ret.collection_rate = get_choice_bool('collection_rate')
ret.dungeon_counters = get_choice_non_bool('dungeon_counters') if 'dungeon_counters' in weights else 'default'
if ret.dungeon_counters == 'default':
ret.dungeon_counters = 'pickup' if ret.door_shuffle != 'vanilla' or ret.compassshuffle != 'none' else 'off'
ret.pseudoboots = get_choice_bool('pseudoboots')
ret.mirrorscroll = get_choice_bool('mirrorscroll')
ret.shopsanity = get_choice_bool('shopsanity')