Added option to keep original palettes in crossed dungeon mode

If sanc if in a DW dungeon because of crossed+ ER, then you start in bunny form
Mirroring from sanc to the portal is now in logic
Another fix for animated tiles (fairy fountains)
GT Big Key stat changed on credits

Some standard logic fixes for lobbies (more outstanding)
This commit is contained in:
aerinon
2020-11-16 10:51:26 -07:00
parent 11154e1544
commit 4dda394a90
24 changed files with 186 additions and 37 deletions

View File

@@ -129,6 +129,7 @@ class World(object):
set_player_attr('keydropshuffle', False)
set_player_attr('mixed_travel', 'prevent')
set_player_attr('standardize_palettes', 'standardize')
set_player_attr('force_fix', {'gt': False, 'sw': False, 'pod': False, 'tr': False});
def get_name_string_for_object(self, obj):
@@ -148,6 +149,11 @@ class World(object):
for door in doors:
self._door_cache[(door.name, door.player)] = door
def remove_door(self, door, player):
if (door, player) in self._door_cache.keys():
del self._door_cache[(door, player)]
self.doors.remove(door)
def get_regions(self, player=None):
return self.regions if player is None else self._region_cache[player].values()
@@ -176,6 +182,10 @@ class World(object):
return exit
raise RuntimeError('No such entrance %s for player %d' % (entrance, player))
def remove_entrance(self, entrance, player):
if (entrance, player) in self._entrance_cache.keys():
del self._entrance_cache[(entrance, player)]
def get_location(self, location, player):
if isinstance(location, Location):
return location
@@ -862,6 +872,7 @@ class CollectionState(object):
@unique
class RegionType(Enum):
Menu = 0
LightWorld = 1
DarkWorld = 2
Cave = 3 # Also includes Houses

3
CLI.py
View File

@@ -95,7 +95,7 @@ def parse_cli(argv, no_defaults=False):
'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters',
'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots',
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep',
'remote_items', 'keydropshuffle', 'mixed_travel']:
'remote_items', 'keydropshuffle', 'mixed_travel', 'standardize_palettes']:
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
if player == 1:
setattr(ret, name, {1: value})
@@ -146,6 +146,7 @@ def parse_settings():
"experimental": False,
"dungeon_counters": "default",
"mixed_travel": "prevent",
"standardize_palettes": "standardize",
"multi": 1,
"names": "",

View File

