Merged in DR v0.4.0.8

This commit is contained in:
codemann8
2021-07-07 19:45:14 -05:00
18 changed files with 273 additions and 66 deletions

View File

@@ -139,6 +139,7 @@ class World(object):
set_player_attr('treasure_hunt_total', 0) set_player_attr('treasure_hunt_total', 0)
set_player_attr('potshuffle', False) set_player_attr('potshuffle', False)
set_player_attr('pot_contents', None) set_player_attr('pot_contents', None)
set_player_attr('fakeboots', False)
set_player_attr('shopsanity', False) set_player_attr('shopsanity', False)
set_player_attr('keydropshuffle', False) set_player_attr('keydropshuffle', False)
@@ -2430,7 +2431,7 @@ access_mode = {"items": 0, "locations": 1, "none": 2}
# byte 6: BSMC BBEE (big, small, maps, compass, bosses, enemies) # byte 6: BSMC BBEE (big, small, maps, compass, bosses, enemies)
boss_mode = {"none": 0, "simple": 1, "full": 2, "random": 3, "chaos": 3} boss_mode = {"none": 0, "simple": 1, "full": 2, "random": 3, "chaos": 3}
enemy_mode = {"none": 0, "shuffled": 1, "random": 2, "chaos": 2} enemy_mode = {"none": 0, "shuffled": 1, "random": 2, "chaos": 2, "legacy": 3}
# byte 7: HHHD DP?? (enemy_health, enemy_dmg, potshuffle, ?) # byte 7: HHHD DP?? (enemy_health, enemy_dmg, potshuffle, ?)
e_health = {"default": 0, "easy": 1, "normal": 2, "hard": 3, "expert": 4} e_health = {"default": 0, "easy": 1, "normal": 2, "hard": 3, "expert": 4}

3
CLI.py
View File

@@ -98,7 +98,7 @@ def parse_cli(argv, no_defaults=False):
'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'openpyramid', 'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'openpyramid',
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory', 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max', 'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max',
'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'fakeboots',
'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters', 'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters',
'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots', 'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots',
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep', 'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep',
@@ -149,6 +149,7 @@ def parse_settings():
"ow_fluteshuffle": "vanilla", "ow_fluteshuffle": "vanilla",
"shuffle": "vanilla", "shuffle": "vanilla",
"shufflelinks": False, "shufflelinks": False,
"fakeboots": False,
"shufflepots": False, "shufflepots": False,
"shuffleenemies": "none", "shuffleenemies": "none",

View File

@@ -1482,14 +1482,12 @@ def link_entrances(world, player):
blacksmith_hut = blacksmith_doors.pop() blacksmith_hut = blacksmith_doors.pop()
connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player)
doors.remove(blacksmith_hut) doors.remove(blacksmith_hut)
exit_pool.remove(blacksmith_hut)
# place dam and pyramid fairy, have limited options # place dam and pyramid fairy, have limited options
random.shuffle(bomb_shop_doors) random.shuffle(bomb_shop_doors)
bomb_shop = bomb_shop_doors.pop() bomb_shop = bomb_shop_doors.pop()
connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) connect_entrance(world, bomb_shop, 'Big Bomb Shop', player)
doors.remove(bomb_shop) doors.remove(bomb_shop)
exit_pool.remove(bomb_shop)
# handle remaining caves # handle remaining caves
for cave in caves: for cave in caves:

22
Fill.py
View File

@@ -3,7 +3,7 @@ import logging
from BaseClasses import CollectionState from BaseClasses import CollectionState
from Items import ItemFactory from Items import ItemFactory
from Regions import shop_to_location_table from Regions import shop_to_location_table, retro_shops
class FillError(RuntimeError): class FillError(RuntimeError):
@@ -496,6 +496,10 @@ def balance_multiworld_progression(world):
new_location = replacement_locations.pop() new_location = replacement_locations.pop()
new_location.item, old_location.item = old_location.item, new_location.item new_location.item, old_location.item = old_location.item, new_location.item
if world.shopsanity[new_location.player]:
check_shop_swap(new_location)
if world.shopsanity[old_location.player]:
check_shop_swap(old_location)
new_location.event, old_location.event = True, False new_location.event, old_location.event = True, False
state.collect(new_location.item, True, new_location) state.collect(new_location.item, True, new_location)
replaced_items = True replaced_items = True
@@ -516,6 +520,18 @@ def balance_multiworld_progression(world):
raise RuntimeError('Not all required items reachable. Something went terribly wrong here.') raise RuntimeError('Not all required items reachable. Something went terribly wrong here.')
def check_shop_swap(l):
if l.parent_region.name in shop_to_location_table:
if l.name in shop_to_location_table[l.parent_region.name]:
idx = shop_to_location_table[l.parent_region.name].index(l.name)
inv_slot = l.parent_region.shop.inventory[idx]
inv_slot['item'] = l.item.name
elif l.parent_region in retro_shops:
idx = retro_shops[l.parent_region.name].index(l.name)
inv_slot = l.parent_region.shop.inventory[idx]
inv_slot['item'] = l.item.name
def balance_money_progression(world): def balance_money_progression(world):
logger = logging.getLogger('') logger = logging.getLogger('')
state = CollectionState(world) state = CollectionState(world)
@@ -557,7 +573,6 @@ def balance_money_progression(world):
for player in range(1, world.players+1): for player in range(1, world.players+1):
logger.debug(f'Money balance for P{player}: Needed: {total_price[player]} Available: {available_money[player]}') logger.debug(f'Money balance for P{player}: Needed: {total_price[player]} Available: {available_money[player]}')
def get_sphere_locations(sphere_state, locations): def get_sphere_locations(sphere_state, locations):
sphere_state.sweep_for_events(key_only=True, locations=locations) sphere_state.sweep_for_events(key_only=True, locations=locations)
return [loc for loc in locations if sphere_state.can_reach(loc) and sphere_state.not_flooding_a_key(sphere_state.world, loc)] return [loc for loc in locations if sphere_state.can_reach(loc) and sphere_state.not_flooding_a_key(sphere_state.world, loc)]
@@ -672,6 +687,7 @@ def balance_money_progression(world):
logger.debug(f'Upgrading {best_target.item.name} @ {best_target.name} for 300 Rupees') logger.debug(f'Upgrading {best_target.item.name} @ {best_target.name} for 300 Rupees')
best_target.item = ItemFactory('Rupees (300)', best_target.item.player) best_target.item = ItemFactory('Rupees (300)', best_target.item.player)
best_target.item.location = best_target best_target.item.location = best_target
check_shop_swap(best_target.item.location)
else: else:
old_item = best_target.item old_item = best_target.item
logger.debug(f'Swapping {best_target.item.name} @ {best_target.name} for {best_swap.item.name} @ {best_swap.name}') logger.debug(f'Swapping {best_target.item.name} @ {best_target.name} for {best_swap.item.name} @ {best_swap.name}')
@@ -679,6 +695,8 @@ def balance_money_progression(world):
best_target.item.location = best_target best_target.item.location = best_target
best_swap.item = old_item best_swap.item = old_item
best_swap.item.location = best_swap best_swap.item.location = best_swap
check_shop_swap(best_target.item.location)
check_shop_swap(best_swap.item.location)
increase = best_value - old_value increase = best_value - old_value
difference -= increase difference -= increase
wallet[target_player] += increase wallet[target_player] += increase

