Enemizer support

This commit is contained in:
Bonta-kun 2019-05-30 01:10:16 +02:00 committed by Kevin Cathcart
parent 0b9e144f8e
commit 16abf033c3
5 changed files with 238 additions and 17 deletions

1
.gitignore vendored
View File

@ -11,3 +11,4 @@ README.html
.vs/
*multidata
*multisave
EnemizerCLI/

View File

@ -207,14 +207,19 @@ def start():
''')
parser.add_argument('--suppress_rom', help='Do not create an output rom file.', action='store_true')
parser.add_argument('--gui', help='Launch the GUI', action='store_true')
# Deliberately not documented, only useful for vt site integration right now:
parser.add_argument('--shufflebosses', help=argparse.SUPPRESS, default='none', const='none', nargs='?', choices=['none', 'basic', 'normal', 'chaos'])
parser.add_argument('--jsonout', action='store_true', help='''\
Output .json patch to stdout instead of a patched rom. Used
for VT site integration, do not use otherwise.
''')
parser.add_argument('--multi', default=1, type=lambda value: min(max(int(value), 1), 255))
parser.add_argument('--skip_playthrough', action='store_true', default=False)
parser.add_argument('--enemizercli', default='')
parser.add_argument('--shufflebosses', default='none', choices=['none', 'basic', 'normal', 'chaos'])
parser.add_argument('--shuffleenemies', default=False, action='store_true')
parser.add_argument('--enemy_health', default='default', choices=['default', 'easy', 'normal', 'hard', 'expert'])
parser.add_argument('--enemy_damage', default='default', choices=['default', 'shuffled', 'chaos'])
parser.add_argument('--shufflepalette', default=False, action='store_true')
parser.add_argument('--shufflepots', default=False, action='store_true')
parser.add_argument('--multi', default=1, type=lambda value: min(max(int(value), 1), 255))
parser.add_argument('--outputpath')
args = parser.parse_args()

70
Gui.py
View File

@ -5,7 +5,7 @@ import json
import random
import os
import shutil
from tkinter import Checkbutton, OptionMenu, Toplevel, LabelFrame, PhotoImage, Tk, LEFT, RIGHT, BOTTOM, TOP, StringVar, IntVar, Frame, Label, W, E, X, Entry, Spinbox, Button, filedialog, messagebox, ttk
from tkinter import Checkbutton, OptionMenu, Toplevel, LabelFrame, PhotoImage, Tk, LEFT, RIGHT, BOTTOM, TOP, StringVar, IntVar, Frame, Label, W, E, X, BOTH, Entry, Spinbox, Button, filedialog, messagebox, ttk
from urllib.parse import urlparse
from urllib.request import urlopen
@ -242,12 +242,67 @@ def guiMain(args=None):
heartcolorFrame.pack(expand=True, anchor=E)
fastMenuFrame.pack(expand=True, anchor=E)
bottomFrame = Frame(randomizerWindow)
enemizerFrame = LabelFrame(randomizerWindow, text="Enemizer", padx=5, pady=5)
enemizerFrame.columnconfigure(0, weight=1)
enemizerFrame.columnconfigure(1, weight=1)
enemizerFrame.columnconfigure(2, weight=1)
enemizerPathFrame = Frame(enemizerFrame)
enemizerPathFrame.grid(row=0, column=0, columnspan=3, sticky=W)
enemizerCLIlabel = Label(enemizerPathFrame, text="EnemizerCLI path: ")
enemizerCLIlabel.pack(side=LEFT)
enemizerCLIpathVar = StringVar()
enemizerCLIpathEntry = Entry(enemizerPathFrame, textvariable=enemizerCLIpathVar, width=80)
enemizerCLIpathEntry.pack(side=LEFT)
def EnemizerSelectPath():
path = filedialog.askopenfilename(filetypes=[("EnemizerCLI executable", "*EnemizerCLI*")])
if path:
enemizerCLIpathVar.set(path)
enemizerCLIbrowseButton = Button(enemizerPathFrame, text='...', command=EnemizerSelectPath)
enemizerCLIbrowseButton.pack(side=LEFT)
enemyShuffleVar = IntVar()
enemyShuffleButton = Checkbutton(enemizerFrame, text="Enemy shuffle", variable=enemyShuffleVar)
enemyShuffleButton.grid(row=1, column=0)
paletteShuffleVar = IntVar()
paletteShuffleButton = Checkbutton(enemizerFrame, text="Palette shuffle", variable=paletteShuffleVar)
paletteShuffleButton.grid(row=1, column=1)
potShuffleVar = IntVar()
potShuffleButton = Checkbutton(enemizerFrame, text="Pot shuffle", variable=potShuffleVar)
potShuffleButton.grid(row=1, column=2)
enemizerBossFrame = Frame(enemizerFrame)
enemizerBossFrame.grid(row=2, column=0)
enemizerBossLabel = Label(enemizerBossFrame, text='Boss shuffle')
enemizerBossLabel.pack(side=LEFT)
enemizerBossVar = StringVar()
enemizerBossVar.set('none')
enemizerBossOption = OptionMenu(enemizerBossFrame, enemizerBossVar, 'none', 'basic', 'normal', 'chaos')
enemizerBossOption.pack(side=LEFT)
enemizerDamageFrame = Frame(enemizerFrame)
enemizerDamageFrame.grid(row=2, column=1)
enemizerDamageLabel = Label(enemizerDamageFrame, text='Enemy damage')
enemizerDamageLabel.pack(side=LEFT)
enemizerDamageVar = StringVar()
enemizerDamageVar.set('default')
enemizerDamageOption = OptionMenu(enemizerDamageFrame, enemizerDamageVar, 'default', 'shuffled', 'chaos')
enemizerDamageOption.pack(side=LEFT)
enemizerHealthFrame = Frame(enemizerFrame)
enemizerHealthFrame.grid(row=2, column=2)
enemizerHealthLabel = Label(enemizerHealthFrame, text='Enemy health')
enemizerHealthLabel.pack(side=LEFT)
enemizerHealthVar = StringVar()
enemizerHealthVar.set('default')
enemizerHealthOption = OptionMenu(enemizerHealthFrame, enemizerHealthVar, 'default', 'easy', 'normal', 'hard', 'expert')
enemizerHealthOption.pack(side=LEFT)
bottomFrame = Frame(randomizerWindow, pady=5)
worldLabel = Label(bottomFrame, text='Worlds')
worldVar = StringVar()
worldSpinbox = Spinbox(bottomFrame, from_=1, to=100, width=5, textvariable=worldVar)
seedLabel = Label(bottomFrame, text='Seed #')
seedVar = StringVar()
seedEntry = Entry(bottomFrame, width=15, textvariable=seedVar)
@ -281,6 +336,13 @@ def guiMain(args=None):
guiargs.disablemusic = bool(disableMusicVar.get())
guiargs.shuffleganon = bool(shuffleGanonVar.get())
guiargs.hints = bool(hintsVar.get())
guiargs.enemizercli = enemizerCLIpathVar.get()
guiargs.shufflebosses = enemizerBossVar.get()
guiargs.shuffleenemies = bool(enemyShuffleVar.get())
guiargs.enemy_health = enemizerHealthVar.get()
guiargs.enemy_damage = enemizerDamageVar.get()
guiargs.shufflepalette = bool(paletteShuffleVar.get())
guiargs.shufflepots = bool(potShuffleVar.get())
guiargs.custom = bool(customVar.get())
guiargs.customitemarray = [int(bowVar.get()), int(silverarrowVar.get()), int(boomerangVar.get()), int(magicboomerangVar.get()), int(hookshotVar.get()), int(mushroomVar.get()), int(magicpowderVar.get()), int(firerodVar.get()),
int(icerodVar.get()), int(bombosVar.get()), int(etherVar.get()), int(quakeVar.get()), int(lampVar.get()), int(hammerVar.get()), int(shovelVar.get()), int(fluteVar.get()), int(bugnetVar.get()),
@ -291,7 +353,6 @@ def guiMain(args=None):
int(arrow1Var.get()), int(arrow10Var.get()), int(bomb1Var.get()), int(bomb3Var.get()), int(rupee1Var.get()), int(rupee5Var.get()), int(rupee20Var.get()), int(rupee50Var.get()), int(rupee100Var.get()),
int(rupee300Var.get()), int(rupoorVar.get()), int(blueclockVar.get()), int(greenclockVar.get()), int(redclockVar.get()), int(triforcepieceVar.get()), int(triforcecountVar.get()),
int(triforceVar.get()), int(rupoorcostVar.get()), int(universalkeyVar.get())]
guiargs.shufflebosses = None
guiargs.rom = romVar.get()
guiargs.jsonout = None
guiargs.sprite = sprite
@ -326,6 +387,7 @@ def guiMain(args=None):
rightHalfFrame.pack(side=RIGHT)
topFrame.pack(side=TOP)
bottomFrame.pack(side=BOTTOM)
enemizerFrame.pack(side=BOTTOM, fill=BOTH)
# Adjuster Controls

28
Main.py
View File

@ -3,13 +3,14 @@ import copy
from itertools import zip_longest
import json
import logging
import os
import random
import time
from BaseClasses import World, CollectionState, Item, Region, Location, Shop
from Regions import create_regions, mark_light_world_regions
from EntranceShuffle import link_entrances
from Rom import patch_rom, Sprite, LocalRom, JsonRom
from Rom import patch_rom, get_enemizer_patch, apply_rom_settings, Sprite, LocalRom, JsonRom
from Rules import set_rules
from Dungeons import create_dungeons, fill_dungeons, fill_dungeons_restrictive
from Fill import distribute_items_cutoff, distribute_items_staleness, distribute_items_restrictive, flood_items, balance_multiworld_progression
@ -124,6 +125,8 @@ def main(args, seed=None):
outfilebase = 'ER_%s_%s-%s-%s%s_%s-%s%s%s%s%s_%s' % (world.logic, world.difficulty, world.mode, world.goal, "" if world.timer in ['none', 'display'] else "-" + world.timer, world.shuffle, world.algorithm, "-keysanity" if world.keysanity else "", "-retro" if world.retro else "", "-prog_" + world.progressive if world.progressive in ['off', 'random'] else "", "-nohints" if not world.hints else "", world.seed)
use_enemizer = args.enemizercli and (args.shufflebosses != 'none' or args.shuffleenemies or args.enemy_health != 'default' or args.enemy_health != 'default' or args.enemy_damage or args.shufflepalette or args.shufflepots)
jsonout = {}
if not args.suppress_rom:
if world.players > 1:
@ -131,17 +134,32 @@ def main(args, seed=None):
else:
player = 1
local_rom = None
if args.jsonout:
rom = JsonRom()
else:
rom = LocalRom(args.rom)
patch_rom(world, player, rom, bytearray(logic_hash), args.heartbeep, args.heartcolor, sprite, player_names)
if use_enemizer:
local_rom = LocalRom(args.rom)
rom = JsonRom()
else:
rom = LocalRom(args.rom)
patch_rom(world, player, rom, bytearray(logic_hash))
enemizer_patch = []
if use_enemizer:
enemizer_patch = get_enemizer_patch(world, player, rom, args.rom, args.enemizercli, args.shuffleenemies, args.enemy_health, args.enemy_damage, args.shufflepalette, args.shufflepots)
if args.jsonout:
jsonout['patch'] = rom.patches
if use_enemizer:
jsonout['enemizer' % player] = enemizer_patch
else:
apply_rom_settings(rom, args.heartbeep, args.heartcolor, world.quickswap, world.fastmenu, world.disable_music, sprite, player_names)
if use_enemizer:
local_rom.patch_enemizer(rom.patches, os.path.join(os.path.dirname(args.enemizercli), "enemizerBasePatch.json"), enemizer_patch)
rom = local_rom
apply_rom_settings(rom, args.heartbeep, args.heartcolor, world.quickswap, world.fastmenu, world.disable_music, sprite)
rom.write_to_file(output_path('%s.sfc' % outfilebase))
if args.create_spoiler and not args.jsonout:

145
Rom.py
View File

@ -4,6 +4,7 @@ import hashlib
import logging
import os
import struct
import subprocess
import random
from BaseClasses import ShopType, Region, Location, Item
@ -11,7 +12,7 @@ from Dungeons import dungeon_music_addresses
from Text import MultiByteTextMapper, 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 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 local_path, int16_as_bytes, int32_as_bytes
from Utils import output_path, local_path, int16_as_bytes, int32_as_bytes
from Items import ItemFactory, item_table
@ -84,7 +85,7 @@ class LocalRom(object):
logging.getLogger('').warning('Supplied Base Rom does not match known MD5 for JAP(1.0) release. Will try to patch anyway.')
# extend to 2MB
self.buffer.extend(bytearray([0x00] * (2097152 - len(self.buffer))))
self.buffer.extend(bytearray([0x00] * (0x200000 - len(self.buffer))))
# load randomizer patches
with open(local_path('data/base2current.json'), 'r') as stream:
@ -100,6 +101,24 @@ class LocalRom(object):
if RANDOMIZERBASEHASH != patchedmd5.hexdigest():
raise RuntimeError('Provided Base Rom unsuitable for patching. Please provide a JAP(1.0) "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc" rom to use as a base.')
def patch_enemizer(self, rando_patch, base_enemizer_patch_path, enemizer_patch):
# extend to 4MB
self.buffer.extend(bytearray([0x00] * (0x400000 - len(self.buffer))))
# apply randomizer patches
for address, values in rando_patch.items():
self.write_bytes(int(address), values)
# load base enemizer patches
with open(base_enemizer_patch_path, 'r') as f:
base_enemizer_patch = json.load(f)
for patch in base_enemizer_patch:
self.write_bytes(patch["address"], patch["patchData"])
# apply enemizer patches
for patch in enemizer_patch:
self.write_bytes(patch["address"], patch["patchData"])
def write_crc(self):
crc = (sum(self.buffer[:0x7FDC] + self.buffer[0x7FE0:]) + 0x01FE) & 0xFFFF
inv = crc ^ 0xFFFF
@ -117,6 +136,124 @@ def read_rom(stream):
buffer = buffer[0x200:]
return buffer
def get_enemizer_patch(world, player, rom, baserom_path, enemizercli, shuffleenemies, enemy_health, enemy_damage, shufflepalette, shufflepots):
baserom_path = os.path.abspath(baserom_path)
basepatch_path = os.path.abspath(local_path('data/base2current.json'))
randopatch_path = os.path.abspath(output_path('enemizer_randopatch.json'))
options_path = os.path.abspath(output_path('enemizer_options.json'))
enemizer_output_path = os.path.abspath(output_path('enemizer_output.json'))
# write options file for enemizer
options = {
'RandomizeEnemies': shuffleenemies,
'RandomizeEnemiesType': 3,
'RandomizeBushEnemyChance': True,
'RandomizeEnemyHealthRange': enemy_health != 'default',
'RandomizeEnemyHealthType': {'default': 0, 'easy': 0, 'normal': 1, 'hard': 2, 'expert': 3}[enemy_health],
'OHKO': False,
'RandomizeEnemyDamage': enemy_damage != 'default',
'AllowEnemyZeroDamage': True,
'ShuffleEnemyDamageGroups': enemy_damage != 'default',
'EnemyDamageChaosMode': enemy_damage == 'chaos',
'EasyModeEscape': False,
'EnemiesAbsorbable': False,
'AbsorbableSpawnRate': 10,
'AbsorbableTypes': {
'FullMagic': True, 'SmallMagic': True, 'Bomb_1': True, 'BlueRupee': True, 'Heart': True, 'BigKey': True, 'Key': True,
'Fairy': True, 'Arrow_10': True, 'Arrow_5': True, 'Bomb_8': True, 'Bomb_4': True, 'GreenRupee': True, 'RedRupee': True
},
'BossMadness': False,
'RandomizeBosses': True,
'RandomizeBossesType': 0,
'RandomizeBossHealth': False,
'RandomizeBossHealthMinAmount': 0,
'RandomizeBossHealthMaxAmount': 300,
'RandomizeBossDamage': False,
'RandomizeBossDamageMinAmount': 0,
'RandomizeBossDamageMaxAmount': 200,
'RandomizeBossBehavior': False,
'RandomizeDungeonPalettes': shufflepalette,
'SetBlackoutMode': False,
'RandomizeOverworldPalettes': shufflepalette,
'RandomizeSpritePalettes': shufflepalette,
'SetAdvancedSpritePalettes': False,
'PukeMode': False,
'NegativeMode': False,
'GrayscaleMode': False,
'GenerateSpoilers': False,
'RandomizeLinkSpritePalette': False,
'RandomizePots': shufflepots,
'ShuffleMusic': False,
'BootlegMagic': True,
'CustomBosses': False,
'AndyMode': False,
'HeartBeepSpeed': 0,
'AlternateGfx': False,
'ShieldGraphics': "shield_gfx/normal.gfx",
'SwordGraphics': "sword_gfx/normal.gfx",
'BeeMizer': False,
'BeesLevel': 0,
'RandomizeTileTrapPattern': True,
'RandomizeTileTrapFloorTile': False,
'AllowKillableThief': shuffleenemies,
'RandomizeSpriteOnHit': False,
'DebugMode': False,
'DebugForceEnemy': False,
'DebugForceEnemyId': 0,
'DebugForceBoss': False,
'DebugForceBossId': 0,
'DebugOpenShutterDoors': False,
'DebugForceEnemyDamageZero': False,
'DebugShowRoomIdInRupeeCounter': False,
'UseManualBosses': True,
'ManualBosses': {
'EasternPalace': world.get_dungeon("Eastern Palace", player).boss.enemizer_name,
'DesertPalace': world.get_dungeon("Desert Palace", player).boss.enemizer_name,
'TowerOfHera': world.get_dungeon("Tower of Hera", player).boss.enemizer_name,
'AgahnimsTower': 'Agahnim',
'PalaceOfDarkness': world.get_dungeon("Palace of Darkness", player).boss.enemizer_name,
'SwampPalace': world.get_dungeon("Swamp Palace", player).boss.enemizer_name,
'SkullWoods': world.get_dungeon("Skull Woods", player).boss.enemizer_name,
'ThievesTown': world.get_dungeon("Thieves Town", player).boss.enemizer_name,
'IcePalace': world.get_dungeon("Ice Palace", player).boss.enemizer_name,
'MiseryMire': world.get_dungeon("Misery Mire", player).boss.enemizer_name,
'TurtleRock': world.get_dungeon("Turtle Rock", player).boss.enemizer_name,
'GanonsTower1': world.get_dungeon('Ganons Tower', player).bosses['bottom'].enemizer_name,
'GanonsTower2': world.get_dungeon('Ganons Tower', player).bosses['middle'].enemizer_name,
'GanonsTower3': world.get_dungeon('Ganons Tower', player).bosses['top'].enemizer_name,
'GanonsTower4': 'Agahnim2',
'Ganon': 'Ganon',
}
}
rom.write_to_file(randopatch_path)
with open(options_path, 'w') as f:
json.dump(options, f)
subprocess.check_call([os.path.abspath(enemizercli),
'--rom', baserom_path,
'--seed', str(world.rom_seeds[player]),
'--base', basepatch_path,
'--randomizer', randopatch_path,
'--enemizer', options_path,
'--output', enemizer_output_path],
cwd=os.path.dirname(enemizercli), stdout=subprocess.DEVNULL)
with open(enemizer_output_path, 'r') as f:
ret = json.load(f)
if os.path.exists(randopatch_path):
os.remove(randopatch_path)
if os.path.exists(options_path):
os.remove(options_path)
if os.path.exists(enemizer_output_path):
os.remove(enemizer_output_path)
return ret
class Sprite(object):
default_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,
@ -275,7 +412,7 @@ class Sprite(object):
# split into palettes of 15 colors
return array_chunk(palette_as_colors, 15)
def patch_rom(world, player, rom, hashtable, beep='normal', color='red', sprite=None):
def patch_rom(world, player, rom, hashtable):
random.seed(world.rom_seeds[player])
# patch items
for location in world.get_locations():
@ -852,8 +989,6 @@ def patch_rom(world, player, rom, hashtable, beep='normal', color='red', sprite=
]
rom.write_bytes(0x180215, code)
apply_rom_settings(rom, beep, color, world.quickswap, world.fastmenu, world.disable_music, sprite)
return rom
def write_custom_shops(rom, world, player):