@@ -41,6 +41,14 @@ def link_doors(world, player):
for entrance, ext in straight_staircases:
connect_two_way(world, entrance, ext, player)
if world.intensity[player] < 3 or world.doorShuffle == 'vanilla':
mirror_route = world.get_entrance('Sanctuary Mirror Route', player)
mr_door = mirror_route.door
sanctuary = mirror_route.parent_region
sanctuary.exits.remove(mirror_route)
world.remove_entrance(mirror_route, player)
world.remove_door(mr_door, player)
connect_custom(world, player)
find_inaccessible_regions(world, player)
@@ -637,6 +645,11 @@ def within_dungeon(world, player):
logging.getLogger('').info('%s: %s', world.fish.translate("cli", "cli", "keydoor.shuffle.time"), time.process_time()-start)
smooth_door_pairs(world, player)
if world.intensity[player] >= 3:
portal = world.get_portal('Sanctuary', player)
target = portal.door.entrance.parent_region
connect_simple_door(world, 'Sanctuary Mirror Route', target, player)
def handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map, builder_info):
dungeon_entrances, split_dungeon_entrances, world, player = builder_info
@@ -916,9 +929,17 @@ def cross_dungeon(world, player):
if state.visited_at_all(sanctuary):
reachable_portals.append(portal)
world.sanc_portal[player] = random.choice(reachable_portals)
if world.intensity[player] >= 3:
if player in world.sanc_portal:
portal = world.sanc_portal[player]
else:
portal = world.get_portal('Sanctuary', player)
target = portal.door.entrance.parent_region
connect_simple_door(world, 'Sanctuary Mirror Route', target, player)
check_entrance_fixes(world, player)
if world.standardize_palettes[player] == 'standardize':
palette_assignment(world, player)
refine_hints(dungeon_builders)
@@ -1093,6 +1114,10 @@ def palette_assignment(world, player):
queue.append((ext.connected_region, dist+1))
visited_regions.add(ext.connected_region)
sanc = world.get_region('Sanctuary', player)
if sanc.dungeon.name == 'Hyrule Castle':
room = world.get_room(0x12, player)
room.palette = 0x1d
for connection in ['Sanctuary S', 'Sanctuary N']:
adjacent = world.get_entrance(connection, player)
if adjacent.door.dest and adjacent.door.dest.entrance.parent_region.type == RegionType.Dungeon:
@@ -1143,7 +1168,7 @@ def convert_to_sectors(region_names, world, player):
if existing not in matching_sectors:
matching_sectors.append(existing)
else:
if door and not door.controller and not door.dest and not door.entranceFlag:
if door and not door.controller and not door.dest and not door.entranceFlag and door.type != DoorType.Logical:
outstanding_doors.append(door)
sector = Sector()
if not new_sector:
@@ -1418,7 +1443,8 @@ def find_key_door_candidates(region, checked, world, player):
valid = True
if valid and d not in candidates:
candidates.append(d)
if ext.connected_region.type != RegionType.Dungeon or ext.connected_region.dungeon == dungeon:
connected = ext.connected_region
if connected and (connected.type != RegionType.Dungeon or connected.dungeon == dungeon):
queue.append((ext.connected_region, d, current))
if d is not None:
checked_doors.append(d)
@@ -1815,6 +1841,8 @@ class DROptions(Flag):
Map_Info = 0x04
Debug = 0x08
Rails = 0x10 # If on, draws rails
OriginalPalettes = 0x20
Reserved = 0x40 # Reserved for PoD sliding wall?
Open_Desert_Wall = 0x80 # If on, pre opens the desert wall, no fire required

View File

@@ -120,6 +120,7 @@ def create_doors(world, player):
# logically one way the sanc, but should be linked - also toggle
create_door(player, 'Sanctuary N', Nrml).dir(No, 0x12, Mid, High).no_exit().toggler().pos(0),
create_door(player, 'Sanctuary S', Nrml).dir(So, 0x12, Mid, High).pos(2).portal(Z, 0x22, 1),
create_door(player, 'Sanctuary Mirror Route', Lgcl),
# Eastern Palace
create_door(player, 'Eastern Lobby S', Nrml).dir(So, 0xc9, Mid, High).pos(4).portal(Z, 0x20),

View File

@@ -266,7 +266,7 @@ def generate_itempool(world, player):
amt = world.pool_adjustment[player]
if amt < 0:
for _ in range(amt, 0):
pool.remove('Rupees (20)')
pool.remove(next(iter([x for x in pool if x in ['Rupees (20)', 'Rupees (5)', 'Rupee (1)']])))
elif amt > 0:
for _ in range(0, amt):
pool.append('Rupees (20)')

View File

@@ -978,11 +978,22 @@ def filter_big_chest(locations):
return [x for x in locations if '- Big Chest' not in x.name]
def count_locations_exclude_logic(locations, key_logic):
cnt = 0
for loc in locations:
if loc not in key_logic.bk_restricted and not loc.forced_item and not prize_or_event(loc):
cnt += 1
return cnt
def prize_or_event(loc):
return loc.name in dungeon_events or '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2']
def count_free_locations(state):
cnt = 0
for loc in state.found_locations:
if '- Prize' not in loc.name and loc.name not in dungeon_events and not loc.forced_item:
if loc.name not in ['Agahnim 1', 'Agahnim 2']:
if not prize_or_event(loc) and not loc.forced_item:
cnt += 1
return cnt
@@ -990,8 +1001,7 @@ def count_free_locations(state):
def count_locations_exclude_big_chest(state):
cnt = 0
for loc in state.found_locations:
if '- Big Chest' not in loc.name and '- Prize' not in loc.name and loc.name not in dungeon_events:
if not loc.forced_item and loc.name not in ['Agahnim 1', 'Agahnim 2']:
if '- Big Chest' not in loc.name and not loc.forced_item and not prize_or_event(loc):
cnt += 1
return cnt