View File

@@ -28,7 +28,7 @@ from Fill import sell_potions, sell_keys, balance_multiworld_progression, balanc
from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops
from Utils import output_path, parse_player_names from Utils import output_path, parse_player_names
__version__ = '0.4.0.6-u' __version__ = '0.4.0.8-u'
class EnemizerError(RuntimeError): class EnemizerError(RuntimeError):
@@ -93,6 +93,7 @@ def main(args, seed=None, fish=None):
world.treasure_hunt_count = args.triforce_goal.copy() world.treasure_hunt_count = args.triforce_goal.copy()
world.treasure_hunt_total = args.triforce_pool.copy() world.treasure_hunt_total = args.triforce_pool.copy()
world.shufflelinks = args.shufflelinks.copy() world.shufflelinks = args.shufflelinks.copy()
world.fakeboots = args.fakeboots.copy()
world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)} world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)}
@@ -263,12 +264,10 @@ def main(args, seed=None, fish=None):
or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default' or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default'
or sprite_random_on_hit) or sprite_random_on_hit)
if use_enemizer:
base_patch = LocalRom(args.rom) # update base2current.json
rom = JsonRom() if args.jsonout or use_enemizer else LocalRom(args.rom) rom = JsonRom() if args.jsonout or use_enemizer else LocalRom(args.rom)
if use_enemizer and (args.enemizercli or not args.jsonout): if use_enemizer and (args.enemizercli or not args.jsonout):
base_patch = LocalRom(args.rom) # update base2current.json (side effect)
if args.rom and not(os.path.isfile(args.rom)): if args.rom and not(os.path.isfile(args.rom)):
raise RuntimeError("Could not find valid base rom for enemizing at expected path %s." % args.rom) raise RuntimeError("Could not find valid base rom for enemizing at expected path %s." % args.rom)
if os.path.exists(args.enemizercli): if os.path.exists(args.enemizercli):

View File

@@ -150,6 +150,7 @@ def roll_settings(weights):
ret.dungeon_counters = 'pickup' if ret.door_shuffle != 'vanilla' or ret.compassshuffle == 'on' else 'off' ret.dungeon_counters = 'pickup' if ret.door_shuffle != 'vanilla' or ret.compassshuffle == 'on' else 'off'
ret.shufflelinks = get_choice('shufflelinks') == 'on' ret.shufflelinks = get_choice('shufflelinks') == 'on'
ret.fakeboots = get_choice('fakeboots') == 'on'
ret.shopsanity = get_choice('shopsanity') == 'on' ret.shopsanity = get_choice('shopsanity') == 'on'
ret.keydropshuffle = get_choice('keydropshuffle') == 'on' ret.keydropshuffle = get_choice('keydropshuffle') == 'on'
ret.mixed_travel = get_choice('mixed_travel') if 'mixed_travel' in weights else 'prevent' ret.mixed_travel = get_choice('mixed_travel') if 'mixed_travel' in weights else 'prevent'
@@ -202,15 +203,17 @@ def roll_settings(weights):
boss_choice = old_style_bosses[boss_choice] boss_choice = old_style_bosses[boss_choice]
ret.shufflebosses = boss_choice ret.shufflebosses = boss_choice
ret.shuffleenemies = {'none': 'none', enemy_choice = get_choice('enemy_shuffle')
'shuffled': 'shuffled', if enemy_choice == 'chaos':
'random': 'chaos' enemy_choice = 'random'
}[get_choice('enemy_shuffle')] ret.shuffleenemies = enemy_choice
ret.enemy_damage = {'default': 'default', old_style_damage = {'none': 'default',
'shuffled': 'shuffled', 'chaos': 'random'}
'random': 'chaos' damage_choice = get_choice('enemy_damage')
}[get_choice('enemy_damage')] if damage_choice in old_style_damage:
damage_choice = old_style_damage[damage_choice]
ret.enemy_damage = damage_choice
ret.enemy_health = get_choice('enemy_health') ret.enemy_health = get_choice('enemy_health')

View File

