New Game: Zillion (#1081)
* Option RangeWithSpecialMax * amendment to typing in web options * compare string with number * lots of work on zillion * fix zillion fill logic * fix a few more issues in zillion fill logic * can make zillion patch and use it * put multi items in zillion rom * work on ZillionClient * logging and auth in client * work on sending and receiving items * implement item_handling flag * fix locations ids to NuktiServer package * use rewrite of zri * cache logic rule data for performance * use new id maps * fix some problems with the big recent merge * ZillionClient: use new context manager for Memory class * fix ItemClassification for Zillion items and some debug statements for asserts, documentation on running scripts for manual testing type correction in CommonContext * fix some issues in client, start on docs, put rescue and item ram addresses in slot data * use new location name system fix item locations getting out of sync in progression balancing * zillion client can read slot name from game * zillion: new item names * remove extra unneeded import * newer options (room gen and starting cards) * update comment in zillion patch * zillion non static regions * change some logging, update some comments * allow ZillionClient to exit in certain situations * todo note to fix options doc strings * don't force auto forfeit * rework validation of floppy requirement and item counts and fix race condition in generate_output * reorganize Zillion component structure with System class * documentation updates for Zillion * attempt inno_setup.iss * remove todo comment for something done * update comment * rework item count zillion options and some small cleanups * fix location check count * data package version 1 * Zillion can pass unit tests without rom * fix freeze if closing ZillionClient while it's waiting for server login * specify commit hash for zilliandomizer package * some changes to options validation * Zillion doors saved on multiworld server * add missing function in inno_setup and name of vanilla continues in options * rework zillion sync task and context * Apply documentation suggestions from SoldierofOrder Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com> * update zillion package * workaround for asyncio udp bug There is a bug in Python in Windows https://github.com/python/cpython/issues/91227 that makes it so if I look for RetroArch before it's ready, it breaks the asyncio udp transport system. As a workaround, we don't look for RetroArch until the user asks for it with /sms * a few of the smaller suggestions from review * logic only looks at my locations instead of all the multiworld locations * some adjustments from pull request discussion and some unit tests * patch webhost changes from pull request discussion * zillion logic tests * better vblr test * test interaction of character rescue items with logic * move unit tests to new worlds folder * comment improvements * fix minor logic issue and add memory read timeout * capitalization in option display names Opa-Opa is a proper noun * redirect zz stdout to debug * fix option validation bug making unbeatable seeds * remove line that does nothing * attach logic cache to world Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com> Co-authored-by: Doug Hoskisson <doughoskisson@novuslabs.com>
This commit is contained in:
parent
ed76c13961
commit
265ee7098a
|
@ -13,6 +13,7 @@
|
|||
*.z64
|
||||
*.n64
|
||||
*.nes
|
||||
*.sms
|
||||
*.gb
|
||||
*.gbc
|
||||
*.gba
|
||||
|
|
|
@ -151,6 +151,11 @@ class CommonContext:
|
|||
hint_cost: typing.Optional[int]
|
||||
player_names: typing.Dict[int, str]
|
||||
|
||||
finished_game: bool
|
||||
ready: bool
|
||||
auth: typing.Optional[str]
|
||||
seed_name: typing.Optional[str]
|
||||
|
||||
# locations
|
||||
locations_checked: typing.Set[int] # local state
|
||||
locations_scouted: typing.Set[int]
|
||||
|
@ -288,7 +293,7 @@ class CommonContext:
|
|||
self.input_requests += 1
|
||||
return await self.input_queue.get()
|
||||
|
||||
async def connect(self, address=None):
|
||||
async def connect(self, address: typing.Optional[str] = None) -> None:
|
||||
await self.disconnect()
|
||||
self.server_task = asyncio.create_task(server_loop(self, address), name="server loop")
|
||||
|
||||
|
|
|
@ -151,6 +151,9 @@ components: Iterable[Component] = (
|
|||
Component('ChecksFinder Client', 'ChecksFinderClient'),
|
||||
# Starcraft 2
|
||||
Component('Starcraft 2 Client', 'Starcraft2Client'),
|
||||
# Zillion
|
||||
Component('Zillion Client', 'ZillionClient',
|
||||
file_identifier=SuffixIdentifier('.apzl')),
|
||||
# Functions
|
||||
Component('Open host.yaml', func=open_host_yaml),
|
||||
Component('Open Patch', func=open_patch),
|
||||
|
|
|
@ -44,6 +44,13 @@ def update(yes=False, force=False):
|
|||
wheel = line.split('/')[-1]
|
||||
name, version, _ = wheel.split('-', 2)
|
||||
line = f'{name}=={version}'
|
||||
elif line.startswith('git+https://'):
|
||||
# extract name and version
|
||||
end = line.split('/')[-1]
|
||||
name_hash, egg = end.split("#", 1)
|
||||
name, _ = name_hash.split("@", 1)
|
||||
version = egg.split('==')[-1]
|
||||
line = f'{name}=={version}'
|
||||
requirements = pkg_resources.parse_requirements(line)
|
||||
for requirement in requirements:
|
||||
requirement = str(requirement)
|
||||
|
|
|
@ -32,6 +32,7 @@ Currently, the following games are supported:
|
|||
* Pokémon Red and Blue
|
||||
* Hylics 2
|
||||
* Overcooked! 2
|
||||
* Zillion
|
||||
|
||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
|
||||
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||
|
|
6
Utils.py
6
Utils.py
|
@ -295,6 +295,12 @@ def get_default_options() -> OptionsType:
|
|||
"sni": "SNI",
|
||||
"rom_start": True,
|
||||
},
|
||||
"zillion_options": {
|
||||
"rom_file": "Zillion (UE) [!].sms",
|
||||
# RetroArch doesn't make it easy to launch a game from the command line.
|
||||
# You have to know the path to the emulator core library on the user's computer.
|
||||
"rom_start": "retroarch",
|
||||
},
|
||||
"pokemon_rb_options": {
|
||||
"red_rom_file": "Pokemon Red (UE) [S][!].gb",
|
||||
"blue_rom_file": "Pokemon Blue (UE) [S][!].gb",
|
||||
|
|
|
@ -75,6 +75,8 @@ def download_slot_file(room_id, player_id: int):
|
|||
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apz5"
|
||||
elif slot_data.game == "VVVVVV":
|
||||
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_SP.apv6"
|
||||
elif slot_data.game == "Zillion":
|
||||
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_SP.apzl"
|
||||
elif slot_data.game == "Super Mario 64":
|
||||
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_SP.apsm64ex"
|
||||
elif slot_data.game == "Dark Souls III":
|
||||
|
|
|
@ -0,0 +1,358 @@
|
|||
import asyncio
|
||||
import base64
|
||||
import platform
|
||||
from typing import Any, Coroutine, Dict, Optional, Type, cast
|
||||
|
||||
# CommonClient import first to trigger ModuleUpdater
|
||||
from CommonClient import CommonContext, server_loop, gui_enabled, \
|
||||
ClientCommandProcessor, logger, get_base_parser
|
||||
from NetUtils import ClientStatus
|
||||
import Utils
|
||||
|
||||
import colorama # type: ignore
|
||||
|
||||
from zilliandomizer.zri.memory import Memory
|
||||
from zilliandomizer.zri import events
|
||||
from zilliandomizer.utils.loc_name_maps import id_to_loc
|
||||
from zilliandomizer.options import Chars
|
||||
from zilliandomizer.patch import RescueInfo
|
||||
|
||||
from worlds.zillion.id_maps import make_id_to_others
|
||||
from worlds.zillion.config import base_id
|
||||
|
||||
|
||||
class ZillionCommandProcessor(ClientCommandProcessor):
|
||||
ctx: "ZillionContext"
|
||||
|
||||
def _cmd_sms(self) -> None:
|
||||
""" Tell the client that Zillion is running in RetroArch. """
|
||||
logger.info("ready to look for game")
|
||||
self.ctx.look_for_retroarch.set()
|
||||
|
||||
|
||||
class ZillionContext(CommonContext):
|
||||
game = "Zillion"
|
||||
command_processor: Type[ClientCommandProcessor] = ZillionCommandProcessor
|
||||
items_handling = 1 # receive items from other players
|
||||
|
||||
from_game: "asyncio.Queue[events.EventFromGame]"
|
||||
to_game: "asyncio.Queue[events.EventToGame]"
|
||||
ap_local_count: int
|
||||
""" local checks watched by server """
|
||||
next_item: int
|
||||
""" index in `items_received` """
|
||||
ap_id_to_name: Dict[int, str]
|
||||
ap_id_to_zz_id: Dict[int, int]
|
||||
start_char: Chars = "JJ"
|
||||
rescues: Dict[int, RescueInfo] = {}
|
||||
loc_mem_to_id: Dict[int, int] = {}
|
||||
got_slot_data: asyncio.Event
|
||||
""" serves as a flag for whether I am logged in to the server """
|
||||
|
||||
look_for_retroarch: asyncio.Event
|
||||
"""
|
||||
There is a bug in Python in Windows
|
||||
https://github.com/python/cpython/issues/91227
|
||||
that makes it so if I look for RetroArch before it's ready,
|
||||
it breaks the asyncio udp transport system.
|
||||
|
||||
As a workaround, we don't look for RetroArch until this event is set.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
server_address: str,
|
||||
password: str) -> None:
|
||||
super().__init__(server_address, password)
|
||||
self.from_game = asyncio.Queue()
|
||||
self.to_game = asyncio.Queue()
|
||||
self.got_slot_data = asyncio.Event()
|
||||
|
||||
self.look_for_retroarch = asyncio.Event()
|
||||
if platform.system() != "Windows":
|
||||
# asyncio udp bug is only on Windows
|
||||
self.look_for_retroarch.set()
|
||||
|
||||
self.reset_game_state()
|
||||
|
||||
def reset_game_state(self) -> None:
|
||||
for _ in range(self.from_game.qsize()):
|
||||
self.from_game.get_nowait()
|
||||
for _ in range(self.to_game.qsize()):
|
||||
self.to_game.get_nowait()
|
||||
self.got_slot_data.clear()
|
||||
|
||||
self.ap_local_count = 0
|
||||
self.next_item = 0
|
||||
self.ap_id_to_name = {}
|
||||
self.ap_id_to_zz_id = {}
|
||||
self.rescues = {}
|
||||
self.loc_mem_to_id = {}
|
||||
|
||||
self.locations_checked.clear()
|
||||
self.missing_locations.clear()
|
||||
self.checked_locations.clear()
|
||||
self.finished_game = False
|
||||
self.items_received.clear()
|
||||
|
||||
# override
|
||||
def on_deathlink(self, data: Dict[str, Any]) -> None:
|
||||
self.to_game.put_nowait(events.DeathEventToGame())
|
||||
return super().on_deathlink(data)
|
||||
|
||||
# override
|
||||
async def server_auth(self, password_requested: bool = False) -> None:
|
||||
if password_requested and not self.password:
|
||||
await super().server_auth(password_requested)
|
||||
if not self.auth:
|
||||
logger.info('waiting for connection to game...')
|
||||
return
|
||||
logger.info("logging in to server...")
|
||||
await self.send_connect()
|
||||
|
||||
# override
|
||||
def run_gui(self) -> None:
|
||||
from kvui import GameManager
|
||||
|
||||
class ZillionManager(GameManager):
|
||||
logging_pairs = [
|
||||
("Client", "Archipelago")
|
||||
]
|
||||
base_title = "Archipelago Zillion Client"
|
||||
|
||||
self.ui = ZillionManager(self)
|
||||
run_co: Coroutine[Any, Any, None] = self.ui.async_run() # type: ignore
|
||||
# kivy types missing
|
||||
self.ui_task = asyncio.create_task(run_co, name="UI")
|
||||
|
||||
def on_package(self, cmd: str, args: Dict[str, Any]) -> None:
|
||||
if cmd == "Connected":
|
||||
logger.info("logged in to Archipelago server")
|
||||
if "slot_data" not in args:
|
||||
logger.warn("`Connected` packet missing `slot_data`")
|
||||
return
|
||||
slot_data = args["slot_data"]
|
||||
|
||||
if "start_char" not in slot_data:
|
||||
logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `start_char`")
|
||||
return
|
||||
self.start_char = slot_data['start_char']
|
||||
if self.start_char not in {"Apple", "Champ", "JJ"}:
|
||||
logger.warn("invalid Zillion `Connected` packet, "
|
||||
f"`slot_data` `start_char` has invalid value: {self.start_char}")
|
||||
|
||||
if "rescues" not in slot_data:
|
||||
logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `rescues`")
|
||||
return
|
||||
rescues = slot_data["rescues"]
|
||||
self.rescues = {}
|
||||
for rescue_id, json_info in rescues.items():
|
||||
assert rescue_id in ("0", "1"), f"invalid rescue_id in Zillion slot_data: {rescue_id}"
|
||||
# TODO: just take start_char out of the RescueInfo so there's no opportunity for a mismatch?
|
||||
assert json_info["start_char"] == self.start_char, \
|
||||
f'mismatch in Zillion slot data: {json_info["start_char"]} {self.start_char}'
|
||||
ri = RescueInfo(json_info["start_char"],
|
||||
json_info["room_code"],
|
||||
json_info["mask"])
|
||||
self.rescues[0 if rescue_id == "0" else 1] = ri
|
||||
|
||||
if "loc_mem_to_id" not in slot_data:
|
||||
logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `loc_mem_to_id`")
|
||||
return
|
||||
loc_mem_to_id = slot_data["loc_mem_to_id"]
|
||||
self.loc_mem_to_id = {}
|
||||
for mem_str, id_str in loc_mem_to_id.items():
|
||||
mem = int(mem_str)
|
||||
id_ = int(id_str)
|
||||
room_i = mem // 256
|
||||
assert 0 <= room_i < 74
|
||||
assert id_ in id_to_loc
|
||||
self.loc_mem_to_id[mem] = id_
|
||||
|
||||
self.got_slot_data.set()
|
||||
|
||||
payload = {
|
||||
"cmd": "Get",
|
||||
"keys": [f"zillion-{self.auth}-doors"]
|
||||
}
|
||||
asyncio.create_task(self.send_msgs([payload]))
|
||||
elif cmd == "Retrieved":
|
||||
if "keys" not in args:
|
||||
logger.warning(f"invalid Retrieved packet to ZillionClient: {args}")
|
||||
return
|
||||
keys = cast(Dict[str, Optional[str]], args["keys"])
|
||||
doors_b64 = keys[f"zillion-{self.auth}-doors"]
|
||||
if doors_b64:
|
||||
logger.info("received door data from server")
|
||||
doors = base64.b64decode(doors_b64)
|
||||
self.to_game.put_nowait(events.DoorEventToGame(doors))
|
||||
|
||||
def process_from_game_queue(self) -> None:
|
||||
if self.from_game.qsize():
|
||||
event_from_game = self.from_game.get_nowait()
|
||||
if isinstance(event_from_game, events.AcquireLocationEventFromGame):
|
||||
server_id = event_from_game.id + base_id
|
||||
loc_name = id_to_loc[event_from_game.id]
|
||||
self.locations_checked.add(server_id)
|
||||
if server_id in self.missing_locations:
|
||||
self.ap_local_count += 1
|
||||
n_locations = len(self.missing_locations) + len(self.checked_locations) - 1 # -1 to ignore win
|
||||
logger.info(f'New Check: {loc_name} ({self.ap_local_count}/{n_locations})')
|
||||
asyncio.create_task(self.send_msgs([
|
||||
{"cmd": 'LocationChecks', "locations": [server_id]}
|
||||
]))
|
||||
else:
|
||||
# This will happen a lot in Zillion,
|
||||
# because all the key words are local and unwatched by the server.
|
||||
logger.debug(f"DEBUG: {loc_name} not in missing")
|
||||
elif isinstance(event_from_game, events.DeathEventFromGame):
|
||||
asyncio.create_task(self.send_death())
|
||||
elif isinstance(event_from_game, events.WinEventFromGame):
|
||||
if not self.finished_game:
|
||||
asyncio.create_task(self.send_msgs([
|
||||
{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}
|
||||
]))
|
||||
self.finished_game = True
|
||||
elif isinstance(event_from_game, events.DoorEventFromGame):
|
||||
if self.auth:
|
||||
doors_b64 = base64.b64encode(event_from_game.doors).decode()
|
||||
payload = {
|
||||
"cmd": "Set",
|
||||
"key": f"zillion-{self.auth}-doors",
|
||||
"operations": [{"operation": "replace", "value": doors_b64}]
|
||||
}
|
||||
asyncio.create_task(self.send_msgs([payload]))
|
||||
else:
|
||||
logger.warning(f"WARNING: unhandled event from game {event_from_game}")
|
||||
|
||||
def process_items_received(self) -> None:
|
||||
if len(self.items_received) > self.next_item:
|
||||
zz_item_ids = [self.ap_id_to_zz_id[item.item] for item in self.items_received]
|
||||
for index in range(self.next_item, len(self.items_received)):
|
||||
ap_id = self.items_received[index].item
|
||||
from_name = self.player_names[self.items_received[index].player]
|
||||
# TODO: colors in this text, like sni client?
|
||||
logger.info(f'received {self.ap_id_to_name[ap_id]} from {from_name}')
|
||||
self.to_game.put_nowait(
|
||||
events.ItemEventToGame(zz_item_ids)
|
||||
)
|
||||
self.next_item = len(self.items_received)
|
||||
|
||||
|
||||
async def zillion_sync_task(ctx: ZillionContext) -> None:
|
||||
logger.info("started zillion sync task")
|
||||
|
||||
# to work around the Python bug where we can't check for RetroArch
|
||||
if not ctx.look_for_retroarch.is_set():
|
||||
logger.info("Start Zillion in RetroArch, then use the /sms command to connect to it.")
|
||||
await asyncio.wait((
|
||||
asyncio.create_task(ctx.look_for_retroarch.wait()),
|
||||
asyncio.create_task(ctx.exit_event.wait())
|
||||
), return_when=asyncio.FIRST_COMPLETED)
|
||||
|
||||
last_log = ""
|
||||
|
||||
def log_no_spam(msg: str) -> None:
|
||||
nonlocal last_log
|
||||
if msg != last_log:
|
||||
last_log = msg
|
||||
logger.info(msg)
|
||||
|
||||
# to only show this message once per client run
|
||||
help_message_shown = False
|
||||
|
||||
with Memory(ctx.from_game, ctx.to_game) as memory:
|
||||
while not ctx.exit_event.is_set():
|
||||
ram = await memory.read()
|
||||
name = memory.get_player_name(ram).decode()
|
||||
if len(name):
|
||||
if name == ctx.auth:
|
||||
# this is the name we know
|
||||
if ctx.server and ctx.server.socket: # type: ignore
|
||||
if memory.have_generation_info():
|
||||
log_no_spam("everything connected")
|
||||
await memory.process_ram(ram)
|
||||
ctx.process_from_game_queue()
|
||||
ctx.process_items_received()
|
||||
else: # no generation info
|
||||
if ctx.got_slot_data.is_set():
|
||||
memory.set_generation_info(ctx.rescues, ctx.loc_mem_to_id)
|
||||
ctx.ap_id_to_name, ctx.ap_id_to_zz_id, _ap_id_to_zz_item = \
|
||||
make_id_to_others(ctx.start_char)
|
||||
ctx.next_item = 0
|
||||
ctx.ap_local_count = len(ctx.checked_locations)
|
||||
else: # no slot data yet
|
||||
asyncio.create_task(ctx.send_connect())
|
||||
log_no_spam("logging in to server...")
|
||||
await asyncio.wait((
|
||||
ctx.got_slot_data.wait(),
|
||||
ctx.exit_event.wait(),
|
||||
asyncio.sleep(6)
|
||||
), return_when=asyncio.FIRST_COMPLETED) # to not spam connect packets
|
||||
else: # server not connected
|
||||
log_no_spam("waiting for server connection...")
|
||||
else: # new game
|
||||
log_no_spam("connected to new game")
|
||||
await ctx.disconnect()
|
||||
ctx.reset_server_state()
|
||||
ctx.reset_game_state()
|
||||
memory.reset_game_state()
|
||||
|
||||
ctx.auth = name
|
||||
asyncio.create_task(ctx.connect())
|
||||
await asyncio.wait((
|
||||
ctx.got_slot_data.wait(),
|
||||
ctx.exit_event.wait(),
|
||||
asyncio.sleep(6)
|
||||
), return_when=asyncio.FIRST_COMPLETED) # to not spam connect packets
|
||||
else: # no name found in game
|
||||
if not help_message_shown:
|
||||
logger.info('In RetroArch, make sure "Settings > Network > Network Commands" is on.')
|
||||
help_message_shown = True
|
||||
log_no_spam("looking for connection to game...")
|
||||
await asyncio.sleep(0.3)
|
||||
|
||||
await asyncio.sleep(0.09375)
|
||||
logger.info("zillion sync task ending")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
parser = get_base_parser()
|
||||
parser.add_argument('diff_file', default="", type=str, nargs="?",
|
||||
help='Path to a .apzl Archipelago Binary Patch file')
|
||||
# SNI parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
|
||||
args = parser.parse_args()
|
||||
print(args)
|
||||
|
||||
if args.diff_file:
|
||||
import Patch
|
||||
logger.info("patch file was supplied - creating sms rom...")
|
||||
meta, rom_file = Patch.create_rom_file(args.diff_file)
|
||||
if "server" in meta:
|
||||
args.connect = meta["server"]
|
||||
logger.info(f"wrote rom file to {rom_file}")
|
||||
|
||||
ctx = ZillionContext(args.connect, args.password)
|
||||
if ctx.server_task is None:
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||
|
||||
if gui_enabled:
|
||||
ctx.run_gui()
|
||||
ctx.run_cli()
|
||||
|
||||
sync_task = asyncio.create_task(zillion_sync_task(ctx))
|
||||
|
||||
await ctx.exit_event.wait()
|
||||
|
||||
ctx.server_address = None
|
||||
logger.debug("waiting for sync task to end")
|
||||
await sync_task
|
||||
logger.debug("sync task ended")
|
||||
await ctx.shutdown()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
Utils.init_logging("ZillionClient", exception_logger="Client")
|
||||
|
||||
colorama.init()
|
||||
asyncio.run(main())
|
||||
colorama.deinit()
|
|
@ -155,3 +155,12 @@ smw_options:
|
|||
# True for operating system default program
|
||||
# Alternatively, a path to a program to open the .sfc file with
|
||||
rom_start: true
|
||||
zillion_options:
|
||||
# File name of the Zillion US rom
|
||||
rom_file: "Zillion (UE) [!].sms"
|
||||
# Set this to false to never autostart a rom (such as after patching)
|
||||
# True for operating system default program
|
||||
# Alternatively, a path to a program to open the .sfc file with
|
||||
# RetroArch doesn't make it easy to launch a game from the command line.
|
||||
# You have to know the path to the emulator core library on the user's computer.
|
||||
rom_start: "retroarch"
|
||||
|
|
|
@ -59,6 +59,7 @@ Name: "generator/smw"; Description: "Super Mario World ROM Setup"; Types: ful
|
|||
Name: "generator/soe"; Description: "Secret of Evermore ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning
|
||||
Name: "generator/lttp"; Description: "A Link to the Past ROM Setup and Enemizer"; Types: full hosting; ExtraDiskSpaceRequired: 5191680
|
||||
Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 100663296; Flags: disablenouninstallwarning
|
||||
Name: "generator/zl"; Description: "Zillion ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 150000; Flags: disablenouninstallwarning
|
||||
Name: "generator/pkmn_r"; Description: "Pokemon Red ROM Setup"; Types: full hosting
|
||||
Name: "generator/pkmn_b"; Description: "Pokemon Blue ROM Setup"; Types: full hosting
|
||||
Name: "server"; Description: "Server"; Types: full hosting
|
||||
|
@ -77,6 +78,7 @@ Name: "client/pkmn/red"; Description: "Pokemon Client - Pokemon Red Setup"; Typ
|
|||
Name: "client/pkmn/blue"; Description: "Pokemon Client - Pokemon Blue Setup"; Types: full playing; ExtraDiskSpaceRequired: 1048576
|
||||
Name: "client/cf"; Description: "ChecksFinder"; Types: full playing
|
||||
Name: "client/sc2"; Description: "Starcraft 2"; Types: full playing
|
||||
Name: "client/zl"; Description: "Zillion"; Types: full playing
|
||||
Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing
|
||||
|
||||
[Dirs]
|
||||
|
@ -89,6 +91,7 @@ Source: "{code:GetDKC3ROMPath}"; DestDir: "{app}"; DestName: "Donkey Kong Countr
|
|||
Source: "{code:GetSMWROMPath}"; DestDir: "{app}"; DestName: "Super Mario World (USA).sfc"; Flags: external; Components: client/sni/smw or generator/smw
|
||||
Source: "{code:GetSoEROMPath}"; DestDir: "{app}"; DestName: "Secret of Evermore (USA).sfc"; Flags: external; Components: generator/soe
|
||||
Source: "{code:GetOoTROMPath}"; DestDir: "{app}"; DestName: "The Legend of Zelda - Ocarina of Time.z64"; Flags: external; Components: client/oot or generator/oot
|
||||
Source: "{code:GetZlROMPath}"; DestDir: "{app}"; DestName: "Zillion (UE) [!].sms"; Flags: external; Components: client/zl or generator/zl
|
||||
Source: "{code:GetRedROMPath}"; DestDir: "{app}"; DestName: "Pokemon Red (UE) [S][!].gb"; Flags: external; Components: client/pkmn/red or generator/pkmn_r
|
||||
Source: "{code:GetBlueROMPath}"; DestDir: "{app}"; DestName: "Pokemon Blue (UE) [S][!].gb"; Flags: external; Components: client/pkmn/blue or generator/pkmn_b
|
||||
Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, Archipelago*.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
|
@ -104,6 +107,7 @@ Source: "{#source_path}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: i
|
|||
Source: "{#source_path}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft
|
||||
Source: "{#source_path}\ArchipelagoOoTClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot
|
||||
Source: "{#source_path}\ArchipelagoOoTAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot
|
||||
Source: "{#source_path}\ArchipelagoZillionClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/zl
|
||||
Source: "{#source_path}\ArchipelagoFF1Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ff1
|
||||
Source: "{#source_path}\ArchipelagoPokemonClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/pkmn
|
||||
Source: "{#source_path}\ArchipelagoChecksFinderClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/cf
|
||||
|
@ -118,6 +122,7 @@ Name: "{group}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.e
|
|||
Name: "{group}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Components: client/factorio
|
||||
Name: "{group}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Components: client/minecraft
|
||||
Name: "{group}\{#MyAppName} Ocarina of Time Client"; Filename: "{app}\ArchipelagoOoTClient.exe"; Components: client/oot
|
||||
Name: "{group}\{#MyAppName} Zillion Client"; Filename: "{app}\ArchipelagoZillionClient.exe"; Components: client/zl
|
||||
Name: "{group}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\ArchipelagoFF1Client.exe"; Components: client/ff1
|
||||
Name: "{group}\{#MyAppName} Pokemon Client"; Filename: "{app}\ArchipelagoPokemonClient.exe"; Components: client/pkmn
|
||||
Name: "{group}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Components: client/cf
|
||||
|
@ -129,6 +134,7 @@ Name: "{commondesktop}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNI
|
|||
Name: "{commondesktop}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Tasks: desktopicon; Components: client/factorio
|
||||
Name: "{commondesktop}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Tasks: desktopicon; Components: client/minecraft
|
||||
Name: "{commondesktop}\{#MyAppName} Ocarina of Time Client"; Filename: "{app}\ArchipelagoOoTClient.exe"; Tasks: desktopicon; Components: client/oot
|
||||
Name: "{commondesktop}\{#MyAppName} Zillion Client"; Filename: "{app}\ArchipelagoZillionClient.exe"; Tasks: desktopicon; Components: client/zl
|
||||
Name: "{commondesktop}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\ArchipelagoFF1Client.exe"; Tasks: desktopicon; Components: client/ff1
|
||||
Name: "{commondesktop}\{#MyAppName} Pokemon Client"; Filename: "{app}\ArchipelagoPokemonClient.exe"; Tasks: desktopicon; Components: client/pkmn
|
||||
Name: "{commondesktop}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Tasks: desktopicon; Components: client/cf
|
||||
|
@ -169,6 +175,11 @@ Root: HKCR; Subkey: "{#MyAppName}smwpatch"; ValueData: "Arch
|
|||
Root: HKCR; Subkey: "{#MyAppName}smwpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
|
||||
Root: HKCR; Subkey: "{#MyAppName}smwpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
|
||||
|
||||
Root: HKCR; Subkey: ".apzl"; ValueData: "{#MyAppName}zlpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/zl
|
||||
Root: HKCR; Subkey: "{#MyAppName}zlpatch"; ValueData: "Archipelago Zillion Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/zl
|
||||
Root: HKCR; Subkey: "{#MyAppName}zlpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoZillionClient.exe,0"; ValueType: string; ValueName: ""; Components: client/zl
|
||||
Root: HKCR; Subkey: "{#MyAppName}zlpatch\shell\open\command"; ValueData: """{app}\ArchipelagoZillionClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/zl
|
||||
|
||||
Root: HKCR; Subkey: ".apsmz3"; ValueData: "{#MyAppName}smz3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
|
||||
Root: HKCR; Subkey: "{#MyAppName}smz3patch"; ValueData: "Archipelago SMZ3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
|
||||
Root: HKCR; Subkey: "{#MyAppName}smz3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
|
||||
|
@ -254,6 +265,9 @@ var SoERomFilePage: TInputFileWizardPage;
|
|||
var ootrom: string;
|
||||
var OoTROMFilePage: TInputFileWizardPage;
|
||||
|
||||
var zlrom: string;
|
||||
var ZlROMFilePage: TInputFileWizardPage;
|
||||
|
||||
var redrom: string;
|
||||
var RedROMFilePage: TInputFileWizardPage;
|
||||
|
||||
|
@ -273,6 +287,15 @@ begin
|
|||
end;
|
||||
end;
|
||||
|
||||
function GetSMSMD5OfFile(const rom: string): string;
|
||||
var data: AnsiString;
|
||||
begin
|
||||
if LoadStringFromFile(rom, data) then
|
||||
begin
|
||||
Result := GetMD5OfString(data);
|
||||
end;
|
||||
end;
|
||||
|
||||
function CheckRom(name: string; hash: string): string;
|
||||
var rom: string;
|
||||
begin
|
||||
|
@ -292,6 +315,25 @@ begin
|
|||
end;
|
||||
end;
|
||||
|
||||
function CheckSMSRom(name: string; hash: string): string;
|
||||
var rom: string;
|
||||
begin
|
||||
log('Handling ' + name)
|
||||
rom := FileSearch(name, WizardDirValue());
|
||||
if Length(rom) > 0 then
|
||||
begin
|
||||
log('existing ROM found');
|
||||
log(IntToStr(CompareStr(GetSMSMD5OfFile(rom), hash)));
|
||||
if CompareStr(GetSMSMD5OfFile(rom), hash) = 0 then
|
||||
begin
|
||||
log('existing ROM verified');
|
||||
Result := rom;
|
||||
exit;
|
||||
end;
|
||||
log('existing ROM failed verification');
|
||||
end;
|
||||
end;
|
||||
|
||||
function AddRomPage(name: string): TInputFileWizardPage;
|
||||
begin
|
||||
Result :=
|
||||
|
@ -307,6 +349,7 @@ begin
|
|||
'.sfc');
|
||||
end;
|
||||
|
||||
|
||||
function AddGBRomPage(name: string): TInputFileWizardPage;
|
||||
begin
|
||||
Result :=
|
||||
|
@ -322,6 +365,21 @@ begin
|
|||
'.gb');
|
||||
end;
|
||||
|
||||
function AddSMSRomPage(name: string): TInputFileWizardPage;
|
||||
begin
|
||||
Result :=
|
||||
CreateInputFilePage(
|
||||
wpSelectComponents,
|
||||
'Select ROM File',
|
||||
'Where is your ' + name + ' located?',
|
||||
'Select the file, then click Next.');
|
||||
|
||||
Result.Add(
|
||||
'Location of ROM file:',
|
||||
'SMS ROM files|*.sms|All files|*.*',
|
||||
'.sms');
|
||||
end;
|
||||
|
||||
procedure AddOoTRomPage();
|
||||
begin
|
||||
ootrom := FileSearch('The Legend of Zelda - Ocarina of Time.z64', WizardDirValue());
|
||||
|
@ -366,6 +424,8 @@ begin
|
|||
Result := not (SoEROMFilePage.Values[0] = '')
|
||||
else if (assigned(OoTROMFilePage)) and (CurPageID = OoTROMFilePage.ID) then
|
||||
Result := not (OoTROMFilePage.Values[0] = '')
|
||||
else if (assigned(ZlROMFilePage)) and (CurPageID = ZlROMFilePage.ID) then
|
||||
Result := not (ZlROMFilePage.Values[0] = '')
|
||||
else
|
||||
Result := True;
|
||||
end;
|
||||
|
@ -466,6 +526,22 @@ begin
|
|||
Result := '';
|
||||
end;
|
||||
|
||||
function GetZlROMPath(Param: string): string;
|
||||
begin
|
||||
if Length(zlrom) > 0 then
|
||||
Result := zlrom
|
||||
else if Assigned(ZlROMFilePage) then
|
||||
begin
|
||||
R := CompareStr(GetMD5OfFile(ZlROMFilePage.Values[0]), 'd4bf9e7bcf9a48da53785d2ae7bc4270');
|
||||
if R <> 0 then
|
||||
MsgBox('Zillion ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||
|
||||
Result := ZlROMFilePage.Values[0]
|
||||
end
|
||||
else
|
||||
Result := '';
|
||||
end;
|
||||
|
||||
function GetRedROMPath(Param: string): string;
|
||||
begin
|
||||
if Length(redrom) > 0 then
|
||||
|
@ -522,6 +598,10 @@ begin
|
|||
if Length(soerom) = 0 then
|
||||
SoEROMFilePage:= AddRomPage('Secret of Evermore (USA).sfc');
|
||||
|
||||
zlrom := CheckSMSRom('Zillion (UE) [!].sms', 'd4bf9e7bcf9a48da53785d2ae7bc4270');
|
||||
if Length(zlrom) = 0 then
|
||||
ZlROMFilePage:= AddSMSRomPage('Zillion (UE) [!].sms');
|
||||
|
||||
redrom := CheckRom('Pokemon Red (UE) [S][!].gb','3d45c1ee9abd5738df46d2bdda8b57dc');
|
||||
if Length(redrom) = 0 then
|
||||
RedROMFilePage:= AddGBRomPage('Pokemon Red (UE) [S][!].gb');
|
||||
|
@ -547,6 +627,8 @@ begin
|
|||
Result := not (WizardIsComponentSelected('generator/soe'));
|
||||
if (assigned(OoTROMFilePage)) and (PageID = OoTROMFilePage.ID) then
|
||||
Result := not (WizardIsComponentSelected('generator/oot') or WizardIsComponentSelected('client/oot'));
|
||||
if (assigned(ZlROMFilePage)) and (PageID = ZlROMFilePage.ID) then
|
||||
Result := not (WizardIsComponentSelected('generator/zl') or WizardIsComponentSelected('client/zl'));
|
||||
if (assigned(RedROMFilePage)) and (PageID = RedROMFilePage.ID) then
|
||||
Result := not (WizardIsComponentSelected('generator/pkmn_r') or WizardIsComponentSelected('client/pkmn/red'));
|
||||
if (assigned(BlueROMFilePage)) and (PageID = BlueROMFilePage.ID) then
|
||||
|
|
|
@ -0,0 +1,144 @@
|
|||
from . import ZillionTestBase
|
||||
|
||||
|
||||
class TestGoalVanilla(ZillionTestBase):
|
||||
options = {
|
||||
"start_char": "JJ",
|
||||
"jump_levels": "vanilla",
|
||||
"gun_levels": "vanilla",
|
||||
"floppy_disk_count": 7,
|
||||
"floppy_req": 6,
|
||||
}
|
||||
|
||||
def test_floppies(self):
|
||||
self.collect_by_name(["Apple", "Champ", "Red ID Card"])
|
||||
self.assertBeatable(False) # 0 floppies
|
||||
floppies = self.get_items_by_name("Floppy Disk")
|
||||
win = self.get_item_by_name("Win")
|
||||
self.collect(floppies[:-2]) # 1 too few
|
||||
self.assertEqual(self.count("Floppy Disk"), 5)
|
||||
self.assertBeatable(False)
|
||||
self.collect(floppies[-2:-1]) # exact
|
||||
self.assertEqual(self.count("Floppy Disk"), 6)
|
||||
self.assertBeatable(True)
|
||||
self.remove([win]) # reset
|
||||
self.collect(floppies[-1:]) # 1 extra
|
||||
self.assertEqual(self.count("Floppy Disk"), 7)
|
||||
self.assertBeatable(True)
|
||||
|
||||
def test_with_everything(self):
|
||||
self.collect_by_name(["Apple", "Champ", "Red ID Card", "Floppy Disk"])
|
||||
self.assertBeatable(True)
|
||||
|
||||
def test_no_jump(self):
|
||||
self.collect_by_name(["Champ", "Red ID Card", "Floppy Disk"])
|
||||
self.assertBeatable(False)
|
||||
|
||||
def test_no_gun(self):
|
||||
self.collect_by_name(["Apple", "Red ID Card", "Floppy Disk"])
|
||||
self.assertBeatable(False)
|
||||
|
||||
def test_no_red(self):
|
||||
self.collect_by_name(["Apple", "Champ", "Floppy Disk"])
|
||||
self.assertBeatable(False)
|
||||
|
||||
|
||||
class TestGoalBalanced(ZillionTestBase):
|
||||
options = {
|
||||
"start_char": "JJ",
|
||||
"jump_levels": "balanced",
|
||||
"gun_levels": "balanced",
|
||||
}
|
||||
|
||||
def test_jump(self):
|
||||
self.collect_by_name(["Red ID Card", "Floppy Disk", "Zillion"])
|
||||
self.assertBeatable(False) # not enough jump
|
||||
opas = self.get_items_by_name("Opa-Opa")
|
||||
self.collect(opas[:1]) # too few
|
||||
self.assertEqual(self.count("Opa-Opa"), 1)
|
||||
self.assertBeatable(False)
|
||||
self.collect(opas[1:])
|
||||
self.assertBeatable(True)
|
||||
|
||||
def test_guns(self):
|
||||
self.collect_by_name(["Red ID Card", "Floppy Disk", "Opa-Opa"])
|
||||
self.assertBeatable(False) # not enough gun
|
||||
guns = self.get_items_by_name("Zillion")
|
||||
self.collect(guns[:1]) # too few
|
||||
self.assertEqual(self.count("Zillion"), 1)
|
||||
self.assertBeatable(False)
|
||||
self.collect(guns[1:])
|
||||
self.assertBeatable(True)
|
||||
|
||||
|
||||
class TestGoalRestrictive(ZillionTestBase):
|
||||
options = {
|
||||
"start_char": "JJ",
|
||||
"jump_levels": "restrictive",
|
||||
"gun_levels": "restrictive",
|
||||
}
|
||||
|
||||
def test_jump(self):
|
||||
self.collect_by_name(["Champ", "Red ID Card", "Floppy Disk", "Zillion"])
|
||||
self.assertBeatable(False) # not enough jump
|
||||
self.collect_by_name("Opa-Opa")
|
||||
self.assertBeatable(False) # with all opas, jj champ can't jump
|
||||
self.collect_by_name("Apple")
|
||||
self.assertBeatable(True)
|
||||
|
||||
def test_guns(self):
|
||||
self.collect_by_name(["Apple", "Red ID Card", "Floppy Disk", "Opa-Opa"])
|
||||
self.assertBeatable(False) # not enough gun
|
||||
self.collect_by_name("Zillion")
|
||||
self.assertBeatable(False) # with all guns, jj apple can't gun
|
||||
self.collect_by_name("Champ")
|
||||
self.assertBeatable(True)
|
||||
|
||||
|
||||
class TestGoalAppleStart(ZillionTestBase):
|
||||
""" creation of character rescue items has some special interactions with logic """
|
||||
options = {
|
||||
"start_char": "Apple",
|
||||
"jump_levels": "balanced",
|
||||
"gun_levels": "low",
|
||||
"zillion_count": 5
|
||||
}
|
||||
|
||||
def test_guns_jj_first(self):
|
||||
""" with low gun levels, 5 Zillion is enough to get JJ to gun 3 """
|
||||
self.collect_by_name(["JJ", "Red ID Card", "Floppy Disk", "Opa-Opa"])
|
||||
self.assertBeatable(False) # not enough gun
|
||||
self.collect_by_name("Zillion")
|
||||
self.assertBeatable(True)
|
||||
|
||||
def test_guns_zillions_first(self):
|
||||
""" with low gun levels, 5 Zillion is enough to get JJ to gun 3 """
|
||||
self.collect_by_name(["Zillion", "Red ID Card", "Floppy Disk", "Opa-Opa"])
|
||||
self.assertBeatable(False) # not enough gun
|
||||
self.collect_by_name("JJ")
|
||||
self.assertBeatable(True)
|
||||
|
||||
|
||||
class TestGoalChampStart(ZillionTestBase):
|
||||
""" creation of character rescue items has some special interactions with logic """
|
||||
options = {
|
||||
"start_char": "Champ",
|
||||
"jump_levels": "low",
|
||||
"gun_levels": "balanced",
|
||||
"opa_opa_count": 5,
|
||||
"opas_per_level": 1
|
||||
}
|
||||
|
||||
def test_jump_jj_first(self):
|
||||
""" with low jump levels, 5 level-ups is enough to get JJ to jump 3 """
|
||||
self.collect_by_name(["JJ", "Red ID Card", "Floppy Disk", "Zillion"])
|
||||
self.assertBeatable(False) # not enough jump
|
||||
self.collect_by_name("Opa-Opa")
|
||||
self.assertBeatable(True)
|
||||
|
||||
def test_jump_opa_first(self):
|
||||
""" with low jump levels, 5 level-ups is enough to get JJ to jump 3 """
|
||||
self.collect_by_name(["Opa-Opa", "Red ID Card", "Floppy Disk", "Zillion"])
|
||||
self.assertBeatable(False) # not enough jump
|
||||
self.collect_by_name("JJ")
|
||||
self.assertBeatable(True)
|
|
@ -0,0 +1,26 @@
|
|||
from test.worlds.zillion import ZillionTestBase
|
||||
|
||||
from worlds.zillion.options import ZillionJumpLevels, ZillionGunLevels, validate
|
||||
from zilliandomizer.options import VBLR_CHOICES
|
||||
|
||||
|
||||
class OptionsTest(ZillionTestBase):
|
||||
auto_construct = False
|
||||
|
||||
def test_validate_default(self) -> None:
|
||||
self.world_setup()
|
||||
validate(self.world, 1)
|
||||
|
||||
def test_vblr_ap_to_zz(self) -> None:
|
||||
""" all of the valid values for the AP options map to valid values for ZZ options """
|
||||
for option_name, vblr_class in (
|
||||
("jump_levels", ZillionJumpLevels),
|
||||
("gun_levels", ZillionGunLevels)
|
||||
):
|
||||
for value in vblr_class.name_lookup.values():
|
||||
self.options = {option_name: value}
|
||||
self.world_setup()
|
||||
zz_options, _item_counts = validate(self.world, 1)
|
||||
assert getattr(zz_options, option_name) in VBLR_CHOICES
|
||||
|
||||
# TODO: test validate with invalid combinations of options
|
|
@ -0,0 +1,5 @@
|
|||
from test.worlds.test_base import WorldTestBase
|
||||
|
||||
|
||||
class ZillionTestBase(WorldTestBase):
|
||||
game = "Zillion"
|
|
@ -0,0 +1,395 @@
|
|||
from collections import deque, Counter
|
||||
from contextlib import redirect_stdout
|
||||
import functools
|
||||
from typing import Any, Dict, List, Set, Tuple, Optional, cast
|
||||
import os
|
||||
import logging
|
||||
|
||||
from BaseClasses import ItemClassification, LocationProgressType, \
|
||||
MultiWorld, Item, CollectionState, RegionType, \
|
||||
Entrance, Tutorial
|
||||
from Options import AssembleOptions
|
||||
from .logic import cs_to_zz_locs
|
||||
from .region import ZillionLocation, ZillionRegion
|
||||
from .options import zillion_options, validate
|
||||
from .id_maps import item_name_to_id as _item_name_to_id, \
|
||||
loc_name_to_id as _loc_name_to_id, make_id_to_others, \
|
||||
zz_reg_name_to_reg_name, base_id
|
||||
from .item import ZillionItem
|
||||
from .patch import ZillionDeltaPatch, get_base_rom_path
|
||||
|
||||
from zilliandomizer.randomizer import Randomizer as ZzRandomizer
|
||||
from zilliandomizer.system import System
|
||||
from zilliandomizer.logic_components.items import RESCUE, items as zz_items, Item as ZzItem
|
||||
from zilliandomizer.logic_components.locations import Location as ZzLocation, Req
|
||||
from zilliandomizer.options import Chars
|
||||
|
||||
from ..AutoWorld import World, WebWorld
|
||||
|
||||
|
||||
class ZillionWebWorld(WebWorld):
|
||||
theme = "stone"
|
||||
tutorials = [Tutorial(
|
||||
"Multiworld Setup Guide",
|
||||
"A guide to playing Zillion randomizer.",
|
||||
"English",
|
||||
"setup_en.md",
|
||||
"setup/en",
|
||||
["beauxq"]
|
||||
)]
|
||||
|
||||
|
||||
class ZillionWorld(World):
|
||||
"""
|
||||
Zillion is a metroidvania style game released in 1987 for the 8-bit Sega Master System.
|
||||
|
||||
It's based on the anime Zillion (赤い光弾ジリオン, Akai Koudan Zillion).
|
||||
"""
|
||||
game = "Zillion"
|
||||
web = ZillionWebWorld()
|
||||
|
||||
option_definitions: Dict[str, AssembleOptions] = zillion_options
|
||||
topology_present: bool = True # indicate if world type has any meaningful layout/pathing
|
||||
|
||||
# map names to their IDs
|
||||
item_name_to_id: Dict[str, int] = _item_name_to_id
|
||||
location_name_to_id: Dict[str, int] = _loc_name_to_id
|
||||
|
||||
# increment this every time something in your world's names/id mappings changes.
|
||||
# While this is set to 0 in *any* AutoWorld, the entire DataPackage is considered in testing mode and will be
|
||||
# retrieved by clients on every connection.
|
||||
data_version: int = 1
|
||||
|
||||
# NOTE: remote_items and remote_start_inventory are now available in the network protocol for the client to set.
|
||||
# These values will be removed.
|
||||
# if a world is set to remote_items, then it just needs to send location checks to the server and the server
|
||||
# sends back the items
|
||||
# if a world is set to remote_items = False, then the server never sends an item where receiver == finder,
|
||||
# the client finds its own items in its own world.
|
||||
remote_items: bool = False
|
||||
|
||||
logger: logging.Logger
|
||||
|
||||
class LogStreamInterface:
|
||||
logger: logging.Logger
|
||||
buffer: List[str]
|
||||
|
||||
def __init__(self, logger: logging.Logger) -> None:
|
||||
self.logger = logger
|
||||
self.buffer = []
|
||||
|
||||
def write(self, msg: str) -> None:
|
||||
if msg.endswith('\n'):
|
||||
self.buffer.append(msg[:-1])
|
||||
self.logger.debug("".join(self.buffer))
|
||||
self.buffer = []
|
||||
else:
|
||||
self.buffer.append(msg)
|
||||
|
||||
def flush(self) -> None:
|
||||
pass
|
||||
|
||||
lsi: LogStreamInterface
|
||||
|
||||
id_to_zz_item: Optional[Dict[int, ZzItem]] = None
|
||||
zz_system: System
|
||||
_item_counts: "Counter[str]" = Counter()
|
||||
"""
|
||||
These are the items counts that will be in the game,
|
||||
which might be different from the item counts the player asked for in options
|
||||
(if the player asked for something invalid).
|
||||
"""
|
||||
my_locations: List[ZillionLocation] = []
|
||||
""" This is kind of a cache to avoid iterating through all the multiworld locations in logic. """
|
||||
|
||||
def __init__(self, world: MultiWorld, player: int):
|
||||
super().__init__(world, player)
|
||||
self.logger = logging.getLogger("Zillion")
|
||||
self.lsi = ZillionWorld.LogStreamInterface(self.logger)
|
||||
self.zz_system = System()
|
||||
|
||||
def _make_item_maps(self, start_char: Chars) -> None:
|
||||
_id_to_name, _id_to_zz_id, id_to_zz_item = make_id_to_others(start_char)
|
||||
self.id_to_zz_item = id_to_zz_item
|
||||
|
||||
@classmethod
|
||||
def stage_assert_generate(cls, world: MultiWorld) -> None:
|
||||
"""Checks that a game is capable of generating, usually checks for some base file like a ROM.
|
||||
Not run for unittests since they don't produce output"""
|
||||
rom_file = get_base_rom_path()
|
||||
if not os.path.exists(rom_file):
|
||||
raise FileNotFoundError(rom_file)
|
||||
|
||||
def generate_early(self) -> None:
|
||||
if not hasattr(self.world, "zillion_logic_cache"):
|
||||
setattr(self.world, "zillion_logic_cache", {})
|
||||
|
||||
zz_op, item_counts = validate(self.world, self.player)
|
||||
|
||||
self._item_counts = item_counts
|
||||
|
||||
rom_dir_name = os.path.dirname(get_base_rom_path())
|
||||
with redirect_stdout(self.lsi): # type: ignore
|
||||
self.zz_system.make_patcher(rom_dir_name)
|
||||
self.zz_system.make_randomizer(zz_op)
|
||||
|
||||
self.zz_system.make_map()
|
||||
|
||||
# just in case the options changed anything (I don't think they do)
|
||||
assert self.zz_system.randomizer, "init failed"
|
||||
for zz_name in self.zz_system.randomizer.locations:
|
||||
if zz_name != 'main':
|
||||
assert self.zz_system.randomizer.loc_name_2_pretty[zz_name] in self.location_name_to_id, \
|
||||
f"{self.zz_system.randomizer.loc_name_2_pretty[zz_name]} not in location map"
|
||||
|
||||
self._make_item_maps(zz_op.start_char)
|
||||
|
||||
def create_regions(self) -> None:
|
||||
assert self.zz_system.randomizer, "generate_early hasn't been called"
|
||||
assert self.id_to_zz_item, "generate_early hasn't been called"
|
||||
p = self.player
|
||||
w = self.world
|
||||
self.my_locations = []
|
||||
|
||||
self.zz_system.randomizer.place_canister_gun_reqs()
|
||||
|
||||
start = self.zz_system.randomizer.regions['start']
|
||||
|
||||
all: Dict[str, ZillionRegion] = {}
|
||||
for here_zz_name, zz_r in self.zz_system.randomizer.regions.items():
|
||||
here_name = "Menu" if here_zz_name == "start" else zz_reg_name_to_reg_name(here_zz_name)
|
||||
all[here_name] = ZillionRegion(zz_r, here_name, RegionType.Generic, here_name, p, w)
|
||||
self.world.regions.append(all[here_name])
|
||||
|
||||
limited_skill = Req(gun=3, jump=3, skill=self.zz_system.randomizer.options.skill, hp=940, red=1, floppy=126)
|
||||
queue = deque([start])
|
||||
done: Set[str] = set()
|
||||
while len(queue):
|
||||
zz_here = queue.popleft()
|
||||
here_name = "Menu" if zz_here.name == "start" else zz_reg_name_to_reg_name(zz_here.name)
|
||||
if here_name in done:
|
||||
continue
|
||||
here = all[here_name]
|
||||
|
||||
for zz_loc in zz_here.locations:
|
||||
# if local gun reqs didn't place "keyword" item
|
||||
if not zz_loc.item:
|
||||
|
||||
def access_rule_wrapped(zz_loc_local: ZzLocation,
|
||||
p: int,
|
||||
zz_r: ZzRandomizer,
|
||||
id_to_zz_item: Dict[int, ZzItem],
|
||||
cs: CollectionState) -> bool:
|
||||
accessible = cs_to_zz_locs(cs, p, zz_r, id_to_zz_item)
|
||||
return zz_loc_local in accessible
|
||||
|
||||
access_rule = functools.partial(access_rule_wrapped,
|
||||
zz_loc, self.player, self.zz_system.randomizer, self.id_to_zz_item)
|
||||
|
||||
loc_name = self.zz_system.randomizer.loc_name_2_pretty[zz_loc.name]
|
||||
loc = ZillionLocation(zz_loc, self.player, loc_name, here)
|
||||
loc.access_rule = access_rule
|
||||
if not (limited_skill >= zz_loc.req):
|
||||
loc.progress_type = LocationProgressType.EXCLUDED
|
||||
self.world.exclude_locations[p].value.add(loc.name)
|
||||
here.locations.append(loc)
|
||||
self.my_locations.append(loc)
|
||||
|
||||
for zz_dest in zz_here.connections.keys():
|
||||
dest_name = "Menu" if zz_dest.name == 'start' else zz_reg_name_to_reg_name(zz_dest.name)
|
||||
dest = all[dest_name]
|
||||
exit = Entrance(p, f"{here_name} to {dest_name}", here)
|
||||
here.exits.append(exit)
|
||||
exit.connect(dest)
|
||||
|
||||
queue.append(zz_dest)
|
||||
done.add(here.name)
|
||||
|
||||
def create_items(self) -> None:
|
||||
if not self.id_to_zz_item:
|
||||
self._make_item_maps("JJ")
|
||||
self.logger.warning("warning: called `create_items` without calling `generate_early` first")
|
||||
assert self.id_to_zz_item, "failed to get item maps"
|
||||
|
||||
# in zilliandomizer, the Randomizer class puts empties in the item pool to fill space,
|
||||
# but here in AP, empties are in the options from options.validate
|
||||
item_counts = self._item_counts
|
||||
self.logger.debug(item_counts)
|
||||
|
||||
for item_name, item_id in self.item_name_to_id.items():
|
||||
zz_item = self.id_to_zz_item[item_id]
|
||||
if item_id >= (4 + base_id): # normal item
|
||||
if item_name in item_counts:
|
||||
count = item_counts[item_name]
|
||||
self.logger.debug(f"Zillion Items: {item_name} {count}")
|
||||
self.world.itempool += [self.create_item(item_name) for _ in range(count)]
|
||||
elif item_id < (3 + base_id) and zz_item.code == RESCUE:
|
||||
# One of the 3 rescues will not be in the pool and its zz_item will be 'empty'.
|
||||
self.logger.debug(f"Zillion Items: {item_name} 1")
|
||||
self.world.itempool.append(self.create_item(item_name))
|
||||
|
||||
def set_rules(self) -> None:
|
||||
# logic for this game is in create_regions
|
||||
pass
|
||||
|
||||
def generate_basic(self) -> None:
|
||||
assert self.zz_system.randomizer, "generate_early hasn't been called"
|
||||
# main location name is an alias
|
||||
main_loc_name = self.zz_system.randomizer.loc_name_2_pretty[self.zz_system.randomizer.locations['main'].name]
|
||||
|
||||
self.world.get_location(main_loc_name, self.player)\
|
||||
.place_locked_item(self.create_item("Win"))
|
||||
self.world.completion_condition[self.player] = \
|
||||
lambda state: state.has("Win", self.player)
|
||||
|
||||
def post_fill(self) -> None:
|
||||
"""Optional Method that is called after regular fill. Can be used to do adjustments before output generation.
|
||||
This happens before progression balancing, so the items may not be in their final locations yet."""
|
||||
|
||||
self.zz_system.post_fill()
|
||||
|
||||
def finalize_item_locations(self) -> None:
|
||||
"""
|
||||
sync zilliandomizer item locations with AP item locations
|
||||
"""
|
||||
assert self.zz_system.randomizer and self.zz_system.patcher, "generate_early hasn't been called"
|
||||
zz_options = self.zz_system.randomizer.options
|
||||
|
||||
# debug_zz_loc_ids: Dict[str, int] = {}
|
||||
empty = zz_items[4]
|
||||
multi_item = empty # a different patcher method differentiates empty from ap multi item
|
||||
multi_items: Dict[str, Tuple[str, str]] = {} # zz_loc_name to (item_name, player_name)
|
||||
for loc in self.world.get_locations():
|
||||
if loc.player == self.player:
|
||||
z_loc = cast(ZillionLocation, loc)
|
||||
# debug_zz_loc_ids[z_loc.zz_loc.name] = id(z_loc.zz_loc)
|
||||
if z_loc.item is None:
|
||||
self.logger.warn("generate_output location has no item - is that ok?")
|
||||
z_loc.zz_loc.item = empty
|
||||
elif z_loc.item.player == self.player:
|
||||
z_item = cast(ZillionItem, z_loc.item)
|
||||
z_loc.zz_loc.item = z_item.zz_item
|
||||
else: # another player's item
|
||||
# print(f"put multi item in {z_loc.zz_loc.name}")
|
||||
z_loc.zz_loc.item = multi_item
|
||||
multi_items[z_loc.zz_loc.name] = (
|
||||
z_loc.item.name,
|
||||
self.world.get_player_name(z_loc.item.player)
|
||||
)
|
||||
# debug_zz_loc_ids.sort()
|
||||
# for name, id_ in debug_zz_loc_ids.items():
|
||||
# print(id_)
|
||||
# print("size:", len(debug_zz_loc_ids))
|
||||
|
||||
# debug_loc_to_id: Dict[str, int] = {}
|
||||
# regions = self.zz_randomizer.regions
|
||||
# for region in regions.values():
|
||||
# for loc in region.locations:
|
||||
# if loc.name not in self.zz_randomizer.locations:
|
||||
# print(f"region {region.name} had location {loc.name} not in locations")
|
||||
# debug_loc_to_id[loc.name] = id(loc)
|
||||
|
||||
# verify that every location got an item
|
||||
for zz_loc in self.zz_system.randomizer.locations.values():
|
||||
assert zz_loc.item, (
|
||||
f"location {self.zz_system.randomizer.loc_name_2_pretty[zz_loc.name]} "
|
||||
f"in world {self.player} didn't get an item"
|
||||
)
|
||||
|
||||
zz_patcher = self.zz_system.patcher
|
||||
|
||||
zz_patcher.write_locations(self.zz_system.randomizer.regions,
|
||||
zz_options.start_char,
|
||||
self.zz_system.randomizer.loc_name_2_pretty)
|
||||
zz_patcher.all_fixes_and_options(zz_options)
|
||||
zz_patcher.set_external_item_interface(zz_options.start_char, zz_options.max_level)
|
||||
zz_patcher.set_multiworld_items(multi_items)
|
||||
zz_patcher.set_rom_to_ram_data(self.world.player_name[self.player].replace(' ', '_').encode())
|
||||
|
||||
def generate_output(self, output_directory: str) -> None:
|
||||
"""This method gets called from a threadpool, do not use world.random here.
|
||||
If you need any last-second randomization, use MultiWorld.slot_seeds[slot] instead."""
|
||||
self.finalize_item_locations()
|
||||
|
||||
assert self.zz_system.patcher, "didn't get patcher from generate_early"
|
||||
# original_rom_bytes = self.zz_patcher.rom
|
||||
patched_rom_bytes = self.zz_system.patcher.get_patched_bytes()
|
||||
|
||||
out_file_base = self.world.get_out_file_name_base(self.player)
|
||||
|
||||
filename = os.path.join(
|
||||
output_directory,
|
||||
f'{out_file_base}{ZillionDeltaPatch.result_file_ending}'
|
||||
)
|
||||
with open(filename, "wb") as binary_file:
|
||||
binary_file.write(patched_rom_bytes)
|
||||
patch = ZillionDeltaPatch(
|
||||
os.path.splitext(filename)[0] + ZillionDeltaPatch.patch_file_ending,
|
||||
player=self.player,
|
||||
player_name=self.world.player_name[self.player],
|
||||
patched_path=filename
|
||||
)
|
||||
patch.write()
|
||||
os.remove(filename)
|
||||
|
||||
def fill_slot_data(self) -> Dict[str, Any]: # json of WebHostLib.models.Slot
|
||||
"""Fill in the `slot_data` field in the `Connected` network package.
|
||||
This is a way the generator can give custom data to the client.
|
||||
The client will receive this as JSON in the `Connected` response."""
|
||||
|
||||
# TODO: share a TypedDict data structure with client
|
||||
|
||||
# TODO: tell client which canisters are keywords
|
||||
# so it can open and get those when restoring doors
|
||||
|
||||
zz_patcher = self.zz_system.patcher
|
||||
assert zz_patcher, "didn't get patcher from generate_early"
|
||||
assert self.zz_system.randomizer, "didn't get randomizer from generate_early"
|
||||
|
||||
rescues: Dict[str, Any] = {}
|
||||
for i in (0, 1):
|
||||
if i in zz_patcher.rescue_locations:
|
||||
ri = zz_patcher.rescue_locations[i]
|
||||
rescues[str(i)] = {
|
||||
"start_char": ri.start_char,
|
||||
"room_code": ri.room_code,
|
||||
"mask": ri.mask
|
||||
}
|
||||
return {
|
||||
"start_char": self.zz_system.randomizer.options.start_char,
|
||||
"rescues": rescues,
|
||||
"loc_mem_to_id": zz_patcher.loc_memory_to_loc_id
|
||||
}
|
||||
|
||||
# def modify_multidata(self, multidata: Dict[str, Any]) -> None:
|
||||
# """For deeper modification of server multidata."""
|
||||
# # not modifying multidata, just want to call this at the end of the generation process
|
||||
# cache = getattr(self.world, "zillion_logic_cache")
|
||||
# import sys
|
||||
# print(sys.getsizeof(cache))
|
||||
|
||||
# end of ordered Main.py calls
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
"""Create an item for this world type and player.
|
||||
Warning: this may be called with self.world = None, for example by MultiServer"""
|
||||
item_id = _item_name_to_id[name]
|
||||
|
||||
if not self.id_to_zz_item:
|
||||
self._make_item_maps("JJ")
|
||||
self.logger.warning("warning: called `create_item` without calling `generate_early` first")
|
||||
assert self.id_to_zz_item, "failed to get item maps"
|
||||
|
||||
classification = ItemClassification.filler
|
||||
zz_item = self.id_to_zz_item[item_id]
|
||||
if zz_item.required:
|
||||
classification = ItemClassification.progression
|
||||
if not zz_item.is_progression:
|
||||
classification = ItemClassification.progression_skip_balancing
|
||||
|
||||
z_item = ZillionItem(name, classification, item_id, self.player, zz_item)
|
||||
return z_item
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
"""Called when the item pool needs to be filled with additional items to match location count."""
|
||||
return "Empty"
|
|
@ -0,0 +1 @@
|
|||
base_id = 8675309
|
|
@ -0,0 +1,74 @@
|
|||
# Zillion
|
||||
|
||||
Zillion is a metroidvania-style game released in 1987 for the 8-bit Sega Master System.
|
||||
|
||||
It's based on the anime Zillion (赤い光弾ジリオン, Akai Koudan Zillion).
|
||||
|
||||
## Where is the settings page?
|
||||
|
||||
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a config file.
|
||||
|
||||
## What changes are made to this game?
|
||||
|
||||
The way the original game lets the player choose who to level up has a few drawbacks in a multiworld randomizer:
|
||||
- Possible softlock from making bad choices (example: nobody has jump 3 when it's required)
|
||||
- In multiworld, you won't be able to choose because you won't know it's coming beforehand.
|
||||
|
||||
So this randomizer uses a new level-up system:
|
||||
- Everyone levels up together (even if they're not rescued yet).
|
||||
- You can choose how many opa-opas are required for a level up.
|
||||
- You can set a max level from 1 to 8.
|
||||
- The currently active character is still the only one that gets the health refill.
|
||||
|
||||
---
|
||||
|
||||
You can set these options to choose when characters will be able to attain certain jump levels:
|
||||
|
||||
```
|
||||
jump levels
|
||||
|
||||
vanilla balanced low restrictive
|
||||
|
||||
jj ap ch jj ap ch jj ap ch jj ap ch
|
||||
2 3 1 1 2 1 1 1 1 1 1 1
|
||||
2 3 1 2 2 1 1 2 1 1 1 1
|
||||
2 3 1 2 3 1 2 2 1 1 2 1
|
||||
2 3 1 2 3 2 2 3 1 1 2 1
|
||||
3 3 2 3 3 2 2 3 2 2 2 1
|
||||
3 3 2 3 3 2 3 3 2 2 2 1
|
||||
3 3 3 3 3 3 3 3 2 2 3 1
|
||||
3 3 3 3 3 3 3 3 3 2 3 2
|
||||
```
|
||||
|
||||
Note that in "restrictive" mode, Apple is the only one that can get jump level 3.
|
||||
|
||||
---
|
||||
|
||||
You can set these options to choose when characters will be able to attain certain Zillion power (gun) levels:
|
||||
|
||||
```
|
||||
zillion power
|
||||
|
||||
vanilla balanced low restrictive
|
||||
|
||||
jj ap ch jj ap ch jj ap ch jj ap ch
|
||||
1 1 3 1 1 2 1 1 1 1 1 1
|
||||
2 2 3 2 1 2 1 1 2 1 1 2
|
||||
3 3 3 2 2 3 2 1 2 2 1 2
|
||||
3 2 3 2 1 3 2 1 3
|
||||
3 3 3 2 2 3 2 2 3
|
||||
3 2 3
|
||||
3 3 3
|
||||
```
|
||||
|
||||
Note that in "restrictive" mode, Champ is the only one that can get Zillion power level 3.
|
||||
|
||||
## What does another world's item look like in Zillion?
|
||||
|
||||
Canisters retain their original appearance, so you won't know if an item belongs to another player until you collect it.
|
||||
|
||||
When you collect an item, you see the name of the player it goes to. You can see in the client log what item was collected.
|
||||
|
||||
## When the player receives an item, what happens?
|
||||
|
||||
The item collect sound is played. You can see in the client log what item was received.
|
|
@ -0,0 +1,104 @@
|
|||
# Zillion Setup Guide
|
||||
|
||||
## Required Software
|
||||
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `Zillion Client - Zillion Patch Setup`
|
||||
|
||||
- RetroArch 1.10.3 or newer from: [RetroArch Website](https://retroarch.com?page=platforms).
|
||||
|
||||
- Your legally obtained Zillion ROM file, named `Zillion (UE) [!].sms`
|
||||
|
||||
## Installation Procedures
|
||||
|
||||
### RetroArch
|
||||
|
||||
RetroArch 1.9.x will not work, as it is older than 1.10.3.
|
||||
|
||||
1. Enter the RetroArch main menu screen.
|
||||
2. Go to Main Menu --> Online Updater --> Core Downloader. Scroll down and install "Sega - MS/GG (SMS Plus GX)".
|
||||
3. Go to Settings --> User Interface. Set "Show Advanced Settings" to ON.
|
||||
4. Go to Settings --> Network. Set "Network Commands" to ON. (It is found below Request Device 16.) Leave the default
|
||||
Network Command Port at 55355.
|
||||
|
||||

|
||||
|
||||
### Linux Setup
|
||||
|
||||
Put your Zillion ROM file in the Archipelago directory in your home directory.
|
||||
|
||||
### Windows Setup
|
||||
|
||||
1. During the installation of Archipelago, install the Zillion Client. If you did not do this,
|
||||
or you are on an older version, you may run the installer again to install the Zillion Client.
|
||||
2. During setup, you will be asked to locate your base ROM file. This is the Zillion ROM file mentioned above in Required Software.
|
||||
|
||||
---
|
||||
# Play
|
||||
|
||||
## Create a Config (.yaml) File
|
||||
|
||||
### What is a config file and why do I need one?
|
||||
|
||||
See the guide on setting up a basic YAML at the Archipelago setup
|
||||
guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
|
||||
|
||||
### Where do I get a config file?
|
||||
|
||||
The [player settings page](/games/Zillion/player-settings) on the website allows you to configure your personal settings and export a config file from
|
||||
them.
|
||||
|
||||
### Verifying your config file
|
||||
|
||||
If you would like to validate your config file to make sure it works, you may do so on the [YAML Validator page](/mysterycheck).
|
||||
|
||||
## Generating a Single-Player Game
|
||||
|
||||
1. Navigate to the [player settings page](/games/Zillion/player-settings), configure your options, and click the "Generate Game" button.
|
||||
2. A "Seed Info" page will appear.
|
||||
3. Click the "Create New Room" link.
|
||||
4. A server page will appear. Download your patch file from this page.
|
||||
5. Patch your ROM file.
|
||||
- Linux
|
||||
- In the launcher, choose "Open Patch" and select your patch file.
|
||||
- Windows
|
||||
- Double-click on your patch file.
|
||||
The Zillion Client will launch automatically, and create your ROM in the location of the patch file.
|
||||
6. Open the ROM in RetroArch using the core "SMS Plus GX".
|
||||
- For a single player game, any emulator (or a Sega Master System) can be used, but there are additional features with RetroArch and the Zillion Client.
|
||||
- If you press reset or restore a save state and return to the surface in the game, the Zillion Client will keep open all the doors that you have opened.
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
1. Provide your config (yaml) file to the host and obtain your patch file.
|
||||
- When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that is done, the host will provide you with either a link to download your patch file, or with a zip file containing everyone's patch files. Your patch file should have a `.apzl` extension.
|
||||
- If you activate the "room generation" option in your config (yaml), you might want to tell your host that the generation will take longer than normal. It takes approximately 20 seconds longer for each Zillion player that enables this option.
|
||||
2. Create your ROM.
|
||||
- Linux
|
||||
- In the Archipelago Launcher, choose "Open Patch" and select your `.apzl` patch file.
|
||||
- Windows
|
||||
- Put your patch file on your desktop or somewhere convenient, and double click it.
|
||||
- This should automatically launch the client, and will also create your ROM in the same place as your patch file.
|
||||
3. Connect to the client.
|
||||
- Use RetroArch to open the ROM that was generated.
|
||||
- Be sure to select the **SMS Plus GX** core. This core will allow external tools to read RAM data.
|
||||
4. Connect to the Archipelago Server.
|
||||
- The patch file which launched your client should have automatically connected you to the AP Server. There are a few reasons this may not happen however, including if the game is hosted on the website but was generated elsewhere. If the client window shows "Server Status: Not Connected", simply ask the host for the address of the server, and copy/paste it into the "Server" input field then press enter.
|
||||
- The client will attempt to reconnect to the new server address, and should momentarily show "Server Status: Connected".
|
||||
5. Play the game.
|
||||
- When the client shows both Game and Server as connected, you're ready to begin playing. Congratulations on successfully joining a multiworld game!
|
||||
|
||||
## Hosting a MultiWorld game
|
||||
|
||||
The recommended way to host a game is to use our hosting service. The process is relatively simple:
|
||||
|
||||
1. Collect config files from your players.
|
||||
2. Create a zip file containing your players' config files.
|
||||
3. Upload that zip file to the [Generation page](/generate).
|
||||
- Generate page: [WebHost Seed Generation Page](/generate)
|
||||
4. Wait a moment while the seed is generated.
|
||||
5. When the seed is generated, a "Seed Info" page will appear.
|
||||
6. Click "Create New Room". This will take you to the server page. Provide the link to this page to your players, so
|
||||
they may download their patch files from there.
|
||||
7. Note that a link to a MultiWorld Tracker is at the top of the room page. The tracker shows the progress of all
|
||||
players in the game. Any observers may also be given the link to this page.
|
||||
8. Once all players have joined, you may begin playing.
|
|
@ -0,0 +1,93 @@
|
|||
from typing import Dict, Tuple
|
||||
from zilliandomizer.logic_components.items import Item as ZzItem, \
|
||||
item_name_to_id as zz_item_name_to_zz_id, items as zz_items, \
|
||||
item_name_to_item as zz_item_name_to_zz_item
|
||||
from zilliandomizer.options import Chars
|
||||
from zilliandomizer.utils.loc_name_maps import loc_to_id as pretty_loc_name_to_id
|
||||
from zilliandomizer.utils import parse_reg_name
|
||||
from .config import base_id as base_id
|
||||
|
||||
item_name_to_id = {
|
||||
"Apple": 0 + base_id,
|
||||
"Champ": 1 + base_id,
|
||||
"JJ": 2 + base_id,
|
||||
"Win": 3 + base_id,
|
||||
"Empty": 4 + base_id,
|
||||
"ID Card": 5 + base_id,
|
||||
"Red ID Card": 6 + base_id,
|
||||
"Floppy Disk": 7 + base_id,
|
||||
"Bread": 8 + base_id,
|
||||
"Opa-Opa": 9 + base_id,
|
||||
"Zillion": 10 + base_id,
|
||||
"Scope": 11 + base_id,
|
||||
}
|
||||
|
||||
|
||||
_zz_rescue_0 = zz_item_name_to_zz_item["rescue_0"]
|
||||
_zz_rescue_1 = zz_item_name_to_zz_item["rescue_1"]
|
||||
_zz_empty = zz_item_name_to_zz_item["empty"]
|
||||
|
||||
|
||||
def make_id_to_others(start_char: Chars) -> Tuple[
|
||||
Dict[int, str], Dict[int, int], Dict[int, ZzItem]
|
||||
]:
|
||||
""" returns id_to_name, id_to_zz_id, id_to_zz_item """
|
||||
id_to_name: Dict[int, str] = {}
|
||||
id_to_zz_id: Dict[int, int] = {}
|
||||
id_to_zz_item: Dict[int, ZzItem] = {}
|
||||
|
||||
if start_char == "JJ":
|
||||
name_to_zz_item = {
|
||||
"Apple": _zz_rescue_0,
|
||||
"Champ": _zz_rescue_1,
|
||||
"JJ": _zz_empty
|
||||
}
|
||||
elif start_char == "Apple":
|
||||
name_to_zz_item = {
|
||||
"Apple": _zz_empty,
|
||||
"Champ": _zz_rescue_1,
|
||||
"JJ": _zz_rescue_0
|
||||
}
|
||||
else: # Champ
|
||||
name_to_zz_item = {
|
||||
"Apple": _zz_rescue_0,
|
||||
"Champ": _zz_empty,
|
||||
"JJ": _zz_rescue_1
|
||||
}
|
||||
|
||||
for name, ap_id in item_name_to_id.items():
|
||||
id_to_name[ap_id] = name
|
||||
|
||||
if ap_id >= 4 + base_id:
|
||||
index = ap_id - base_id
|
||||
zz_item = zz_items[index]
|
||||
assert zz_item.id == index and zz_item.name == name
|
||||
elif ap_id < 3 + base_id:
|
||||
# rescue
|
||||
assert name in {"Apple", "Champ", "JJ"}
|
||||
zz_item = name_to_zz_item[name]
|
||||
else: # main
|
||||
zz_item = zz_item_name_to_zz_item["main"]
|
||||
|
||||
id_to_zz_id[ap_id] = zz_item_name_to_zz_id[zz_item.debug_name]
|
||||
id_to_zz_item[ap_id] = zz_item
|
||||
|
||||
return id_to_name, id_to_zz_id, id_to_zz_item
|
||||
|
||||
|
||||
def make_room_name(row: int, col: int) -> str:
|
||||
return f"{chr(ord('A') + row - 1)}-{col + 1}"
|
||||
|
||||
|
||||
loc_name_to_id: Dict[str, int] = {
|
||||
name: id_ + base_id
|
||||
for name, id_ in pretty_loc_name_to_id.items()
|
||||
}
|
||||
|
||||
|
||||
def zz_reg_name_to_reg_name(zz_reg_name: str) -> str:
|
||||
if zz_reg_name[0] == 'r' and zz_reg_name[3] == 'c':
|
||||
row, col = parse_reg_name(zz_reg_name)
|
||||
end = zz_reg_name[5:]
|
||||
return f"{make_room_name(row, col)} {end.upper()}"
|
||||
return zz_reg_name
|
|
@ -0,0 +1,12 @@
|
|||
from BaseClasses import Item, ItemClassification as IC
|
||||
from zilliandomizer.logic_components.items import Item as ZzItem
|
||||
|
||||
|
||||
class ZillionItem(Item):
|
||||
game = "Zillion"
|
||||
__slots__ = ("zz_item",)
|
||||
zz_item: ZzItem
|
||||
|
||||
def __init__(self, name: str, classification: IC, code: int, player: int, zz_item: ZzItem) -> None:
|
||||
super().__init__(name, classification, code, player)
|
||||
self.zz_item = zz_item
|
|
@ -0,0 +1,77 @@
|
|||
from typing import Dict, FrozenSet, Tuple, cast, List, Counter as _Counter
|
||||
from BaseClasses import CollectionState
|
||||
from zilliandomizer.logic_components.locations import Location
|
||||
from zilliandomizer.randomizer import Randomizer
|
||||
from zilliandomizer.logic_components.items import Item, items
|
||||
from .region import ZillionLocation
|
||||
from .item import ZillionItem
|
||||
from .id_maps import item_name_to_id
|
||||
|
||||
zz_empty = items[4]
|
||||
|
||||
# TODO: unit tests for these
|
||||
|
||||
|
||||
def set_randomizer_locs(cs: CollectionState, p: int, zz_r: Randomizer) -> int:
|
||||
"""
|
||||
sync up zilliandomizer locations with archipelago locations
|
||||
|
||||
returns a hash of the player and of the set locations with their items
|
||||
"""
|
||||
z_world = cs.world.worlds[p]
|
||||
my_locations = cast(List[ZillionLocation], getattr(z_world, "my_locations"))
|
||||
|
||||
_hash = p
|
||||
for z_loc in my_locations:
|
||||
zz_name = z_loc.zz_loc.name
|
||||
zz_item = z_loc.item.zz_item \
|
||||
if isinstance(z_loc.item, ZillionItem) and z_loc.item.player == p \
|
||||
else zz_empty
|
||||
zz_r.locations[zz_name].item = zz_item
|
||||
_hash += hash(zz_name) ^ hash(zz_item)
|
||||
return _hash
|
||||
|
||||
|
||||
def item_counts(cs: CollectionState, p: int) -> Tuple[Tuple[str, int], ...]:
|
||||
"""
|
||||
the zilliandomizer items that player p has collected
|
||||
|
||||
((item_name, count), (item_name, count), ...)
|
||||
"""
|
||||
return tuple((item_name, cs.item_count(item_name, p)) for item_name in item_name_to_id)
|
||||
|
||||
|
||||
LogicCacheType = Dict[int, Tuple[_Counter[Tuple[str, int]], FrozenSet[Location]]]
|
||||
|
||||
|
||||
def cs_to_zz_locs(cs: CollectionState, p: int, zz_r: Randomizer, id_to_zz_item: Dict[int, Item]) -> FrozenSet[Location]:
|
||||
"""
|
||||
given an Archipelago `CollectionState`,
|
||||
returns frozenset of accessible zilliandomizer locations
|
||||
"""
|
||||
# caching this function because it would be slow
|
||||
logic_cache: LogicCacheType = getattr(cs.world, "zillion_logic_cache", {})
|
||||
_hash = set_randomizer_locs(cs, p, zz_r)
|
||||
counts = item_counts(cs, p)
|
||||
_hash += hash(counts)
|
||||
|
||||
if _hash in logic_cache and logic_cache[_hash][0] == cs.prog_items:
|
||||
# print("cache hit")
|
||||
return logic_cache[_hash][1]
|
||||
|
||||
# print("cache miss")
|
||||
have_items: List[Item] = []
|
||||
for name, count in counts:
|
||||
have_items.extend([id_to_zz_item[item_name_to_id[name]]] * count)
|
||||
# have_req is the result of converting AP CollectionState to zilliandomizer collection state
|
||||
have_req = zz_r.make_ability(have_items)
|
||||
|
||||
# This `get_locations` is where the core of the logic comes in.
|
||||
# It takes a zilliandomizer collection state (a set of the abilities that I have)
|
||||
# and returns list of all the zilliandomizer locations I can access with those abilities.
|
||||
tr = frozenset(zz_r.get_locations(have_req))
|
||||
|
||||
# save result in cache
|
||||
logic_cache[_hash] = (cs.prog_items.copy(), tr)
|
||||
|
||||
return tr
|
|
@ -0,0 +1,380 @@
|
|||
from collections import Counter
|
||||
# import logging
|
||||
from typing import TYPE_CHECKING, Any, Dict, Tuple, cast
|
||||
from Options import AssembleOptions, DefaultOnToggle, Range, SpecialRange, Toggle, Choice
|
||||
from zilliandomizer.options import \
|
||||
Options as ZzOptions, char_to_gun, char_to_jump, ID, \
|
||||
VBLR as ZzVBLR, chars, Chars, ItemCounts as ZzItemCounts
|
||||
from zilliandomizer.options.parsing import validate as zz_validate
|
||||
if TYPE_CHECKING:
|
||||
from BaseClasses import MultiWorld
|
||||
|
||||
|
||||
class ZillionContinues(SpecialRange):
|
||||
"""
|
||||
number of continues before game over
|
||||
|
||||
game over teleports you to your ship, keeping items and open doors
|
||||
"""
|
||||
default = 3
|
||||
range_start = 0
|
||||
range_end = 21
|
||||
display_name = "continues"
|
||||
special_range_names = {
|
||||
"vanilla": 3,
|
||||
"infinity": 21
|
||||
}
|
||||
|
||||
|
||||
class ZillionEarlyScope(Toggle):
|
||||
""" whether to make sure there is a scope available early """
|
||||
display_name = "early scope"
|
||||
|
||||
|
||||
class ZillionFloppyReq(Range):
|
||||
""" how many floppy disks are required """
|
||||
range_start = 0
|
||||
range_end = 8
|
||||
default = 5
|
||||
display_name = "floppies required"
|
||||
|
||||
|
||||
class VBLR(Choice):
|
||||
option_vanilla = 0
|
||||
option_balanced = 1
|
||||
option_low = 2
|
||||
option_restrictive = 3
|
||||
default = 1
|
||||
|
||||
|
||||
class ZillionGunLevels(VBLR):
|
||||
"""
|
||||
Zillion gun power for the number of Zillion power ups you pick up
|
||||
|
||||
For "restrictive", Champ is the only one that can get Zillion gun power level 3.
|
||||
"""
|
||||
display_name = "gun levels"
|
||||
|
||||
|
||||
class ZillionJumpLevels(VBLR):
|
||||
"""
|
||||
jump levels for each character level
|
||||
|
||||
For "restrictive", Apple is the only one that can get jump level 3.
|
||||
"""
|
||||
display_name = "jump levels"
|
||||
|
||||
|
||||
class ZillionRandomizeAlarms(DefaultOnToggle):
|
||||
""" whether to randomize the locations of alarm sensors """
|
||||
display_name = "randomize alarms"
|
||||
|
||||
|
||||
class ZillionMaxLevel(Range):
|
||||
""" the highest level you can get """
|
||||
range_start = 3
|
||||
range_end = 8
|
||||
default = 8
|
||||
display_name = "max level"
|
||||
|
||||
|
||||
class ZillionOpasPerLevel(Range):
|
||||
"""
|
||||
how many Opa-Opas are required to level up
|
||||
|
||||
Lower makes you level up faster.
|
||||
"""
|
||||
range_start = 1
|
||||
range_end = 5
|
||||
default = 2
|
||||
display_name = "Opa-Opas per level"
|
||||
|
||||
|
||||
class ZillionStartChar(Choice):
|
||||
""" which character you start with """
|
||||
option_jj = 0
|
||||
option_apple = 1
|
||||
option_champ = 2
|
||||
display_name = "start character"
|
||||
default = "random"
|
||||
|
||||
|
||||
class ZillionIDCardCount(Range):
|
||||
"""
|
||||
how many ID Cards are in the game
|
||||
|
||||
Vanilla is 63
|
||||
|
||||
maximum total for all items is 144
|
||||
"""
|
||||
range_start = 0
|
||||
range_end = 126
|
||||
default = 42
|
||||
display_name = "ID Card count"
|
||||
|
||||
|
||||
class ZillionBreadCount(Range):
|
||||
"""
|
||||
how many Breads are in the game
|
||||
|
||||
Vanilla is 33
|
||||
|
||||
maximum total for all items is 144
|
||||
"""
|
||||
range_start = 0
|
||||
range_end = 126
|
||||
default = 50
|
||||
display_name = "Bread count"
|
||||
|
||||
|
||||
class ZillionOpaOpaCount(Range):
|
||||
"""
|
||||
how many Opa-Opas are in the game
|
||||
|
||||
Vanilla is 26
|
||||
|
||||
maximum total for all items is 144
|
||||
"""
|
||||
range_start = 0
|
||||
range_end = 126
|
||||
default = 26
|
||||
display_name = "Opa-Opa count"
|
||||
|
||||
|
||||
class ZillionZillionCount(Range):
|
||||
"""
|
||||
how many Zillion gun power ups are in the game
|
||||
|
||||
Vanilla is 6
|
||||
|
||||
maximum total for all items is 144
|
||||
"""
|
||||
range_start = 0
|
||||
range_end = 126
|
||||
default = 8
|
||||
display_name = "Zillion power up count"
|
||||
|
||||
|
||||
class ZillionFloppyDiskCount(Range):
|
||||
"""
|
||||
how many Floppy Disks are in the game
|
||||
|
||||
Vanilla is 5
|
||||
|
||||
maximum total for all items is 144
|
||||
"""
|
||||
range_start = 0
|
||||
range_end = 126
|
||||
default = 7
|
||||
display_name = "Floppy Disk count"
|
||||
|
||||
|
||||
class ZillionScopeCount(Range):
|
||||
"""
|
||||
how many Scopes are in the game
|
||||
|
||||
Vanilla is 4
|
||||
|
||||
maximum total for all items is 144
|
||||
"""
|
||||
range_start = 0
|
||||
range_end = 126
|
||||
default = 4
|
||||
display_name = "Scope count"
|
||||
|
||||
|
||||
class ZillionRedIDCardCount(Range):
|
||||
"""
|
||||
how many Red ID Cards are in the game
|
||||
|
||||
Vanilla is 1
|
||||
|
||||
maximum total for all items is 144
|
||||
"""
|
||||
range_start = 0
|
||||
range_end = 126
|
||||
default = 2
|
||||
display_name = "Red ID Card count"
|
||||
|
||||
|
||||
class ZillionSkill(Range):
|
||||
""" the difficulty level of the game """
|
||||
range_start = 0
|
||||
range_end = 5
|
||||
default = 2
|
||||
|
||||
|
||||
class ZillionStartingCards(SpecialRange):
|
||||
"""
|
||||
how many ID Cards to start the game with
|
||||
|
||||
Refilling at the ship also ensures you have at least this many cards.
|
||||
0 gives vanilla behavior.
|
||||
"""
|
||||
default = 2
|
||||
range_start = 0
|
||||
range_end = 10
|
||||
display_name = "starting cards"
|
||||
special_range_names = {
|
||||
"vanilla": 0
|
||||
}
|
||||
|
||||
|
||||
class ZillionRoomGen(Toggle):
|
||||
""" whether to generate rooms with random terrain """
|
||||
display_name = "room generation"
|
||||
|
||||
|
||||
zillion_options: Dict[str, AssembleOptions] = {
|
||||
"continues": ZillionContinues,
|
||||
# "early_scope": ZillionEarlyScope, # TODO: implement
|
||||
"floppy_req": ZillionFloppyReq,
|
||||
"gun_levels": ZillionGunLevels,
|
||||
"jump_levels": ZillionJumpLevels,
|
||||
"randomize_alarms": ZillionRandomizeAlarms,
|
||||
"max_level": ZillionMaxLevel,
|
||||
"start_char": ZillionStartChar,
|
||||
"opas_per_level": ZillionOpasPerLevel,
|
||||
"id_card_count": ZillionIDCardCount,
|
||||
"bread_count": ZillionBreadCount,
|
||||
"opa_opa_count": ZillionOpaOpaCount,
|
||||
"zillion_count": ZillionZillionCount,
|
||||
"floppy_disk_count": ZillionFloppyDiskCount,
|
||||
"scope_count": ZillionScopeCount,
|
||||
"red_id_card_count": ZillionRedIDCardCount,
|
||||
"skill": ZillionSkill,
|
||||
"starting_cards": ZillionStartingCards,
|
||||
"room_gen": ZillionRoomGen,
|
||||
}
|
||||
|
||||
|
||||
def convert_item_counts(ic: "Counter[str]") -> ZzItemCounts:
|
||||
tr: ZzItemCounts = {
|
||||
ID.card: ic["ID Card"],
|
||||
ID.red: ic["Red ID Card"],
|
||||
ID.floppy: ic["Floppy Disk"],
|
||||
ID.bread: ic["Bread"],
|
||||
ID.gun: ic["Zillion"],
|
||||
ID.opa: ic["Opa-Opa"],
|
||||
ID.scope: ic["Scope"],
|
||||
ID.empty: ic["Empty"],
|
||||
}
|
||||
return tr
|
||||
|
||||
|
||||
def validate(world: "MultiWorld", p: int) -> "Tuple[ZzOptions, Counter[str]]":
|
||||
"""
|
||||
adjusts options to make game completion possible
|
||||
|
||||
`world` parameter is MultiWorld object that has my options on it
|
||||
`p` is my player id
|
||||
"""
|
||||
for option_name in zillion_options:
|
||||
assert hasattr(world, option_name), f"Zillion option {option_name} didn't get put in world object"
|
||||
wo = cast(Any, world) # so I don't need getattr on all the options
|
||||
|
||||
skill = wo.skill[p].value
|
||||
|
||||
jump_levels = cast(ZillionJumpLevels, wo.jump_levels[p])
|
||||
jump_option = jump_levels.get_current_option_name().lower()
|
||||
required_level = char_to_jump["Apple"][cast(ZzVBLR, jump_option)].index(3) + 1
|
||||
if skill == 0:
|
||||
# because of hp logic on final boss
|
||||
required_level = 8
|
||||
|
||||
gun_levels = cast(ZillionGunLevels, wo.gun_levels[p])
|
||||
gun_option = gun_levels.get_current_option_name().lower()
|
||||
guns_required = char_to_gun["Champ"][cast(ZzVBLR, gun_option)].index(3)
|
||||
|
||||
floppy_req = cast(ZillionFloppyReq, wo.floppy_req[p])
|
||||
|
||||
card = cast(ZillionIDCardCount, wo.id_card_count[p])
|
||||
bread = cast(ZillionBreadCount, wo.bread_count[p])
|
||||
opa = cast(ZillionOpaOpaCount, wo.opa_opa_count[p])
|
||||
gun = cast(ZillionZillionCount, wo.zillion_count[p])
|
||||
floppy = cast(ZillionFloppyDiskCount, wo.floppy_disk_count[p])
|
||||
scope = cast(ZillionScopeCount, wo.scope_count[p])
|
||||
red = cast(ZillionRedIDCardCount, wo.red_id_card_count[p])
|
||||
item_counts = Counter({
|
||||
"ID Card": card,
|
||||
"Bread": bread,
|
||||
"Opa-Opa": opa,
|
||||
"Zillion": gun,
|
||||
"Floppy Disk": floppy,
|
||||
"Scope": scope,
|
||||
"Red ID Card": red
|
||||
})
|
||||
minimums = Counter({
|
||||
"ID Card": 0,
|
||||
"Bread": 0,
|
||||
"Opa-Opa": required_level - 1,
|
||||
"Zillion": guns_required,
|
||||
"Floppy Disk": floppy_req.value,
|
||||
"Scope": 0,
|
||||
"Red ID Card": 1
|
||||
})
|
||||
for key in minimums:
|
||||
item_counts[key] = max(minimums[key], item_counts[key])
|
||||
max_movables = 144 - sum(minimums.values())
|
||||
movables = item_counts - minimums
|
||||
while sum(movables.values()) > max_movables:
|
||||
# logging.warning("zillion options validate: player options item counts too high")
|
||||
total = sum(movables.values())
|
||||
scaler = max_movables / total
|
||||
for key in movables:
|
||||
movables[key] = int(movables[key] * scaler)
|
||||
item_counts = movables + minimums
|
||||
|
||||
# now have required items, and <= 144
|
||||
|
||||
# now fill remaining with empty
|
||||
total = sum(item_counts.values())
|
||||
diff = 144 - total
|
||||
if "Empty" not in item_counts:
|
||||
item_counts["Empty"] = 0
|
||||
item_counts["Empty"] += diff
|
||||
assert sum(item_counts.values()) == 144
|
||||
|
||||
max_level = cast(ZillionMaxLevel, wo.max_level[p])
|
||||
max_level.value = max(required_level, max_level.value)
|
||||
|
||||
opas_per_level = cast(ZillionOpasPerLevel, wo.opas_per_level[p])
|
||||
while (opas_per_level.value > 1) and (1 + item_counts["Opa-Opa"] // opas_per_level.value < max_level.value):
|
||||
# logging.warning(
|
||||
# "zillion options validate: option opas_per_level incompatible with options max_level and opa_opa_count"
|
||||
# )
|
||||
opas_per_level.value -= 1
|
||||
|
||||
# that should be all of the level requirements met
|
||||
|
||||
start_char = cast(ZillionStartChar, wo.start_char[p])
|
||||
start_char_name = start_char.get_current_option_name()
|
||||
if start_char_name == "Jj":
|
||||
start_char_name = "JJ"
|
||||
assert start_char_name in chars
|
||||
start_char_name = cast(Chars, start_char_name)
|
||||
|
||||
starting_cards = cast(ZillionStartingCards, wo.starting_cards[p])
|
||||
|
||||
room_gen = cast(ZillionRoomGen, wo.room_gen[p])
|
||||
|
||||
zz_item_counts = convert_item_counts(item_counts)
|
||||
zz_op = ZzOptions(
|
||||
zz_item_counts,
|
||||
cast(ZzVBLR, jump_option),
|
||||
cast(ZzVBLR, gun_option),
|
||||
opas_per_level.value,
|
||||
max_level.value,
|
||||
False, # tutorial
|
||||
skill,
|
||||
start_char_name,
|
||||
floppy_req.value,
|
||||
wo.continues[p].value,
|
||||
wo.randomize_alarms[p].value,
|
||||
False, # wo.early_scope[p].value,
|
||||
True, # balance defense
|
||||
starting_cards.value,
|
||||
bool(room_gen.value)
|
||||
)
|
||||
zz_validate(zz_op)
|
||||
return zz_op, item_counts
|
|
@ -0,0 +1,34 @@
|
|||
from typing import BinaryIO, Optional, cast
|
||||
import Utils
|
||||
from worlds.Files import APDeltaPatch
|
||||
import os
|
||||
|
||||
USHASH = 'd4bf9e7bcf9a48da53785d2ae7bc4270'
|
||||
|
||||
|
||||
class ZillionDeltaPatch(APDeltaPatch):
|
||||
hash = USHASH
|
||||
game = "Zillion"
|
||||
patch_file_ending = ".apzl"
|
||||
result_file_ending = ".sms"
|
||||
|
||||
@classmethod
|
||||
def get_source_data(cls) -> bytes:
|
||||
with open(get_base_rom_path(), "rb") as stream:
|
||||
return read_rom(stream)
|
||||
|
||||
|
||||
def get_base_rom_path(file_name: Optional[str] = None) -> str:
|
||||
options = Utils.get_options()
|
||||
if not file_name:
|
||||
file_name = cast(str, options["zillion_options"]["rom_file"])
|
||||
if not os.path.exists(file_name):
|
||||
file_name = Utils.user_path(file_name)
|
||||
return file_name
|
||||
|
||||
|
||||
def read_rom(stream: BinaryIO) -> bytes:
|
||||
""" reads rom into bytearray """
|
||||
data = stream.read()
|
||||
# I'm not aware of any sms header.
|
||||
return data
|
|
@ -0,0 +1,50 @@
|
|||
from typing import Optional
|
||||
from BaseClasses import MultiWorld, Region, RegionType, Location, Item, CollectionState
|
||||
from zilliandomizer.logic_components.regions import Region as ZzRegion
|
||||
from zilliandomizer.logic_components.locations import Location as ZzLocation
|
||||
from zilliandomizer.logic_components.items import RESCUE
|
||||
|
||||
from .id_maps import loc_name_to_id
|
||||
from .item import ZillionItem
|
||||
|
||||
|
||||
class ZillionRegion(Region):
|
||||
zz_r: ZzRegion
|
||||
|
||||
def __init__(self,
|
||||
zz_r: ZzRegion,
|
||||
name: str,
|
||||
type_: RegionType,
|
||||
hint: str,
|
||||
player: int,
|
||||
world: Optional[MultiWorld] = None) -> None:
|
||||
super().__init__(name, type_, hint, player, world)
|
||||
self.zz_r = zz_r
|
||||
|
||||
|
||||
class ZillionLocation(Location):
|
||||
zz_loc: ZzLocation
|
||||
game: str = "Zillion"
|
||||
|
||||
def __init__(self,
|
||||
zz_loc: ZzLocation,
|
||||
player: int,
|
||||
name: str,
|
||||
parent: Optional[Region] = None) -> None:
|
||||
loc_id = loc_name_to_id[name]
|
||||
super().__init__(player, name, loc_id, parent)
|
||||
self.zz_loc = zz_loc
|
||||
|
||||
# override
|
||||
def can_fill(self, state: CollectionState, item: Item, check_access: bool = True) -> bool:
|
||||
saved_gun_req = -1
|
||||
if isinstance(item, ZillionItem) \
|
||||
and item.zz_item.code == RESCUE \
|
||||
and self.player == item.player:
|
||||
# RESCUE removes the gun requirement from a location.
|
||||
saved_gun_req = self.zz_loc.req.gun
|
||||
self.zz_loc.req.gun = 0
|
||||
super_result = super().can_fill(state, item, check_access)
|
||||
if saved_gun_req != -1:
|
||||
self.zz_loc.req.gun = saved_gun_req
|
||||
return super_result
|
|
@ -0,0 +1 @@
|
|||
git+https://github.com/beauxq/zilliandomizer@45a45eaca4119a4d06d2c31546ad19f3abd77f63#egg=zilliandomizer==0.4.4
|
Loading…
Reference in New Issue