View File

@@ -24,7 +24,7 @@ from Fill import distribute_items_cutoff, distribute_items_staleness, distribute
from ItemList import generate_itempool, difficulties, fill_prizes, fill_specific_items
from Utils import output_path, parse_player_names
__version__ = '0.2.0.10u'
__version__ = '0.2.0.11u'
class EnemizerError(RuntimeError):
pass
@@ -68,6 +68,7 @@ def main(args, seed=None, fish=None):
world.fish = fish
world.keydropshuffle = args.keydropshuffle.copy()
world.mixed_travel = args.mixed_travel.copy()
world.standardize_palettes = args.standardize_palettes.copy()
world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)}
@@ -126,6 +127,7 @@ def main(args, seed=None, fish=None):
else:
mark_dark_world_regions(world, player)
logger.info(world.fish.translate("cli","cli","generating.itempool"))
logger.info(world.fish.translate("cli","cli","generating.itempool"))
for player in range(1, world.players + 1):
generate_itempool(world, player)
@@ -379,6 +381,7 @@ def copy_world(world):
ret.experimental = world.experimental.copy()
ret.keydropshuffle = world.keydropshuffle.copy()
ret.mixed_travel = world.mixed_travel.copy()
ret.standardize_palettes = world.standardize_palettes.copy()
for player in range(1, world.players + 1):
if world.mode[player] != 'inverted':

View File

@@ -71,6 +71,16 @@ The rooms are left alone and it is up to the discretion of the player whether to
The two disjointed sections are forced to be in the same dungeon but never logically required to complete that game.
### Standardize Palettes (--standardize_palettes)
No effect if door shuffle is not on crossed
#### Standardize
Rooms in the same dungeon have their palettes changed to match. Hyrule Castle is split between Sewer and HC palette.
Rooms adjacent to sanctuary get their coloring to match sanc.
#### Original
Room keep their original palettes.
## Map/Compass/Small Key/Big Key shuffle (aka Keysanity)

View File

@@ -36,6 +36,13 @@ otherwise unconnected logically can be reach using these glitches. To prevent th
# Bug Fixes
* 2.0.11u
* Option to keep original palettes in crossed dungeon mode
* If sanc if in a DW dungeon because of crossed+ ER, then you start in bunny form
* Mirroring from sanc to the portal is now in logic
* Another fix for animated tiles (fairy fountains)
* GT Big Key stat fixed on credits
* Todo: Standard logic fixes for lobbies
* 2.0.10u
* Fix POD, TR, GT and SKULL 3 entrance if sanc ends up in that dungeon in crossed ER+
* TR Lobbies that need a bomb and can be entered before bombing should be pre-opened

View File