@@ -14,6 +14,35 @@ Thanks to qadan, cheuer, & compiling
# Bug Fixes and Notes. # Bug Fixes and Notes.
* 0.4.0.8
* Ganon jokes added for when silvers aren't available
* Some text updated (Blind jokes, uncle text)
* Fixed some enemizer Mystery settings
* Added a setting that's random enemy shuffle without Unkillable Thieves possible
* Fixed shop spoiler when money balancing/multiworld balancing
* Fixed a problem with insanity
* Fixed an issue with the credit stats specific to DR (e.g. collection rate total)
* More helpful error message when bps is missing?
* Minor generation issues involving enemizer and the link sprite
* Baserom updates (from Bonta, kan, qwertymodo, ardnaxelark)
* Boss icon on dungeon map (if you have a compass)
* Progressive bow sprite replacement
* Quickswap - consecutive special swaps
* Bonk Counter
* One mind
* MSU fix
* Chest turn tracking (not yet in credits)
* Damaged and magic stats in credits (gt bk removed)
* Fix for infinite bombs
* Fake boots option
* Always allowed medallions for swordless (no option yet)
* 0.4.0.7
* Reduce flashing option added
* Sprite author credit added
* Ranged Crystal switch rules tweaked
* Baserom update: includes Credits Speedup, reduced flashing option, msu resume (but turned off by default)
* Create link sprite's zspr from local ROM and no longer attempts to download it from website
* Some minor bug fixes
* 0.4.0.6 * 0.4.0.6
* Hints now default to off * Hints now default to off
* The maiden gives you a hint to the attic if you bring her to the unlit boss room * The maiden gives you a hint to the attic if you bring her to the unlit boss room

120
Rom.py
View File

