remove remaining sprite data

This commit is contained in:
Fabian Dill 2021-04-27 07:19:53 +02:00
parent e04fbd1d77
commit b8c7d6a72f
4 changed files with 129 additions and 61 deletions

7
data/default.apsprite Normal file
View File

@ -0,0 +1,7 @@
author: Nintendo
data: null
game: A Link to the Past
min_format_version: 1
name: Link
format_version: 1
sprite_version: 1

Binary file not shown.

View File

@ -0,0 +1,7 @@
author: Nintendo
data: null
game: A Link to the Past
min_format_version: 1
name: Link
format_version: 1
sprite_version: 1

View File

@ -14,6 +14,7 @@ import subprocess
import threading import threading
import xxtea import xxtea
import concurrent.futures import concurrent.futures
import bsdiff4
from typing import Optional from typing import Optional
from BaseClasses import CollectionState, Region from BaseClasses import CollectionState, Region
@ -22,10 +23,12 @@ from worlds.alttp.Shops import ShopType
from worlds.alttp.Dungeons import dungeon_music_addresses from worlds.alttp.Dungeons import dungeon_music_addresses
from worlds.alttp.Regions import location_table, old_location_address_to_new_location_address from worlds.alttp.Regions import location_table, old_location_address_to_new_location_address
from worlds.alttp.Text import MultiByteTextMapper, text_addresses, Credits, TextTable from worlds.alttp.Text import MultiByteTextMapper, text_addresses, Credits, TextTable
from worlds.alttp.Text import Uncle_texts, Ganon1_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, Blind_texts, \ from worlds.alttp.Text import Uncle_texts, Ganon1_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, \
Blind_texts, \
BombShop2_texts, junk_texts BombShop2_texts, junk_texts
from worlds.alttp.Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, DeathMountain_texts, \ from worlds.alttp.Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, \
DeathMountain_texts, \
LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, \ LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, \
SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names 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, is_bundled from Utils import output_path, local_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_bundled
@ -41,6 +44,7 @@ except:
enemizer_logger = logging.getLogger("Enemizer") enemizer_logger = logging.getLogger("Enemizer")
class LocalRom(object): class LocalRom(object):
def __init__(self, file, patch=True, vanillaRom=None, name=None, hash=None): def __init__(self, file, patch=True, vanillaRom=None, name=None, hash=None):
@ -428,8 +432,11 @@ def patch_enemizer(world, team: int, player: int, rom: LocalRom, enemizercli):
except OSError: except OSError:
pass pass
tile_list_lock = threading.Lock() tile_list_lock = threading.Lock()
_tile_collection_table = [] _tile_collection_table = []
def _populate_tile_sets(): def _populate_tile_sets():
with tile_list_lock: with tile_list_lock:
if not _tile_collection_table: if not _tile_collection_table:
@ -442,6 +449,7 @@ def _populate_tile_sets():
for file in os.listdir(dir): for file in os.listdir(dir):
pool.submit(load_tileset_from_file, os.path.join(dir, file)) pool.submit(load_tileset_from_file, os.path.join(dir, file))
class TileSet: class TileSet:
def __init__(self, filename): def __init__(self, filename):
with open(filename, 'rt', encoding='utf-8-sig') as file: with open(filename, 'rt', encoding='utf-8-sig') as file:
@ -497,25 +505,23 @@ def _populate_sprite_table():
for file in os.listdir(dir): for file in os.listdir(dir):
pool.submit(load_sprite_from_file, os.path.join(dir, file)) pool.submit(load_sprite_from_file, os.path.join(dir, file))
class Sprite(object):
palette = (255, 127, 126, 35, 183, 17, 158, 54, 165, 20, 255, 1, 120, 16, 157,
89, 71, 54, 104, 59, 74, 10, 239, 18, 92, 42, 113, 21, 24, 122,
255, 127, 126, 35, 183, 17, 158, 54, 165, 20, 255, 1, 120, 16, 157,
89, 128, 105, 145, 118, 184, 38, 127, 67, 92, 42, 153, 17, 24, 122,
255, 127, 126, 35, 183, 17, 158, 54, 165, 20, 255, 1, 120, 16, 157,
89, 87, 16, 126, 69, 243, 109, 185, 126, 92, 42, 39, 34, 24, 122,
255, 127, 126, 35, 218, 17, 158, 54, 165, 20, 255, 1, 120, 16, 151,
61, 71, 54, 104, 59, 74, 10, 239, 18, 126, 86, 114, 24, 24, 122)
glove_palette = (246, 82, 118, 3) class Sprite():
sprite_size = 28672
palette_size = 120
glove_size = 4
author_name: Optional[str] = None author_name: Optional[str] = None
def __init__(self, filename): def __init__(self, filename):
if not hasattr(Sprite, "palette"):
self.get_vanilla_sprite_data()
with open(filename, 'rb') as file: with open(filename, 'rb') as file:
filedata = bytearray(file.read()) filedata = file.read()
self.name = os.path.basename(filename) self.name = os.path.basename(filename)
self.valid = True self.valid = True
if len(filedata) == 0x7000: if filename.endswith(".apsprite"):
self.from_ap_sprite(filedata)
elif len(filedata) == 0x7000:
# sprite file with graphics and without palette data # sprite file with graphics and without palette data
self.sprite = filedata[:0x7000] self.sprite = filedata[:0x7000]
elif len(filedata) == 0x7078: elif len(filedata) == 0x7078:
@ -534,26 +540,70 @@ class Sprite(object):
self.palette = filedata[0xDD308:0xDD380] self.palette = filedata[0xDD308:0xDD380]
self.glove_palette = filedata[0xDEDF5:0xDEDF9] self.glove_palette = filedata[0xDEDF5:0xDEDF9]
elif filedata.startswith(b'ZSPR'): elif filedata.startswith(b'ZSPR'):
result = self.parse_zspr(filedata, 1) self.from_zspr(filedata, filename)
if result is None: else:
self.valid = False self.valid = False
return
(sprite, palette, self.name, self.author_name) = result def get_vanilla_sprite_data(self):
if self.name == "": from Patch import get_base_rom_path
self.name = os.path.split(filename)[1].split(".")[0] file_name = get_base_rom_path()
if len(sprite) != 0x7000: base_rom_bytes = bytes(read_rom(open(file_name, "rb")))
self.valid = False sprite = base_rom_bytes[0x80000:0x87000]
return palette = base_rom_bytes[0xDD308:0xDD380]
self.sprite = sprite glove_palette = base_rom_bytes[0xDEDF5:0xDEDF9]
if len(palette) == 0: Sprite.base_data = sprite + palette + glove_palette
pass
elif len(palette) == 0x78: def from_ap_sprite(self, filedata):
self.palette = palette filedata = filedata.decode("utf-8-sig")
elif len(palette) == 0x7C: import yaml
self.palette = palette[:0x78] obj = yaml.safe_load(filedata)
self.glove_palette = palette[0x78:] if obj["min_format_version"] > 1:
else: raise Exception("Sprite file requires an updated reader.")
self.valid = False self.author_name = obj["author"]
self.name = obj["name"]
if obj["data"]: # skip patching for vanilla content
data = bsdiff4.patch(Sprite.base_data, obj["data"])
self.sprite = data[:self.sprite_size]
self.palette = data[self.sprite_size:self.palette_size]
self.glove_palette = data[self.sprite_size + self.palette_size:]
def to_ap_sprite(self, path):
from .. import Games
import yaml
payload = {"format_version": 1,
"min_format_version": 1,
"sprite_version": 1,
"name": self.name,
"author": self.author_name,
"game": Games.LTTP.value,
"data": self.get_delta()}
with open(path, "w") as f:
f.write(yaml.safe_dump(payload))
def get_delta(self):
modified_data = self.sprite + self.palette + self.glove_palette
return bsdiff4.diff(Sprite.base_data, modified_data)
def from_zspr(self, filedata, filename):
result = self.parse_zspr(filedata, 1)
if result is None:
self.valid = False
return
(sprite, palette, self.name, self.author_name) = result
if self.name == "":
self.name = os.path.split(filename)[1].split(".")[0]
if len(sprite) != 0x7000:
self.valid = False
return
self.sprite = sprite
if len(palette) == 0:
pass
elif len(palette) == 0x78:
self.palette = palette
elif len(palette) == 0x7C:
self.palette = palette[:0x78]
self.glove_palette = palette[0x78:]
else: else:
self.valid = False self.valid = False
@ -569,7 +619,7 @@ class Sprite(object):
@staticmethod @staticmethod
def default_link_sprite(): def default_link_sprite():
return Sprite(local_path('../../data', 'default.zspr')) return Sprite(local_path('data', 'default.apsprite'))
def decode8(self, pos): def decode8(self, pos):
arr = [[0 for _ in range(8)] for _ in range(8)] arr = [[0 for _ in range(8)] for _ in range(8)]
@ -603,12 +653,12 @@ class Sprite(object):
return arr return arr
def parse_zspr(self, filedata, expected_kind): def parse_zspr(self, filedata, expected_kind):
logger = logging.getLogger('') logger = logging.getLogger('ZSPR')
headerstr = "<4xBHHIHIHH6x" headerstr = "<4xBHHIHIHH6x"
headersize = struct.calcsize(headerstr) headersize = struct.calcsize(headerstr)
if len(filedata) < headersize: if len(filedata) < headersize:
return None return None
(version, csum, icsum, sprite_offset, sprite_size, palette_offset, palette_size, kind) = struct.unpack_from( version, csum, icsum, sprite_offset, sprite_size, palette_offset, palette_size, kind = struct.unpack_from(
headerstr, filedata) headerstr, filedata)
if version not in [1]: if version not in [1]:
logger.error('Error parsing ZSPR file: Version %g not supported', version) logger.error('Error parsing ZSPR file: Version %g not supported', version)
@ -657,7 +707,7 @@ class Sprite(object):
return pair[1] << 8 | pair[0] return pair[1] << 8 | pair[0]
def expand_color(i): def expand_color(i):
return ((i & 0x1F) * 8, (i >> 5 & 0x1F) * 8, (i >> 10 & 0x1F) * 8) return (i & 0x1F) * 8, (i >> 5 & 0x1F) * 8, (i >> 10 & 0x1F) * 8
# turn palette data into a list of RGB tuples with 8 bit values # turn palette data into a list of RGB tuples with 8 bit values
palette_as_colors = [expand_color(make_int16(chnk)) for chnk in array_chunk(self.palette, 2)] palette_as_colors = [expand_color(make_int16(chnk)) for chnk in array_chunk(self.palette, 2)]
@ -689,6 +739,7 @@ bonk_addresses = [0x4CF6C, 0x4CFBA, 0x4CFE0, 0x4CFFB, 0x4D018, 0x4D01B, 0x4D028,
0x4D3F8, 0x4D416, 0x4D420, 0x4D423, 0x4D42D, 0x4D449, 0x4D48C, 0x4D4D9, 0x4D4DC, 0x4D4E3, 0x4D3F8, 0x4D416, 0x4D420, 0x4D423, 0x4D42D, 0x4D449, 0x4D48C, 0x4D4D9, 0x4D4DC, 0x4D4E3,
0x4D504, 0x4D507, 0x4D55E, 0x4D56A] 0x4D504, 0x4D507, 0x4D55E, 0x4D56A]
def patch_rom(world, rom, player, team, enemized): def patch_rom(world, rom, player, team, enemized):
local_random = world.rom_seeds[player] local_random = world.rom_seeds[player]
@ -751,7 +802,6 @@ def patch_rom(world, rom, player, team, enemized):
for music_address in music_addresses: for music_address in music_addresses:
rom.write_byte(music_address, music) rom.write_byte(music_address, music)
if world.mapshuffle[player]: if world.mapshuffle[player]:
rom.write_byte(0x155C9, local_random.choice([0x11, 0x16])) # Randomize GT music too with map shuffle rom.write_byte(0x155C9, local_random.choice([0x11, 0x16])) # Randomize GT music too with map shuffle
@ -1070,7 +1120,6 @@ def patch_rom(world, rom, player, team, enemized):
byte = int(rom.read_byte(address)) byte = int(rom.read_byte(address))
rom.write_byte(address, prize_replacements.get(byte, byte)) rom.write_byte(address, prize_replacements.get(byte, byte))
# Fill in item substitutions table # Fill in item substitutions table
rom.write_bytes(0x184000, [ rom.write_bytes(0x184000, [
# original_item, limit, replacement_item, filler # original_item, limit, replacement_item, filler
@ -1146,9 +1195,12 @@ def patch_rom(world, rom, player, team, enemized):
# Set up requested clock settings # Set up requested clock settings
if world.clock_mode[player] in ['countdown-ohko', 'stopwatch', 'countdown']: if world.clock_mode[player] in ['countdown-ohko', 'stopwatch', 'countdown']:
rom.write_int32(0x180200, world.red_clock_time[player] * 60 * 60) # red clock adjustment time (in frames, sint32) rom.write_int32(0x180200,
rom.write_int32(0x180204, world.blue_clock_time[player] * 60 * 60) # blue clock adjustment time (in frames, sint32) world.red_clock_time[player] * 60 * 60) # red clock adjustment time (in frames, sint32)
rom.write_int32(0x180208, world.green_clock_time[player] * 60 * 60) # green clock adjustment time (in frames, sint32) rom.write_int32(0x180204,
world.blue_clock_time[player] * 60 * 60) # blue clock adjustment time (in frames, sint32)
rom.write_int32(0x180208,
world.green_clock_time[player] * 60 * 60) # green clock adjustment time (in frames, sint32)
else: else:
rom.write_int32(0x180200, 0) # red clock adjustment time (in frames, sint32) rom.write_int32(0x180200, 0) # red clock adjustment time (in frames, sint32)
rom.write_int32(0x180204, 0) # blue clock adjustment time (in frames, sint32) rom.write_int32(0x180204, 0) # blue clock adjustment time (in frames, sint32)
@ -1507,7 +1559,8 @@ def patch_rom(world, rom, player, team, enemized):
rom.write_byte(0xEFD95, digging_game_rng) rom.write_byte(0xEFD95, digging_game_rng)
rom.write_byte(0x1800A3, 0x01) # enable correct world setting behaviour after agahnim kills rom.write_byte(0x1800A3, 0x01) # enable correct world setting behaviour after agahnim kills
rom.write_byte(0x1800A4, 0x01 if world.logic[player] != 'nologic' else 0x00) # enable POD EG fix rom.write_byte(0x1800A4, 0x01 if world.logic[player] != 'nologic' else 0x00) # enable POD EG fix
rom.write_byte(0x186383, 0x01 if world.glitch_triforce or world.logic[player] == 'nologic' else 0x00) # disable glitching to Triforce from Ganons Room rom.write_byte(0x186383, 0x01 if world.glitch_triforce or world.logic[
player] == 'nologic' else 0x00) # disable glitching to Triforce from Ganons Room
rom.write_byte(0x180042, 0x01 if world.save_and_quit_from_boss else 0x00) # Allow Save and Quit after boss kill rom.write_byte(0x180042, 0x01 if world.save_and_quit_from_boss else 0x00) # Allow Save and Quit after boss kill
# remove shield from uncle # remove shield from uncle
@ -1579,7 +1632,6 @@ def patch_rom(world, rom, player, team, enemized):
rom.write_byte(0x4BA1D, tile_set.get_len()) rom.write_byte(0x4BA1D, tile_set.get_len())
rom.write_bytes(0x4BA2A, tile_set.get_bytes()) rom.write_bytes(0x4BA2A, tile_set.get_bytes())
write_strings(rom, world, player, team) write_strings(rom, world, player, team)
rom.write_byte(0x18637C, 1 if world.remote_items[player] else 0) rom.write_byte(0x18637C, 1 if world.remote_items[player] else 0)
@ -1650,7 +1702,7 @@ def write_custom_shops(rom, world, player):
slot = 0 if shop.type == ShopType.TakeAny else index slot = 0 if shop.type == ShopType.TakeAny else index
if item is None: if item is None:
break break
if not item['item'] in item_table: # item not native to ALTTP if not item['item'] in item_table: # item not native to ALTTP
item_code = 0x09 # Hammer item_code = 0x09 # Hammer
else: else:
item_code = ItemFactory(item['item'], player).code item_code = ItemFactory(item['item'], player).code
@ -1691,7 +1743,8 @@ def hud_format_text(text):
def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, sprite: str, palettes_options, def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, sprite: str, palettes_options,
world=None, player=1, allow_random_on_event=False, reduceflashing=False, triforcehud:str = None): world=None, player=1, allow_random_on_event=False, reduceflashing=False,
triforcehud: str = None):
local_random = random if not world else world.rom_seeds[player] local_random = random if not world else world.rom_seeds[player]
# enable instant item menu # enable instant item menu
@ -1716,22 +1769,22 @@ def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, spr
else: else:
rom.write_byte(0x180048, 0x08) rom.write_byte(0x180048, 0x08)
# Reduce flashing by nopping out instructions # Reduce flashing by nopping out instructions
if reduceflashing: if reduceflashing:
rom.write_bytes(0x17E07, [0x06]) # reduce amount of colors changed, add this branch if we need to reduce more ""+ [0x80] + [(0x81-0x08)]"" rom.write_bytes(0x17E07, [
rom.write_bytes(0x17EAB, [0xD0, 0x03, 0xA9, 0x40, 0x29, 0x60]) # nullifies aga lightning, cutscene, vitreous, bat, ether 0x06]) # reduce amount of colors changed, add this branch if we need to reduce more ""+ [0x80] + [(0x81-0x08)]""
rom.write_bytes(0x17EAB,
[0xD0, 0x03, 0xA9, 0x40, 0x29, 0x60]) # nullifies aga lightning, cutscene, vitreous, bat, ether
# ONLY write to black values with this low pale blue to indicate flashing, that's IT. ""BNE + : LDA #$2940 : + : RTS"" # ONLY write to black values with this low pale blue to indicate flashing, that's IT. ""BNE + : LDA #$2940 : + : RTS""
rom.write_bytes(0x123FE, [0x72]) # set lightning flash in misery mire (and standard) to brightness 0x72 rom.write_bytes(0x123FE, [0x72]) # set lightning flash in misery mire (and standard) to brightness 0x72
rom.write_bytes(0x3FA7B, [0x80, 0xac-0x7b]) # branch from palette writing lightning on death mountain rom.write_bytes(0x3FA7B, [0x80, 0xac - 0x7b]) # branch from palette writing lightning on death mountain
rom.write_byte(0x10817F, 0x01) # internal rom option rom.write_byte(0x10817F, 0x01) # internal rom option
else: else:
rom.write_bytes(0x17E07, [0x00]) rom.write_bytes(0x17E07, [0x00])
rom.write_bytes(0x17EAB, [0x85, 0x00, 0x29, 0x1F, 0x00, 0x18]) rom.write_bytes(0x17EAB, [0x85, 0x00, 0x29, 0x1F, 0x00, 0x18])
rom.write_bytes(0x123FE, [0x32]) # original weather flash value rom.write_bytes(0x123FE, [0x32]) # original weather flash value
rom.write_bytes(0x3FA7B, [0xc2, 0x20]) # rep #$20 rom.write_bytes(0x3FA7B, [0xc2, 0x20]) # rep #$20
rom.write_byte(0x10817F, 0x00) # internal rom option rom.write_byte(0x10817F, 0x00) # internal rom option
rom.write_byte(0x18004B, 0x01 if quickswap else 0x00) rom.write_byte(0x18004B, 0x01 if quickswap else 0x00)
@ -1766,7 +1819,8 @@ def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, spr
if triforcehud: if triforcehud:
# set triforcehud # set triforcehud
triforce_flag = (rom.read_byte(0x180167) & 0x80) | {'normal': 0x00, 'hide_goal': 0x01, 'hide_required': 0x02, 'hide_both': 0x03}[triforcehud] triforce_flag = (rom.read_byte(0x180167) & 0x80) | \
{'normal': 0x00, 'hide_goal': 0x01, 'hide_required': 0x02, 'hide_both': 0x03}[triforcehud]
rom.write_byte(0x180167, triforce_flag) rom.write_byte(0x180167, triforce_flag)
if z3pr: if z3pr:
@ -2045,7 +2099,7 @@ def write_strings(rom, world, player, team):
f"\n ≥ Duh\n Oh carp\n{{CHOICE}}" f"\n ≥ Duh\n Oh carp\n{{CHOICE}}"
# Bottle Vendor hint # Bottle Vendor hint
vendor_location = world.get_location("Bottle Merchant", player) vendor_location = world.get_location("Bottle Merchant", player)
tt['bottle_vendor_choice'] = f"I gots {hint_text(vendor_location.item)}\nYous gots 100 rupees?"\ tt['bottle_vendor_choice'] = f"I gots {hint_text(vendor_location.item)}\nYous gots 100 rupees?" \
f"\n ≥ I want\n no way!\n{{CHOICE}}" f"\n ≥ I want\n no way!\n{{CHOICE}}"
tt['sign_north_of_links_house'] = '> Randomizer The telepathic tiles can have hints!' tt['sign_north_of_links_house'] = '> Randomizer The telepathic tiles can have hints!'
@ -2394,7 +2448,7 @@ def set_inverted_mode(world, player, rom):
rom.write_byte(0xDC21D, 0x6B) # inverted mode flute activation (skip weathervane overlay) rom.write_byte(0xDC21D, 0x6B) # inverted mode flute activation (skip weathervane overlay)
rom.write_bytes(0x48DB3, [0xF8, 0x01]) # inverted mode (bird X) rom.write_bytes(0x48DB3, [0xF8, 0x01]) # inverted mode (bird X)
rom.write_byte(0x48D5E, 0x01) # inverted mode (rock X) rom.write_byte(0x48D5E, 0x01) # inverted mode (rock X)
rom.write_bytes(0x48CC1+36, bytes([0xF8]*12)) # (rock X) rom.write_bytes(0x48CC1 + 36, bytes([0xF8] * 12)) # (rock X)
rom.write_int16s(snes_to_pc(0x02E849), rom.write_int16s(snes_to_pc(0x02E849),
[0x0043, 0x0056, 0x0058, 0x006C, 0x006F, 0x0070, 0x007B, 0x007F, 0x001B]) # dw flute [0x0043, 0x0056, 0x0058, 0x006C, 0x006F, 0x0070, 0x007B, 0x007F, 0x001B]) # dw flute
rom.write_int16(snes_to_pc(0x02E8D5), 0x07C8) rom.write_int16(snes_to_pc(0x02E8D5), 0x07C8)