@@ -4,7 +4,7 @@ from BaseClasses import Region, Location, Entrance, RegionType, Shop, ShopType
def create_regions(world, player):
world.regions += [
create_lw_region(player, 'Menu', None, ['Links House S&Q', 'Sanctuary S&Q', 'Old Man S&Q']),
create_menu_region(player, 'Menu', None, ['Links House S&Q', 'Sanctuary S&Q', 'Old Man S&Q']),
create_lw_region(player, 'Light World', ['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure', 'Purple Chest'],
["Blinds Hideout", "Hyrule Castle Secret Entrance Drop", 'Zoras River', 'Kings Grave Outer Rocks', 'Dam',
'Links House', 'Tavern North', 'Chicken House', 'Aginahs Cave', 'Sahasrahlas Hut', 'Kakariko Well Drop', 'Kakariko Well Cave',
@@ -273,7 +273,7 @@ def create_dungeon_regions(world, player):
create_dungeon_region(player, 'Sewers Pull Switch', 'Hyrule Castle', None, ['Sewers Pull Switch N', 'Sewers Pull Switch S']),
create_dungeon_region(player, 'Sanctuary', 'Hyrule Castle',
['Sanctuary'] if not std_flag else ['Sanctuary', 'Zelda Drop Off'],
['Sanctuary S', 'Sanctuary N']),
['Sanctuary S', 'Sanctuary N', 'Sanctuary Mirror Route']),
# Eastern Palace
create_dungeon_region(player, 'Eastern Lobby', 'Eastern Palace', None, ['Eastern Lobby N', 'Eastern Lobby S', 'Eastern Lobby NW', 'Eastern Lobby NE']),
@@ -788,15 +788,22 @@ def create_dungeon_regions(world, player):
world.get_region('GT Crystal Circles', player).crystal_switch = True
def create_menu_region(player, name, locations=None, exits=None):
return _create_region(player, name, RegionType.Menu, 'Menu', locations, exits)
def create_lw_region(player, name, locations=None, exits=None):
return _create_region(player, name, RegionType.LightWorld, 'Light World', locations, exits)
def create_dw_region(player, name, locations=None, exits=None):
return _create_region(player, name, RegionType.DarkWorld, 'Dark World', locations, exits)
def create_cave_region(player, name, hint='Hyrule', locations=None, exits=None):
return _create_region(player, name, RegionType.Cave, hint, locations, exits)
def create_dungeon_region(player, name, hint='Hyrule', locations=None, exits=None):
return _create_region(player, name, RegionType.Dungeon, hint, locations, exits)

31
Rom.py
View File

@@ -14,6 +14,7 @@ import bps.io
from BaseClasses import CollectionState, ShopType, Region, Location, DoorType, RegionType
from DoorShuffle import compass_data, DROptions, boss_indicator
from Dungeons import dungeon_music_addresses
from KeyDoorShuffle import count_locations_exclude_logic
from Regions import location_table
from Text import MultiByteTextMapper, CompressedTextMapper, text_addresses, Credits, TextTable
from Text import Uncle_texts, Ganon1_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, Blind_texts, BombShop2_texts, junk_texts
@@ -24,7 +25,7 @@ from EntranceShuffle import door_addresses, exit_ids
JAP10HASH = '03a63945398191337e896e5771f77173'
RANDOMIZERBASEHASH = 'e16cc8659527baecc02e3b49b83fa49b'
RANDOMIZERBASEHASH = 'f6be3fdaac906a2217e7ee328e27b95b'
class JsonRom(object):
@@ -623,11 +624,13 @@ def patch_rom(world, rom, player, team, enemized):
dr_flags = DROptions.Eternal_Mini_Bosses if world.doorShuffle[player] == 'vanilla' else DROptions.Town_Portal
if world.doorShuffle[player] == 'crossed':
dr_flags |= DROptions.Map_Info
if world.experimental[player]:
if world.experimental[player] and world.goal[player] != 'triforcehunt':
dr_flags |= DROptions.Debug
if world.doorShuffle[player] == 'crossed' and world.logic[player] != 'nologic'\
and world.mixed_travel[player] == 'prevent':
dr_flags |= DROptions.Rails
if world.standardize_palettes[player] == 'original':
dr_flags |= DROptions.OriginalPalettes
# fix hc big key problems (map and compass too)
@@ -656,6 +659,9 @@ def patch_rom(world, rom, player, team, enemized):
rom.write_byte(0x13f038+offset*2, bk_status)
if player in world.sanc_portal.keys():
rom.write_byte(0x159a6, world.sanc_portal[player].ent_offset)
sanc_region = world.sanc_portal[player].door.entrance.parent_region
if sanc_region.is_dark_world and not sanc_region.is_light_world:
rom.write_byte(0x13ff00, 1)
for room in world.rooms:
if room.player == player and room.palette is not None:
rom.write_byte(0x13f200+room.index, room.palette)
@@ -723,10 +729,10 @@ def patch_rom(world, rom, player, team, enemized):
# bot: $7A is 1, 7B is 2, etc so 7D=4, 82=9 (zero unknown...)
return 0x53+num, 0x79+num
# collection rate address: 238C37
if world.keydropshuffle[player]:
rom.write_byte(0x140000, 1)
rom.write_byte(0x187010, 249) # dynamic credits
# collection rate address: 238C37
mid_top, mid_bot = credits_digit(4)
last_top, last_bot = credits_digit(9)
# top half
@@ -736,6 +742,23 @@ def patch_rom(world, rom, player, team, enemized):
rom.write_byte(0x118C71, mid_bot)
rom.write_byte(0x118C72, last_bot)
if world.keydropshuffle[player] or world.doorShuffle[player] != 'vanilla':
gt = world.dungeon_layouts[player]['Ganons Tower']
gt_logic = world.key_logic[player]['Ganons Tower']
total = 0
for region in gt.master_sector.regions:
total += count_locations_exclude_logic(region.locations, gt_logic)
rom.write_byte(0x187012, total) # dynamic credits
# gt big key address: 238B59
mid_top, mid_bot = credits_digit(total // 10)
last_top, last_bot = credits_digit(total % 10)
# top half
rom.write_byte(0x118B75, mid_top)
rom.write_byte(0x118B76, last_top)
# bottom half
rom.write_byte(0x118B93, mid_bot)
rom.write_byte(0x118B94, last_bot)
# patch medallion requirements
if world.required_medallions[player][0] == 'Bombos':
rom.write_byte(0x180022, 0x00) # requirement

View File

@@ -832,8 +832,10 @@ def standard_rules(world, player):
set_rule(world.get_entrance('Sanctuary S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player))
# these are because of rails
if world.shuffle[player] != 'vanilla':
# todo:
set_rule(world.get_entrance('Hyrule Castle Exit (East)', player), lambda state: state.has('Zelda Delivered', player))
set_rule(world.get_entrance('Hyrule Castle Exit (West)', player), lambda state: state.has('Zelda Delivered', player))
set_rule(world.get_entrance('Sanctuary Exit', player), lambda state: state.has('Zelda Delivered', player))
# too restrictive for crossed?
def uncle_item_rule(item):
@@ -874,7 +876,7 @@ def standard_rules(world, player):
rule_list, debug_path = find_rules_for_zelda_delivery(world, player)
set_rule(world.get_location('Zelda Drop Off', player), lambda state: state.has('Zelda Herself', player) and check_rule_list(state, rule_list))
for location in ['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure', 'Purple Chest']:
for location in ['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure', 'Purple Chest', 'Maze Race']:
add_rule(world.get_location(location, player), lambda state: state.has('Zelda Delivered', player))
# Bonk Fairy (Light) is a notable omission in ER shuffles/Retro

View File

@@ -34,6 +34,7 @@ incsrc overrides.asm
incsrc edges.asm
incsrc math.asm
incsrc hudadditions.asm
incsrc dr_lobby.asm
warnpc $279700
incsrc doortables.asm

View File

@@ -641,3 +641,8 @@ db $00,$07,$20,$20,$07,$07,$07,$07,$07,$20,$20,$07,$20,$20,$20,$20
db $07,$07,$02,$02,$02,$02,$07,$07,$07,$20,$20,$07,$20,$20,$20,$07
;27f300
;
org $27ff00
SancDarkWorldFlag:
db 0

9
asm/dr_lobby.asm Normal file
View File

@@ -0,0 +1,9 @@
CheckDarkWorldSanc:
STA $A0 : STA $048E ; what we wrote over
LDA.l SancDarkWorldFlag : BEQ +
SEP #$30
LDA $A0 : CMP #$12 : BNE ++
LDA.l $7EF357 : BNE ++ ; moon pearl?
LDA #$17 : STA $5D : INC $02E0 : LDA.b #$40 : STA !DARK_WORLD
++ REP #$30
+ RTL

View File

@@ -153,6 +153,9 @@ JSL StoreTempBunnyState
org $08c450 ; <- ancilla_receive_item.asm : 146-148 (STY $5D : STZ $02D8)
JSL RetrieveBunnyState : NOP
org $02d9ce ; <- Bank02.asm : Dungeon_LoadEntrance 10829 (STA $A0 : STA $048E)
JSL CheckDarkWorldSanc : NOP
; These two, if enabled together, have implications for vanilla BK doors in IP/Hera/Mire
; IPBJ is common enough to consider not doing this. Mire is not a concern for vanilla - maybe glitched modes
; Hera BK door back can be seen with Pot clipping - likely useful for no logic seeds

View File

@@ -34,7 +34,8 @@ GfxFixer:
}
FixAnimatedTiles:
LDA.L DRMode : cmp #$02 : bne +
LDA.L DRMode : CMP #$02 : BNE +
LDA $040C : CMP.b #$FF : BEQ +
PHX
LDX $A0 : LDA.l TilesetTable, x
CMP $0AA1 : beq ++
@@ -74,6 +75,7 @@ CgramAuxToMain: ; ripped this from bank02 because it ended with rts
OverridePaletteHeader:
lda.l DRMode : cmp #$02 : bne +
lda.l DRFlags : and #$20 : bne +
cpx #$01c2 : !bge +
rep #$20
txa : lsr : tax

View File

@@ -121,12 +121,9 @@ KeyGet:
lda $a0 : cmp #$87 : bne +
jsr ShouldKeyBeCountedForDungeon : bcc -
jsl CountChestKeyLong : bra -
+ phy
+ sty $00
jsr KeyGetPlayer : sta !MULTIWORLD_ITEM_PLAYER_ID
jsl.l $0791b3 ; Player_HaltDashAttackLong
jsl.l Link_ReceiveItem
pla : sta $00
lda !MULTIWORLD_ITEM_PLAYER_ID : bne .end
lda !MULTIWORLD_ITEM_PLAYER_ID : bne .receive
phx
lda $040c : lsr : tax
lda $00 : cmp KeyTable, x : bne +
@@ -134,7 +131,9 @@ KeyGet:
+ cmp #$af : beq - ; universal key
cmp #$24 : beq - ; small key for this dungeon
plx
.end
.receive
jsl.l $0791b3 ; Player_HaltDashAttackLong
jsl.l Link_ReceiveItem
pla : dec : rtl
}

Binary file not shown.

View File

@@ -70,6 +70,12 @@
"force"
]
},
"standardize_palettes" : {
"choices": [
"standardize",
"original"
]
},
"timer": {
"choices": [
"none",

View File

@@ -251,6 +251,11 @@
"Allow: Take the rails off, \"I know what I'm doing\"",
"Force: Force these troublesome connections to be in the same dungeon (but not in logic). No rails will appear"
],
"standardize_palettes": [
"In cross dungeon shuffle, we can keep the rooms original palette or attempt to standardize them",
"Standardize: Attempts to make the palette the same between dungeons",
"Original: Dungeons rooms retain original palettes"
],
"retro": [
"Keys are universal, shooting arrows costs rupees,",
"and a few other little things make this more like Zelda-1. (default: %(default)s)"

View File

@@ -77,6 +77,10 @@
"randomizer.dungeon.mixed_travel.allow": "Allow Mixed Dungeon Travel",
"randomizer.dungeon.mixed_travel.force": "Force Reachable Areas to Same Dungeon",
"randomizer.dungeon.standardize_palettes": "Crossed Dungeon Palette ",
"randomizer.dungeon.standardize_palettes.standardize": "Standardize Palettes",
"randomizer.dungeon.standardize_palettes.original": "Keep Original Palettes",
"randomizer.enemizer.potshuffle": "Pot Shuffle",
"randomizer.enemizer.enemyshuffle": "Enemy Shuffle",

View File

@@ -35,7 +35,7 @@
},
"mixed_travel": {
"type" : "selectbox",
"default": "auto",
"default": "prevent",
"options": [
"prevent",
"allow",
@@ -44,6 +44,17 @@
"config": {
"width": 35
}
},
"standardize_palettes" : {
"type": "selectbox",
"default": "standardize",
"options": [
"standardize",
"original"
],
"config": {
"width": 35
}
}
}
}

View File

@@ -91,7 +91,8 @@ SETTINGSTOPROCESS = {
"dungeonintensity": "intensity",
"experimental": "experimental",
"dungeon_counters": "dungeon_counters",
"mixed_travel": "mixed_travel"
"mixed_travel": "mixed_travel",
"standardize_palettes": "standardize_palettes",
},
"gameoptions": {
"hints": "hints",