@@ -9,17 +9,20 @@ import random
import struct import struct
import sys import sys
import subprocess import subprocess
try:
import bps.apply import bps.apply
import bps.io import bps.io
except ImportError:
raise Exception('Could not load BPS module')
from BaseClasses import CollectionState, ShopType, Region, Location, OWEdge, Door, DoorType, RegionType, PotItem from BaseClasses import CollectionState, ShopType, Region, Location, OWEdge, Door, DoorType, RegionType, PotItem
from DoorShuffle import compass_data, DROptions, boss_indicator from DoorShuffle import compass_data, DROptions, boss_indicator
from Dungeons import dungeon_music_addresses from Dungeons import dungeon_music_addresses
from KeyDoorShuffle import count_locations_exclude_logic
from Regions import location_table, shop_to_location_table, retro_shops from Regions import location_table, shop_to_location_table, retro_shops
from RoomData import DoorKind from RoomData import DoorKind
from Text import MultiByteTextMapper, CompressedTextMapper, text_addresses, Credits, TextTable 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 from Text import Uncle_texts, Ganon1_texts, Ganon_Phase_3_No_Silvers_texts, TavernMan_texts, Sahasrahla2_texts
from Text import Triforce_texts, Blind_texts, BombShop2_texts, junk_texts
from Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, DeathMountain_texts, LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names from Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, DeathMountain_texts, LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names
from Utils import output_path, local_path, int16_as_bytes, int32_as_bytes, snes_to_pc from Utils import output_path, local_path, int16_as_bytes, int32_as_bytes, snes_to_pc
from Items import ItemFactory from Items import ItemFactory
@@ -28,7 +31,7 @@ from OverworldShuffle import default_flute_connections, flute_data
JAP10HASH = '03a63945398191337e896e5771f77173' JAP10HASH = '03a63945398191337e896e5771f77173'
RANDOMIZERBASEHASH = '5d2041f4387123c2de98dd41e6e5c4c6' RANDOMIZERBASEHASH = 'f0a6138148c13414ff4dc89dc0101de6'
class JsonRom(object): class JsonRom(object):
@@ -201,7 +204,7 @@ def patch_enemizer(world, player, rom, baserom_path, enemizercli, random_sprite_
options = { options = {
'RandomizeEnemies': world.enemy_shuffle[player] != 'none', 'RandomizeEnemies': world.enemy_shuffle[player] != 'none',
'RandomizeEnemiesType': 3, 'RandomizeEnemiesType': 3,
'RandomizeBushEnemyChance': world.enemy_shuffle[player] == 'random', 'RandomizeBushEnemyChance': world.enemy_shuffle[player] in ['random', 'legacy'],
'RandomizeEnemyHealthRange': world.enemy_health[player] != 'default', 'RandomizeEnemyHealthRange': world.enemy_health[player] != 'default',
'RandomizeEnemyHealthType': {'default': 0, 'easy': 0, 'normal': 1, 'hard': 2, 'expert': 3}[world.enemy_health[player]], 'RandomizeEnemyHealthType': {'default': 0, 'easy': 0, 'normal': 1, 'hard': 2, 'expert': 3}[world.enemy_health[player]],
'OHKO': False, 'OHKO': False,
@@ -247,9 +250,9 @@ def patch_enemizer(world, player, rom, baserom_path, enemizercli, random_sprite_
'SwordGraphics': "sword_gfx/normal.gfx", 'SwordGraphics': "sword_gfx/normal.gfx",
'BeeMizer': False, 'BeeMizer': False,
'BeesLevel': 0, 'BeesLevel': 0,
'RandomizeTileTrapPattern': world.enemy_shuffle[player] == 'random', 'RandomizeTileTrapPattern': world.enemy_shuffle[player] in ['random', 'legacy'],
'RandomizeTileTrapFloorTile': False, 'RandomizeTileTrapFloorTile': False,
'AllowKillableThief': bool(random.randint(0, 1)) if world.enemy_shuffle[player] == 'random' else world.enemy_shuffle[player] != 'none', 'AllowKillableThief': bool(random.randint(0, 1)) if world.enemy_shuffle[player] == 'legacy' else world.enemy_shuffle[player] != 'none',
'RandomizeSpriteOnHit': random_sprite_on_hit, 'RandomizeSpriteOnHit': random_sprite_on_hit,
'DebugMode': False, 'DebugMode': False,
'DebugForceEnemy': False, 'DebugForceEnemy': False,
@@ -891,7 +894,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
write_int16(rom, 0x187010, credits_total) # dynamic credits write_int16(rom, 0x187010, credits_total) # dynamic credits
if credits_total != 216: if credits_total != 216:
# collection rate address: # collection rate address:
cr_address = 0x2391CC cr_address = 0x2391C4
cr_pc = cr_address - 0x120000 # convert to pc cr_pc = cr_address - 0x120000 # convert to pc
mid_top, mid_bot = credits_digit((credits_total // 10) % 10) mid_top, mid_bot = credits_digit((credits_total // 10) % 10)
last_top, last_bot = credits_digit(credits_total % 10) last_top, last_bot = credits_digit(credits_total % 10)
@@ -902,25 +905,6 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
rom.write_byte(cr_pc+0x3a, mid_bot) rom.write_byte(cr_pc+0x3a, mid_bot)
rom.write_byte(cr_pc+0x3b, last_bot) rom.write_byte(cr_pc+0x3b, 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:
gtbk_address = 0x2390EE
gtbk_pc = gtbk_address - 0x120000 # convert to pc
mid_top, mid_bot = credits_digit(total // 10)
last_top, last_bot = credits_digit(total % 10)
# top half
rom.write_byte(gtbk_pc+0x1c, mid_top)
rom.write_byte(gtbk_pc+0x1d, last_top)
# bottom half
rom.write_byte(gtbk_pc+0x3a, mid_bot)
rom.write_byte(gtbk_pc+0x3b, last_bot)
# patch medallion requirements # patch medallion requirements
if world.required_medallions[player][0] == 'Bombos': if world.required_medallions[player][0] == 'Bombos':
rom.write_byte(0x180022, 0x00) # requirement rom.write_byte(0x180022, 0x00) # requirement
@@ -1264,6 +1248,9 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
rom.write_byte(0x18017E, 0x01) # Fairy fountains only trade in bottles rom.write_byte(0x18017E, 0x01) # Fairy fountains only trade in bottles
# Starting equipment # Starting equipment
if world.fakeboots[player]:
rom.write_byte(0x18008E, 0x01)
equip = [0] * (0x340 + 0x4F) equip = [0] * (0x340 + 0x4F)
equip[0x36C] = 0x18 equip[0x36C] = 0x18
equip[0x36D] = 0x18 equip[0x36D] = 0x18
@@ -1724,7 +1711,8 @@ def hud_format_text(text):
def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, sprite, def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, sprite,
ow_palettes, uw_palettes, reduce_flashing): ow_palettes, uw_palettes, reduce_flashing):
if not os.path.exists("data/sprites/official/001.link.1.zspr"):
if not os.path.exists("data/sprites/official/001.link.1.zspr") and rom.orig_buffer:
dump_zspr(rom.orig_buffer[0x80000:0x87000], rom.orig_buffer[0xdd308:0xdd380], dump_zspr(rom.orig_buffer[0x80000:0x87000], rom.orig_buffer[0xdd308:0xdd380],
rom.orig_buffer[0xdedf5:0xdedf9], "data/sprites/official/001.link.1.zspr", "Nintendo", "Link") rom.orig_buffer[0xdedf5:0xdedf9], "data/sprites/official/001.link.1.zspr", "Nintendo", "Link")
@@ -1812,7 +1800,6 @@ def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, spr
if reduce_flashing: if reduce_flashing:
rom.write_byte(0x18017f, 1) rom.write_byte(0x18017f, 1)
default_ow_palettes(rom) default_ow_palettes(rom)
if ow_palettes == 'random': if ow_palettes == 'random':
randomize_ow_palettes(rom) randomize_ow_palettes(rom)
@@ -1883,6 +1870,61 @@ def dump_zspr(basesprite, basepalette, baseglove, outfilename, author_name, spri
with open('%s' % outfilename, "wb") as zspr_file: with open('%s' % outfilename, "wb") as zspr_file:
zspr_file.write(write_buffer) zspr_file.write(write_buffer)
# .zspr file dumping logic copied with permission from SpriteSomething:
# https://github.com/Artheau/SpriteSomething/blob/master/source/meta/classes/spritelib.py#L443 (thanks miketrethewey!)
def dump_zspr(basesprite, basepalette, baseglove, outfilename, author_name, sprite_name):
palettes = basepalette
# Add glove data
palettes.extend(baseglove)
HEADER_STRING = b"ZSPR"
VERSION = 0x01
SPRITE_TYPE = 0x01 # this format has "1" for the player sprite
RESERVED_BYTES = b'\x00\x00\x00\x00\x00\x00'
QUAD_BYTE_NULL_CHAR = b'\x00\x00\x00\x00'
DOUBLE_BYTE_NULL_CHAR = b'\x00\x00'
SINGLE_BYTE_NULL_CHAR = b'\x00'
write_buffer = bytearray()
write_buffer.extend(HEADER_STRING)
write_buffer.extend(struct.pack('B', VERSION)) # as_u8
checksum_start = len(write_buffer)
write_buffer.extend(QUAD_BYTE_NULL_CHAR) # checksum
sprite_sheet_pointer = len(write_buffer)
write_buffer.extend(QUAD_BYTE_NULL_CHAR)
write_buffer.extend(struct.pack('<H', len(basesprite))) # as_u16
palettes_pointer = len(write_buffer)
write_buffer.extend(QUAD_BYTE_NULL_CHAR)
write_buffer.extend(struct.pack('<H', len(palettes))) # as_u16
write_buffer.extend(struct.pack('<H', SPRITE_TYPE)) # as_u16
write_buffer.extend(RESERVED_BYTES)
# sprite.name
write_buffer.extend(sprite_name.encode('utf-16-le'))
write_buffer.extend(DOUBLE_BYTE_NULL_CHAR)
# author.name
write_buffer.extend(author_name.encode('utf-16-le'))
write_buffer.extend(DOUBLE_BYTE_NULL_CHAR)
# author.name-short
write_buffer.extend(author_name.encode('ascii'))
write_buffer.extend(SINGLE_BYTE_NULL_CHAR)
write_buffer[sprite_sheet_pointer:sprite_sheet_pointer +
4] = struct.pack('<L', len(write_buffer)) # as_u32
write_buffer.extend(basesprite)
write_buffer[palettes_pointer:palettes_pointer +
4] = struct.pack('<L', len(write_buffer)) # as_u32
write_buffer.extend(palettes)
checksum = (sum(write_buffer) + 0xFF + 0xFF) % 0x10000
checksum_complement = 0xFFFF - checksum
write_buffer[checksum_start:checksum_start +
2] = struct.pack('<H', checksum) # as_u16
write_buffer[checksum_start + 2:checksum_start +
4] = struct.pack('<H', checksum_complement) # as_u16
with open('%s' % outfilename, "wb") as zspr_file:
zspr_file.write(write_buffer)
def write_sprite(rom, sprite): def write_sprite(rom, sprite):
if not sprite.valid: if not sprite.valid:
return return
@@ -2222,24 +2264,30 @@ def write_strings(rom, world, player, team):
# We still need the older hints of course. Those are done here. # We still need the older hints of course. Those are done here.
no_silver_text = Ganon_Phase_3_No_Silvers_texts[random.randint(0, len(Ganon_Phase_3_No_Silvers_texts) - 1)]
silverarrows = world.find_items('Silver Arrows', player) silverarrows = world.find_items('Silver Arrows', player)
random.shuffle(silverarrows) random.shuffle(silverarrows)
silverarrow_hint = (' %s?' % hint_text(silverarrows[0]).replace('Ganon\'s', 'my')) if silverarrows else '?\nI think not!' if silverarrows:
tt['ganon_phase_3_no_silvers'] = 'Did you find the silver arrows%s' % silverarrow_hint hint_phrase = hint_text(silverarrows[0]).replace("Ganon's", "my")
tt['ganon_phase_3_no_silvers_alt'] = 'Did you find the silver arrows%s' % silverarrow_hint silverarrow_hint = f'Did you find the silver arrows {hint_phrase}?'
else:
silverarrow_hint = no_silver_text
tt['ganon_phase_3_no_silvers'] = silverarrow_hint
tt['ganon_phase_3_no_silvers_alt'] = silverarrow_hint
prog_bow_locs = world.find_items('Progressive Bow', player) prog_bow_locs = world.find_items('Progressive Bow', player)
distinguished_prog_bow_loc = next((location for location in prog_bow_locs if location.item.code == 0x65), None) distinguished_prog_bow_loc = next((location for location in prog_bow_locs if location.item.code == 0x65), None)
progressive_silvers = world.difficulty_requirements[player].progressive_bow_limit >= 2 or world.swords[player] == 'swordless' progressive_silvers = world.difficulty_requirements[player].progressive_bow_limit >= 2 or world.swords[player] == 'swordless'
if distinguished_prog_bow_loc: if distinguished_prog_bow_loc:
prog_bow_locs.remove(distinguished_prog_bow_loc) prog_bow_locs.remove(distinguished_prog_bow_loc)
silverarrow_hint = (' %s?' % hint_text(distinguished_prog_bow_loc).replace('Ganon\'s', 'my')) if progressive_silvers else '?\nI think not!' hint_phrase = hint_text(distinguished_prog_bow_loc).replace("Ganon's", "my")
tt['ganon_phase_3_no_silvers'] = 'Did you find the silver arrows%s' % silverarrow_hint silverarrow_hint = f'Did you find the silver arrows {hint_phrase}?' if progressive_silvers else no_silver_text
tt['ganon_phase_3_no_silvers'] = silverarrow_hint
if any(prog_bow_locs): if any(prog_bow_locs):
silverarrow_hint = (' %s?' % hint_text(random.choice(prog_bow_locs)).replace('Ganon\'s', 'my')) if progressive_silvers else '?\nI think not!' hint_phrase = hint_text(random.choice(prog_bow_locs)).replace("Ganon's", "my")
tt['ganon_phase_3_no_silvers_alt'] = 'Did you find the silver arrows%s' % silverarrow_hint silverarrow_hint = f'Did you find the silver arrows {hint_phrase}?' if progressive_silvers else no_silver_text
tt['ganon_phase_3_no_silvers_alt'] = silverarrow_hint
crystal5 = world.find_items('Crystal 5', player)[0] crystal5 = world.find_items('Crystal 5', player)[0]
crystal6 = world.find_items('Crystal 6', player)[0] crystal6 = world.find_items('Crystal 6', player)[0]

93
Text.py
View File

@@ -21,13 +21,14 @@ text_addresses = {'Pedestal': (0x180300, 256),
Uncle_texts = [ Uncle_texts = [
# these ones are er specific
'Good Luck!\nYou will need it.', 'Good Luck!\nYou will need it.',
'Forward this message to 10 other people or this seed will be awful.', 'Forward this message to 10 other people or this seed will be awful.',
'I hope you like your seeds bootless and fluteless.', 'I hope you like your seeds bootless and fluteless.',
'10\n9\n8\n7\n6\n5\n4\n3\n2\n1\nGo!', '10\n9\n8\n7\n6\n5\n4\n3\n2\n1\nGo!',
'I\'m off to visit cousin Fritzl.', 'I\'m off to visit cousin Fritzl.',
'Don\'t forget to check Antlion Cave.' 'Don\'t forget to check Antlion Cave.'
] * 2 + [ # these ones are from web randomizer
"We're out of\nWeetabix. To\nthe store!", "We're out of\nWeetabix. To\nthe store!",
"This seed is\nbootless\nuntil boots.", "This seed is\nbootless\nuntil boots.",
"Why do we only\nhave one bed?", "Why do we only\nhave one bed?",
@@ -66,15 +67,32 @@ Uncle_texts = [
"Get to the\nchop...\ncastle!", "Get to the\nchop...\ncastle!",
"Come with me\nif you want\nto live", "Come with me\nif you want\nto live",
"I must go\nmy planet\nneeds me", "I must go\nmy planet\nneeds me",
"Are we in\ngo mode yet?",
"Darn, I\nthought this\nwas combo.",
"Don't check\nanything I\nwouldn't!",
"I know where\nthe bow is!\n",
"This message\nwill self\ndestruct.",
"Time to cast\nMeteo on\nGanon!",
"I have a\nlong, full\nlife ahead!",
"Why did that\nsoda have a\nskull on it?",
"Something\nrandom just\ncame up.",
"I'm bad at\nthis. Can you\ndo it for me?",
"Link!\n Wake up!\n ... Bye!",
"Text me when\nyou hit\ngo mode.",
"Turn off the\nstove before\nyou leave.",
"It's raining.\nI'm taking\nthe umbrella.",
"Count to 30.\nThen come\nfind me.",
"Gonna shuffle\nall the items\nreal quick."
] ]
Triforce_texts = [ Triforce_texts = [
# these ones are er specific
'Product has Hole in center. Bad seller, 0 out of 5.', 'Product has Hole in center. Bad seller, 0 out of 5.',
'Who stole the fourth triangle?', 'Who stole the fourth triangle?',
'Trifource?\nMore Like Tritrice, am I right?' 'Trifource?\nMore Like Tritrice, am I right?'
'\n Well Done!', '\n Well Done!',
'You just wasted 2 hours of your life.', 'You just wasted 2 hours of your life.',
'This was meant to be a trapezoid' 'This was meant to be a trapezoid',
] * 2 + [ # these ones are from web randomizer
"\n G G", "\n G G",
"All your base\nare belong\nto us.", "All your base\nare belong\nto us.",
"You have ended\nthe domination\nof Dr. Wily", "You have ended\nthe domination\nof Dr. Wily",
@@ -107,6 +125,13 @@ Triforce_texts = [
"You get one\nwish. Choose\nwisely, hero!", "You get one\nwish. Choose\nwisely, hero!",
"Can you please\nbreak us three\nup? Thanks.", "Can you please\nbreak us three\nup? Thanks.",
" Pick us up\n before we\n get dizzy!", " Pick us up\n before we\n get dizzy!",
"Thank you,\nMikey. Youre\n2 minutes late",
"This was a\n7000 series\ntrain.",
" I'd buy\n that for\n a rupee!",
" Did you like\n that bow\n placement?",
"I promise the\nnext seed will\nbe better.",
"\n Honk.",
"Breakfast\nis served!",
] ]
BombShop2_texts = ['Bombs!\nBombs!\nBiggest!\nBestest!\nGreatest!\nBoomest!'] BombShop2_texts = ['Bombs!\nBombs!\nBiggest!\nBestest!\nGreatest!\nBoomest!']
Sahasrahla2_texts = ['You already got my item, idiot.', 'Why are you still talking to me?', 'This text won\'t change.', 'Have you met my brother, Hasarahshla?'] Sahasrahla2_texts = ['You already got my item, idiot.', 'Why are you still talking to me?', 'This text won\'t change.', 'Have you met my brother, Hasarahshla?']
@@ -145,6 +170,34 @@ Blind_texts = [
"I tried to\ncatch fog,\nbut I mist.", "I tried to\ncatch fog,\nbut I mist.",
"Winter is a\ngreat time\nto chill.", "Winter is a\ngreat time\nto chill.",
"Do you think\nthe Ice Rod\nis cool?", "Do you think\nthe Ice Rod\nis cool?",
"Pyramids?\nI never saw\nthe point.",
"Stone golems\nare created as\nblank slates.",
"Desert humor\nis often dry.\n",
"Ganon is a\nbacon of\ndespair!",
"Butchering\ncows means\nhigh steaks.",
"I can't search\nthe web...\nToo many links",
"I can whistle\nMost pitches\nbut I can't C",
"The Blinds\nStore is\ncurtain death",
"Dark Aga Rooms\nare not a\nbright idea.",
"Best advice\nfor a Goron?\nBe Boulder.",
"Equestrian\nservices are\na stable job.",
"Do I like\ndrills? Just\na bit.",
"I'd shell out\ngood rupees\nfor a conch.",
"Current\naffairs are\nshocking!",
"A lying Goron\ndeals in\nboulderdash.",
"A bread joke?\nEh, it'd be\nhalf baked.",
"I could take\na stab at a\nsword pun.",
"Gloves open\na handful\nof checks",
"Red mail?\nReturn to\nsender.",
"For sale:\nBaby boots,\nNever found",
"SRL or rtGG?\nI prefer the\nLadder",
"Ladders are\nalways up\nto something",
"Zelda's\nfashion is\nvery chic",
"Zombie geese\nare waterfoul.\n",
"I bought some\ncuccos for a\npoultry sum.",
"The stratus of\nclouds is up\nin the air.",
"Tie two ropes\ntogether?!\nI think knot!",
"Time for you\nto go on a\nBlind date!"
] ]
Ganon1_texts = [ Ganon1_texts = [
"Start your day\nsmiling with a\ndelicious\nwhole grain\nbreakfast\ncreated for\nyour\nincredible\ninsides.", "Start your day\nsmiling with a\ndelicious\nwhole grain\nbreakfast\ncreated for\nyour\nincredible\ninsides.",
@@ -171,6 +224,40 @@ Ganon1_texts = [
"Life, dreams,\nhope...\nWhere'd they\ncome from? And\nwhere are they\nheaded? These\nthings... I am\ngoing to\ndestroy!", "Life, dreams,\nhope...\nWhere'd they\ncome from? And\nwhere are they\nheaded? These\nthings... I am\ngoing to\ndestroy!",
"My minions all\nfailed to\nguard those\nitems?!\n\nWhy am I\nsurrounded by\nincompetent\nfools?!", "My minions all\nfailed to\nguard those\nitems?!\n\nWhy am I\nsurrounded by\nincompetent\nfools?!",
] ]
Ganon_Phase_3_No_Silvers_texts = [
"Did you find\nthe arrows on\nPlanet Zebes?",
"Did you find\nthe arrows?\nI think not.",
"Silver arrows?\nI have never\nheard of them",
"Did you find\nthe arrows on\nThe Moon?",
"Did you find\nthe arrows\nIn dev null?",
"I have sold\nthe arrows for\na green big 20",
"Did you find\nThe arrows in\nCount Dracula?",
"Error 404\nSilver arrows\nnot found.",
"No arrows for\nYou today,\nSorry",
"No arrows?\nCheck your\njunk mail."
"Careful, all\nthat spinning\nmakes me dizzy",
"Did you find\nthe arrows in\nJabu's belly?",
"Silver is not\nan appropriate\narrow material",
"Did you find\nthe arrows in\nNarnia?",
"Are you ready\nTo spin\nTo win?",
"DID YOU FIND\nTHE ARROWS IN\nKEFKA'S TOWER",
"Did you find\nthe arrows in\nRecycle Bin?",
"Silver Arrows?\n\nLUL",
"Imagine\nfinding the\narrows",
"Did you find\nsilvers in\nscenic Ohio?",
"Did you find\nThe arrows in\n*mumblemumble*",
"\nSpin To Win!\n",
"did you find\nthe arrows in\nthe hourglass?",
"Silver Arrows\nare so v30",
"OH, NO, THEY\nACTUALLY SAID\nSILVER MARROW",
"SURELY THE\nLEFTMOST TILES\nWILL STAY UP",
"Did you find\nthe arrows in\nWorld 4-2?",
"You Spin Me\nRight Round\nLike A Record",
"SILLY HERO,\nSILVER IS FOR\nWEREWOLVES!",
"Did you find\nthe silvers in\nganti's ears",
]
TavernMan_texts = [ TavernMan_texts = [
"What do you\ncall a blind\ndinosaur?\na doyouthink-\nhesaurus.", "What do you\ncall a blind\ndinosaur?\na doyouthink-\nhesaurus.",
"A blind man\nwalks into\na bar.\nAnd a table.\nAnd a chair.", "A blind man\nwalks into\na bar.\nAnd a table.\nAnd a chair.",

View File

@@ -11,6 +11,8 @@ dw 0
;Hooks ;Hooks
org $02a999 org $02a999
jsl OWEdgeTransition : nop #4 ;LDA $02A4E3,X : ORA $7EF3CA jsl OWEdgeTransition : nop #4 ;LDA $02A4E3,X : ORA $7EF3CA
;org $02e238 ;LDX #$9E : - DEX : DEX : CMP $DAEE,X : BNE -
;jsl OWSpecialTransition : nop #5
; flute menu cancel ; flute menu cancel
org $0ab7af ;LDA $F2 : ORA $F0 : AND #$C0 org $0ab7af ;LDA $F2 : ORA $F0 : AND #$C0
@@ -285,7 +287,8 @@ OWNewDestination:
++ lda $84 : !add 1,s : sta $84 : pla : pla ++ lda $84 : !add 1,s : sta $84 : pla : pla
.adjustMainAxis .adjustMainAxis
LDA $84 : SEC : SBC #$0400 : AND #$0F00 : ASL : XBA : STA $88 ; vram ;LDA $84 : SEC : SBC #$0400 : AND #$0F80 : ASL : XBA : STA $88 ; vram
LDA $84 : SEC : SBC #$0400 : AND #$0F00 : ASL : XBA : STA $88
LDA $84 : SEC : SBC #$0010 : AND #$003E : LSR : STA $86 LDA $84 : SEC : SBC #$0010 : AND #$003E : LSR : STA $86
pla : pla : sep #$10 : ldy $418 pla : pla : sep #$10 : ldy $418
@@ -346,7 +349,7 @@ OWNewDestination:
; turn into bunny ; turn into bunny
lda $5d : cmp #$17 : beq .return lda $5d : cmp #$17 : beq .return
lda #$17 : sta $5d lda #$17 : sta $5d
lda #$01 : sta $02e0 lda #$01 : sta $2e0
bra .return bra .return
.nobunny .nobunny
lda $5d : cmp #$17 : bne .return lda $5d : cmp #$17 : bne .return
@@ -356,6 +359,11 @@ OWNewDestination:
lda $05 : sta $8a lda $05 : sta $8a
rep #$30 : rts rep #$30 : rts
} }
OWSpecialTransition:
{
LDX #$9E
- DEX : DEX : CMP $DAEE,X : BNE -
}
;Data ;Data
org $aaa000 org $aaa000

Binary file not shown.

View File

@@ -354,6 +354,10 @@
"action": "store_true", "action": "store_true",
"type": "bool" "type": "bool"
}, },
"fakeboots": {
"action": "store_true",
"type": "bool"
},
"calc_playthrough": { "calc_playthrough": {
"action": "store_false", "action": "store_false",
"type": "bool" "type": "bool"
@@ -384,7 +388,8 @@
"choices": [ "choices": [
"none", "none",
"shuffled", "shuffled",
"random" "random",
"legacy"
] ]
}, },
"enemy_health": { "enemy_health": {

View File

@@ -290,6 +290,7 @@
"Keys are universal, shooting arrows costs rupees,", "Keys are universal, shooting arrows costs rupees,",
"and a few other little things make this more like Zelda-1. (default: %(default)s)" "and a few other little things make this more like Zelda-1. (default: %(default)s)"
], ],
"fakeboots": [ " Players starts with fake boots that allow dashing but no item checks (default: %(default)s"],
"startinventory": [ "Specifies a list of items that will be in your starting inventory (separated by commas). (default: %(default)s)" ], "startinventory": [ "Specifies a list of items that will be in your starting inventory (separated by commas). (default: %(default)s)" ],
"usestartinventory": [ "Toggle usage of Starting Inventory." ], "usestartinventory": [ "Toggle usage of Starting Inventory." ],
"custom": [ "Not supported." ], "custom": [ "Not supported." ],

View File

@@ -2,6 +2,7 @@
"gui": { "gui": {
"adjust.nobgm": "Disable Music & MSU-1", "adjust.nobgm": "Disable Music & MSU-1",
"adjust.quickswap": "L/R Quickswapping", "adjust.quickswap": "L/R Quickswapping",
"adjust.reduce_flashing": "Reduce Flashing",
"adjust.heartcolor": "Heart Color", "adjust.heartcolor": "Heart Color",
"adjust.heartcolor.red": "Red", "adjust.heartcolor.red": "Red",
@@ -86,6 +87,7 @@
"randomizer.enemizer.enemyshuffle.none": "None", "randomizer.enemizer.enemyshuffle.none": "None",
"randomizer.enemizer.enemyshuffle.shuffled": "Shuffled", "randomizer.enemizer.enemyshuffle.shuffled": "Shuffled",
"randomizer.enemizer.enemyshuffle.random": "Random", "randomizer.enemizer.enemyshuffle.random": "Random",
"randomizer.enemizer.enemyshuffle.legacy": "Random (including Thieves)",
"randomizer.enemizer.bossshuffle": "Boss Shuffle", "randomizer.enemizer.bossshuffle": "Boss Shuffle",
"randomizer.enemizer.bossshuffle.none": "None", "randomizer.enemizer.bossshuffle.none": "None",
@@ -204,6 +206,7 @@
"randomizer.item.hints": "Include Helpful Hints", "randomizer.item.hints": "Include Helpful Hints",
"randomizer.item.retro": "Retro mode (universal keys)", "randomizer.item.retro": "Retro mode (universal keys)",
"randomizer.item.fakeboots": "Start with Fake Boots",
"randomizer.item.worldstate": "World State", "randomizer.item.worldstate": "World State",
"randomizer.item.worldstate.standard": "Standard", "randomizer.item.worldstate.standard": "Standard",

View File

@@ -5,7 +5,8 @@
"options": [ "options": [
"none", "none",
"shuffled", "shuffled",
"random" "random",
"legacy"
] ]
}, },
"bossshuffle": { "bossshuffle": {

View File

@@ -4,7 +4,8 @@
"shopsanity": { "type": "checkbox" }, "shopsanity": { "type": "checkbox" },
"hints": { "hints": {
"type": "checkbox" "type": "checkbox"
} },
"fakeboots": { "type": "checkbox" }
}, },
"leftItemFrame": { "leftItemFrame": {
"worldstate": { "worldstate": {

View File

@@ -58,6 +58,7 @@ SETTINGSTOPROCESS = {
"hints": "hints", "hints": "hints",
"retro": "retro", "retro": "retro",
"shopsanity": "shopsanity", "shopsanity": "shopsanity",
"fakeboots": "fakeboots",
"worldstate": "mode", "worldstate": "mode",
"logiclevel": "logic", "logiclevel": "logic",
"goal": "goal", "goal": "goal",

View File

@@ -1,4 +1,4 @@
from tkinter import ttk, Frame, E, W, LEFT, RIGHT from tkinter import ttk, Frame, E, W, LEFT, RIGHT, Label
import source.gui.widgets as widgets import source.gui.widgets as widgets
import json import json
import os import os
@@ -17,6 +17,9 @@ def item_page(parent):
self.frames["checkboxes"] = Frame(self) self.frames["checkboxes"] = Frame(self)
self.frames["checkboxes"].pack(anchor=W) self.frames["checkboxes"].pack(anchor=W)
various_options = Label(self.frames["checkboxes"], text="")
various_options.pack(side=LEFT)
self.frames["leftItemFrame"] = Frame(self) self.frames["leftItemFrame"] = Frame(self)
self.frames["rightItemFrame"] = Frame(self) self.frames["rightItemFrame"] = Frame(self)
self.frames["leftItemFrame"].pack(side=LEFT) self.frames["leftItemFrame"].pack(side=LEFT)
@@ -34,7 +37,7 @@ def item_page(parent):
self.widgets[key] = dictWidgets[key] self.widgets[key] = dictWidgets[key]
packAttrs = {"anchor":E} packAttrs = {"anchor":E}
if self.widgets[key].type == "checkbox": if self.widgets[key].type == "checkbox":
packAttrs["anchor"] = W packAttrs["side"] = LEFT
self.widgets[key].pack(packAttrs) self.widgets[key].pack(packAttrs)
return self return self