This commit is contained in:
michaelasantiago
2022-09-02 14:32:44 -04:00
46 changed files with 1081 additions and 811 deletions

View File

@@ -2,7 +2,7 @@ name: Bug Report
description: File a bug report.
title: "Bug: "
labels:
- bug
- bug / fix
body:
- type: markdown
attributes:
@@ -32,4 +32,4 @@ body:
- Local generation
- While playing
validations:
required: true
required: true

View File

@@ -18,8 +18,8 @@ jobs:
python-version: 3.9
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest
python -m pip install --upgrade pip wheel
pip install flake8 pytest pytest-subtests
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8
run: |

View File

@@ -32,8 +32,8 @@ jobs:
python-version: ${{ matrix.python.version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest
python -m pip install --upgrade pip wheel
pip install flake8 pytest pytest-subtests
python ModuleUpdate.py --yes --force --append "WebHostLib/requirements.txt"
- name: Unittests
run: |

View File

@@ -152,8 +152,9 @@ class CommonContext:
# locations
locations_checked: typing.Set[int] # local state
locations_scouted: typing.Set[int]
missing_locations: typing.Set[int]
missing_locations: typing.Set[int] # server state
checked_locations: typing.Set[int] # server state
server_locations: typing.Set[int] # all locations the server knows of, missing_location | checked_locations
locations_info: typing.Dict[int, NetworkItem]
# internals
@@ -184,8 +185,9 @@ class CommonContext:
self.locations_checked = set() # local state
self.locations_scouted = set()
self.items_received = []
self.missing_locations = set()
self.missing_locations = set() # server state
self.checked_locations = set() # server state
self.server_locations = set() # all locations the server knows of, missing_location | checked_locations
self.locations_info = {}
self.input_queue = asyncio.Queue()
@@ -345,6 +347,8 @@ class CommonContext:
cache_package = Utils.persistent_load().get("datapackage", {}).get("games", {})
needed_updates: typing.Set[str] = set()
for game in relevant_games:
if game not in remote_datepackage_versions:
continue
remote_version: int = remote_datepackage_versions[game]
if remote_version == 0: # custom datapackage for this game
@@ -632,6 +636,7 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
# when /missing is used for the client side view of what is missing.
ctx.missing_locations = set(args["missing_locations"])
ctx.checked_locations = set(args["checked_locations"])
ctx.server_locations = ctx.missing_locations | ctx. checked_locations
elif cmd == 'ReceivedItems':
start_index = args["index"]

View File

@@ -63,7 +63,7 @@ class PlandoSettings(enum.IntFlag):
def __str__(self) -> str:
if self.value:
return ", ".join((flag.name for flag in PlandoSettings if self.value & flag.value))
return ", ".join(flag.name for flag in PlandoSettings if self.value & flag.value)
return "Off"
@@ -84,11 +84,6 @@ def mystery_argparse():
parser.add_argument('--seed', help='Define seed number to generate.', type=int)
parser.add_argument('--multi', default=defaults["players"], type=lambda value: max(int(value), 1))
parser.add_argument('--spoiler', type=int, default=defaults["spoiler"])
parser.add_argument('--lttp_rom', default=options["lttp_options"]["rom_file"],
help="Path to the 1.0 JP LttP Baserom.") # absolute, relative to cwd or relative to app path
parser.add_argument('--sm_rom', default=options["sm_options"]["rom_file"],
help="Path to the 1.0 JP SM Baserom.")
parser.add_argument('--enemizercli', default=resolve_path(defaults["enemizer_path"], local_path))
parser.add_argument('--outputpath', default=resolve_path(options["general_options"]["output_path"], user_path),
help="Path to output folder. Absolute or relative to cwd.") # absolute or relative to cwd
parser.add_argument('--race', action='store_true', default=defaults["race"])
@@ -183,10 +178,6 @@ def main(args=None, callback=ERmain):
Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level)
erargs.lttp_rom = args.lttp_rom
erargs.sm_rom = args.sm_rom
erargs.enemizercli = args.enemizercli
settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \
{fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.samesettings else None)
for fname, yamls in weights_cache.items()}

View File

@@ -10,16 +10,20 @@ Scroll down to components= to add components to the launcher as well as setup.py
import argparse
from os.path import isfile
import sys
from typing import Iterable, Sequence, Callable, Union, Optional
import subprocess
import itertools
from Utils import is_frozen, user_path, local_path, init_logging, open_filename, messagebox,\
is_windows, is_macos, is_linux
from shutil import which
import shlex
import subprocess
import sys
from enum import Enum, auto
from os.path import isfile
from shutil import which
from typing import Iterable, Sequence, Callable, Union, Optional
import ModuleUpdate
ModuleUpdate.update()
from Utils import is_frozen, user_path, local_path, init_logging, open_filename, messagebox, \
is_windows, is_macos, is_linux
def open_host_yaml():

View File

@@ -752,6 +752,7 @@ class SpriteSelector():
self.window['pady'] = 5
self.spritesPerRow = 32
self.all_sprites = []
self.invalid_sprites = []
self.sprite_pool = spritePool
def open_custom_sprite_dir(_evt):
@@ -833,6 +834,13 @@ class SpriteSelector():
self.window.focus()
tkinter_center_window(self.window)
if self.invalid_sprites:
invalid = sorted(self.invalid_sprites)
logging.warning(f"The following sprites are invalid: {', '.join(invalid)}")
msg = f"{invalid[0]} "
msg += f"and {len(invalid)-1} more are invalid" if len(invalid) > 1 else "is invalid"
messagebox.showerror("Invalid sprites detected", msg, parent=self.window)
def remove_from_sprite_pool(self, button, spritename):
self.callback(("remove", spritename))
self.spritePoolButtons.buttons.remove(button)
@@ -897,7 +905,13 @@ class SpriteSelector():
sprites = []
for file in os.listdir(path):
sprites.append((file, Sprite(os.path.join(path, file))))
if file == '.gitignore':
continue
sprite = Sprite(os.path.join(path, file))
if sprite.valid:
sprites.append((file, sprite))
else:
self.invalid_sprites.append(file)
sprites.sort(key=lambda s: str.lower(s[1].name or "").strip())

View File

@@ -70,7 +70,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
world.required_medallions = args.required_medallions.copy()
world.game = args.game.copy()
world.player_name = args.name.copy()
world.enemizer = args.enemizercli
world.sprite = args.sprite.copy()
world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option.

View File

@@ -298,7 +298,7 @@ class Toggle(NumericOption):
if type(data) == str:
return cls.from_text(data)
else:
return cls(data)
return cls(int(data))
@classmethod
def get_option_name(cls, value):

View File

@@ -149,8 +149,8 @@ class Context(CommonContext):
def event_invalid_slot(self):
if self.snes_socket is not None and not self.snes_socket.closed:
asyncio.create_task(self.snes_socket.close())
raise Exception('Invalid ROM detected, '
'please verify that you have loaded the correct rom and reconnect your snes (/snes)')
raise Exception("Invalid ROM detected, "
"please verify that you have loaded the correct rom and reconnect your snes (/snes)")
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
@@ -158,7 +158,7 @@ class Context(CommonContext):
if self.rom is None:
self.awaiting_rom = True
snes_logger.info(
'No ROM detected, awaiting snes connection to authenticate to the multiworld server (/snes)')
"No ROM detected, awaiting snes connection to authenticate to the multiworld server (/snes)")
return
self.awaiting_rom = False
self.auth = self.rom
@@ -262,7 +262,7 @@ async def deathlink_kill_player(ctx: Context):
SNES_RECONNECT_DELAY = 5
# LttP
# FXPAK Pro protocol memory mapping used by SNI
ROM_START = 0x000000
WRAM_START = 0xF50000
WRAM_SIZE = 0x20000
@@ -293,21 +293,24 @@ SHOP_LEN = (len(Shops.shop_table) * 3) + 5
DEATH_LINK_ACTIVE_ADDR = ROMNAME_START + 0x15 # 1 byte
# SM
SM_ROMNAME_START = 0x007FC0
SM_ROMNAME_START = ROM_START + 0x007FC0
SM_INGAME_MODES = {0x07, 0x09, 0x0b}
SM_ENDGAME_MODES = {0x26, 0x27}
SM_DEATH_MODES = {0x15, 0x17, 0x18, 0x19, 0x1A}
SM_RECV_PROGRESS_ADDR = SRAM_START + 0x2000 # 2 bytes
SM_RECV_ITEM_ADDR = SAVEDATA_START + 0x4D2 # 1 byte
SM_RECV_ITEM_PLAYER_ADDR = SAVEDATA_START + 0x4D3 # 1 byte
# RECV and SEND are from the gameplay's perspective: SNIClient writes to RECV queue and reads from SEND queue
SM_RECV_QUEUE_START = SRAM_START + 0x2000
SM_RECV_QUEUE_WCOUNT = SRAM_START + 0x2602
SM_SEND_QUEUE_START = SRAM_START + 0x2700
SM_SEND_QUEUE_RCOUNT = SRAM_START + 0x2680
SM_SEND_QUEUE_WCOUNT = SRAM_START + 0x2682
SM_DEATH_LINK_ACTIVE_ADDR = ROM_START + 0x277f04 # 1 byte
SM_REMOTE_ITEM_FLAG_ADDR = ROM_START + 0x277f06 # 1 byte
# SMZ3
SMZ3_ROMNAME_START = 0x00FFC0
SMZ3_ROMNAME_START = ROM_START + 0x00FFC0
SMZ3_INGAME_MODES = {0x07, 0x09, 0x0b}
SMZ3_ENDGAME_MODES = {0x26, 0x27}
@@ -1083,6 +1086,9 @@ async def game_watcher(ctx: Context):
if ctx.awaiting_rom:
await ctx.server_auth(False)
elif ctx.server is None:
snes_logger.warning("ROM detected but no active multiworld server connection. " +
"Connect using command: /connect server:port")
if ctx.auth and ctx.auth != ctx.rom:
snes_logger.warning("ROM change detected, please reconnect to the multiworld server")
@@ -1159,6 +1165,9 @@ async def game_watcher(ctx: Context):
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": [scout_location]}])
await track_locations(ctx, roomid, roomdata)
elif ctx.game == GAME_SM:
if ctx.server is None or ctx.slot is None:
# not successfully connected to a multiworld server, cannot process the game sending items
continue
gamemode = await snes_read(ctx, WRAM_START + 0x0998, 1)
if "DeathLink" in ctx.tags and gamemode and ctx.last_death_link + 1 < time.time():
currently_dead = gamemode[0] in SM_DEATH_MODES
@@ -1169,25 +1178,25 @@ async def game_watcher(ctx: Context):
ctx.finished_game = True
continue
data = await snes_read(ctx, SM_RECV_PROGRESS_ADDR + 0x680, 4)
data = await snes_read(ctx, SM_SEND_QUEUE_RCOUNT, 4)
if data is None:
continue
recv_index = data[0] | (data[1] << 8)
recv_item = data[2] | (data[3] << 8)
recv_item = data[2] | (data[3] << 8) # this is actually SM_SEND_QUEUE_WCOUNT
while (recv_index < recv_item):
itemAdress = recv_index * 8
message = await snes_read(ctx, SM_RECV_PROGRESS_ADDR + 0x700 + itemAdress, 8)
message = await snes_read(ctx, SM_SEND_QUEUE_START + itemAdress, 8)
# worldId = message[0] | (message[1] << 8) # unused
# itemId = message[2] | (message[3] << 8) # unused
itemIndex = (message[4] | (message[5] << 8)) >> 3
recv_index += 1
snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + 0x680,
snes_buffered_write(ctx, SM_SEND_QUEUE_RCOUNT,
bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
from worlds.sm.Locations import locations_start_id
from worlds.sm import locations_start_id
location_id = locations_start_id + itemIndex
ctx.locations_checked.add(location_id)
@@ -1196,15 +1205,14 @@ async def game_watcher(ctx: Context):
f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}])
data = await snes_read(ctx, SM_RECV_PROGRESS_ADDR + 0x600, 4)
data = await snes_read(ctx, SM_RECV_QUEUE_WCOUNT, 2)
if data is None:
continue
# recv_itemOutPtr = data[0] | (data[1] << 8) # unused
itemOutPtr = data[2] | (data[3] << 8)
itemOutPtr = data[0] | (data[1] << 8)
from worlds.sm.Items import items_start_id
from worlds.sm.Locations import locations_start_id
from worlds.sm import items_start_id
from worlds.sm import locations_start_id
if itemOutPtr < len(ctx.items_received):
item = ctx.items_received[itemOutPtr]
itemId = item.item - items_start_id
@@ -1214,10 +1222,10 @@ async def game_watcher(ctx: Context):
locationId = 0x00 #backward compat
playerID = item.player if item.player <= SM_ROM_PLAYER_LIMIT else 0
snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + itemOutPtr * 4, bytes(
snes_buffered_write(ctx, SM_RECV_QUEUE_START + itemOutPtr * 4, bytes(
[playerID & 0xFF, (playerID >> 8) & 0xFF, itemId & 0xFF, locationId & 0xFF]))
itemOutPtr += 1
snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + 0x602,
snes_buffered_write(ctx, SM_RECV_QUEUE_WCOUNT,
bytes([itemOutPtr & 0xFF, (itemOutPtr >> 8) & 0xFF]))
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
color(ctx.item_names[item.item], 'red', 'bold'),
@@ -1225,6 +1233,9 @@ async def game_watcher(ctx: Context):
ctx.location_names[item.location], itemOutPtr, len(ctx.items_received)))
await snes_flush_writes(ctx)
elif ctx.game == GAME_SMZ3:
if ctx.server is None or ctx.slot is None:
# not successfully connected to a multiworld server, cannot process the game sending items
continue
currentGame = await snes_read(ctx, SRAM_START + 0x33FE, 2)
if (currentGame is not None):
if (currentGame[0] != 0):

View File

@@ -1,31 +1,31 @@
from __future__ import annotations
import multiprocessing
import logging
import asyncio
import copy
import ctypes
import logging
import multiprocessing
import os.path
import re
import sys
import typing
import queue
from pathlib import Path
import nest_asyncio
import sc2
from sc2.main import run_game
from sc2.data import Race
from sc2.bot_ai import BotAI
from sc2.data import Race
from sc2.main import run_game
from sc2.player import Bot
from worlds.sc2wol.Regions import MissionInfo
from worlds.sc2wol.MissionTables import lookup_id_to_mission
from MultiServer import mark_raw
from Utils import init_logging, is_windows
from worlds.sc2wol import SC2WoLWorld
from worlds.sc2wol.Items import lookup_id_to_name, item_table
from worlds.sc2wol.Locations import SC2WOL_LOC_ID_OFFSET
from worlds.sc2wol import SC2WoLWorld
from pathlib import Path
import re
from MultiServer import mark_raw
import ctypes
import sys
from Utils import init_logging, is_windows
from worlds.sc2wol.MissionTables import lookup_id_to_mission
from worlds.sc2wol.Regions import MissionInfo
if __name__ == "__main__":
init_logging("SC2Client", exception_logger="Client")
@@ -35,10 +35,12 @@ sc2_logger = logging.getLogger("Starcraft2")
import colorama
from NetUtils import *
from NetUtils import ClientStatus, RawJSONtoTextParser
from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser
nest_asyncio.apply()
max_bonus: int = 8
victory_modulo: int = 100
class StarcraftClientProcessor(ClientCommandProcessor):
@@ -98,13 +100,13 @@ class StarcraftClientProcessor(ClientCommandProcessor):
def _cmd_available(self) -> bool:
"""Get what missions are currently available to play"""
request_available_missions(self.ctx.checked_locations, self.ctx.mission_req_table, self.ctx.ui)
request_available_missions(self.ctx)
return True
def _cmd_unfinished(self) -> bool:
"""Get what missions are currently available to play and have not had all locations checked"""
request_unfinished_missions(self.ctx.checked_locations, self.ctx.mission_req_table, self.ctx.ui, self.ctx)
request_unfinished_missions(self.ctx)
return True
@mark_raw
@@ -125,18 +127,19 @@ class SC2Context(CommonContext):
items_handling = 0b111
difficulty = -1
all_in_choice = 0
mission_req_table = None
items_rec_to_announce = []
rec_announce_pos = 0
items_sent_to_announce = []
sent_announce_pos = 0
announcements = []
announcement_pos = 0
mission_req_table: typing.Dict[str, MissionInfo] = {}
announcements = queue.Queue()
sc2_run_task: typing.Optional[asyncio.Task] = None
missions_unlocked = False
missions_unlocked: bool = False # allow launching missions ignoring requirements
current_tooltip = None
last_loc_list = None
difficulty_override = -1
mission_id_to_location_ids: typing.Dict[int, typing.List[int]] = {}
raw_text_parser: RawJSONtoTextParser
def __init__(self, *args, **kwargs):
super(SC2Context, self).__init__(*args, **kwargs)
self.raw_text_parser = RawJSONtoTextParser(self)
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
@@ -149,30 +152,32 @@ class SC2Context(CommonContext):
self.difficulty = args["slot_data"]["game_difficulty"]
self.all_in_choice = args["slot_data"]["all_in_map"]
slot_req_table = args["slot_data"]["mission_req"]
self.mission_req_table = {}
# Compatibility for 0.3.2 server data.
if "category" not in next(iter(slot_req_table)):
for i, mission_data in enumerate(slot_req_table.values()):
mission_data["category"] = wol_default_categories[i]
for mission in slot_req_table:
self.mission_req_table[mission] = MissionInfo(**slot_req_table[mission])
self.mission_req_table = {
mission: MissionInfo(**slot_req_table[mission]) for mission in slot_req_table
}
self.build_location_to_mission_mapping()
# Look for and set SC2PATH.
# check_game_install_path() returns True if and only if it finds + sets SC2PATH.
if "SC2PATH" not in os.environ and check_game_install_path():
check_mod_install()
if cmd in {"PrintJSON"}:
if "receiving" in args:
if self.slot_concerns_self(args["receiving"]):
self.announcements.append(args["data"])
return
if "item" in args:
if self.slot_concerns_self(args["item"].player):
self.announcements.append(args["data"])
def on_print_json(self, args: dict):
if "receiving" in args and self.slot_concerns_self(args["receiving"]):
relevant = True
elif "item" in args and self.slot_concerns_self(args["item"].player):
relevant = True
else:
relevant = False
if relevant:
self.announcements.put(self.raw_text_parser(copy.deepcopy(args["data"])))
super(SC2Context, self).on_print_json(args)
def run_gui(self):
from kvui import GameManager, HoverBehavior, ServerToolTip, fade_in_animation
from kvui import GameManager, HoverBehavior, ServerToolTip
from kivy.app import App
from kivy.clock import Clock
from kivy.uix.tabbedpanel import TabbedPanelItem
@@ -190,6 +195,7 @@ class SC2Context(CommonContext):
class MissionButton(HoverableButton):
tooltip_text = StringProperty("Test")
ctx: SC2Context
def __init__(self, *args, **kwargs):
super(HoverableButton, self).__init__(*args, **kwargs)
@@ -210,10 +216,7 @@ class SC2Context(CommonContext):
self.ctx.current_tooltip = self.layout
def on_leave(self):
if self.ctx.current_tooltip:
App.get_running_app().root.remove_widget(self.ctx.current_tooltip)
self.ctx.current_tooltip = None
self.ctx.ui.clear_tooltip()
@property
def ctx(self) -> CommonContext:
@@ -235,13 +238,20 @@ class SC2Context(CommonContext):
mission_panel = None
last_checked_locations = {}
mission_id_to_button = {}
launching = False
launching: typing.Union[bool, int] = False # if int -> mission ID
refresh_from_launching = True
first_check = True
ctx: SC2Context
def __init__(self, ctx):
super().__init__(ctx)
def clear_tooltip(self):
if self.ctx.current_tooltip:
App.get_running_app().root.remove_widget(self.ctx.current_tooltip)
self.ctx.current_tooltip = None
def build(self):
container = super().build()
@@ -256,7 +266,7 @@ class SC2Context(CommonContext):
def build_mission_table(self, dt):
if (not self.launching and (not self.last_checked_locations == self.ctx.checked_locations or
not self.refresh_from_launching)) or self.first_check:
not self.refresh_from_launching)) or self.first_check:
self.refresh_from_launching = True
self.mission_panel.clear_widgets()
@@ -267,12 +277,7 @@ class SC2Context(CommonContext):
self.mission_id_to_button = {}
categories = {}
available_missions = []
unfinished_locations = initialize_blank_mission_dict(self.ctx.mission_req_table)
unfinished_missions = calc_unfinished_missions(self.ctx.checked_locations,
self.ctx.mission_req_table,
self.ctx, available_missions=available_missions,
unfinished_locations=unfinished_locations)
available_missions, unfinished_missions = calc_unfinished_missions(self.ctx)
# separate missions into categories
for mission in self.ctx.mission_req_table:
@@ -283,7 +288,8 @@ class SC2Context(CommonContext):
for category in categories:
category_panel = MissionCategory()
category_panel.add_widget(Label(text=category, size_hint_y=None, height=50, outline_width=1))
category_panel.add_widget(
Label(text=category, size_hint_y=None, height=50, outline_width=1))
# Map is completed
for mission in categories[category]:
@@ -295,7 +301,9 @@ class SC2Context(CommonContext):
text = f"[color=6495ED]{text}[/color]"
tooltip = f"Uncollected locations:\n"
tooltip += "\n".join(location for location in unfinished_locations[mission])
tooltip += "\n".join([self.ctx.location_names[loc] for loc in
self.ctx.locations_for_mission(mission)
if loc in self.ctx.missing_locations])
elif mission in available_missions:
text = f"[color=FFFFFF]{text}[/color]"
# Map requirements not met
@@ -303,7 +311,7 @@ class SC2Context(CommonContext):
text = f"[color=a9a9a9]{text}[/color]"
tooltip = f"Requires: "
if len(self.ctx.mission_req_table[mission].required_world) > 0:
tooltip += ", ".join(list(self.ctx.mission_req_table)[req_mission-1] for
tooltip += ", ".join(list(self.ctx.mission_req_table)[req_mission - 1] for
req_mission in
self.ctx.mission_req_table[mission].required_world)
@@ -325,13 +333,17 @@ class SC2Context(CommonContext):
self.refresh_from_launching = False
self.mission_panel.clear_widgets()
self.mission_panel.add_widget(Label(text="Launching Mission"))
self.mission_panel.add_widget(Label(text="Launching Mission: " +
lookup_id_to_mission[self.launching]))
if self.ctx.ui:
self.ctx.ui.clear_tooltip()
def mission_callback(self, button):
if not self.launching:
self.ctx.play_mission(list(self.mission_id_to_button.keys())
[list(self.mission_id_to_button.values()).index(button)])
self.launching = True
mission_id: int = list(self.mission_id_to_button.values()).index(button)
self.ctx.play_mission(list(self.mission_id_to_button)
[mission_id])
self.launching = mission_id
Clock.schedule_once(self.finish_launching, 10)
def finish_launching(self, dt):
@@ -349,7 +361,7 @@ class SC2Context(CommonContext):
def play_mission(self, mission_id):
if self.missions_unlocked or \
is_mission_available(mission_id, self.checked_locations, self.mission_req_table):
is_mission_available(self, mission_id):
if self.sc2_run_task:
if not self.sc2_run_task.done():
sc2_logger.warning("Starcraft 2 Client is still running!")
@@ -358,12 +370,29 @@ class SC2Context(CommonContext):
sc2_logger.warning("Launching Mission without Archipelago authentication, "
"checks will not be registered to server.")
self.sc2_run_task = asyncio.create_task(starcraft_launch(self, mission_id),
name="Starcraft 2 Launch")
name="Starcraft 2 Launch")
else:
sc2_logger.info(
f"{lookup_id_to_mission[mission_id]} is not currently unlocked. "
f"Use /unfinished or /available to see what is available.")
def build_location_to_mission_mapping(self):
mission_id_to_location_ids: typing.Dict[int, typing.Set[int]] = {
mission_info.id: set() for mission_info in self.mission_req_table.values()
}
for loc in self.server_locations:
mission_id, objective = divmod(loc - SC2WOL_LOC_ID_OFFSET, victory_modulo)
mission_id_to_location_ids[mission_id].add(objective)
self.mission_id_to_location_ids = {mission_id: sorted(objectives) for mission_id, objectives in
mission_id_to_location_ids.items()}
def locations_for_mission(self, mission: str):
mission_id: int = self.mission_req_table[mission].id
objectives = self.mission_id_to_location_ids[self.mission_req_table[mission].id]
for objective in objectives:
yield SC2WOL_LOC_ID_OFFSET + mission_id * 100 + objective
async def main():
multiprocessing.freeze_support()
@@ -459,11 +488,7 @@ def calc_difficulty(difficulty):
return 'X'
async def starcraft_launch(ctx: SC2Context, mission_id):
ctx.rec_announce_pos = len(ctx.items_rec_to_announce)
ctx.sent_announce_pos = len(ctx.items_sent_to_announce)
ctx.announcements_pos = len(ctx.announcements)
async def starcraft_launch(ctx: SC2Context, mission_id: int):
sc2_logger.info(f"Launching {lookup_id_to_mission[mission_id]}. If game does not launch check log file for errors.")
with DllDirectory(None):
@@ -472,32 +497,29 @@ async def starcraft_launch(ctx: SC2Context, mission_id):
class ArchipelagoBot(sc2.bot_ai.BotAI):
game_running = False
mission_completed = False
first_bonus = False
second_bonus = False
third_bonus = False
fourth_bonus = False
fifth_bonus = False
sixth_bonus = False
seventh_bonus = False
eight_bonus = False
ctx: SC2Context = None
mission_id = 0
game_running: bool = False
mission_completed: bool = False
boni: typing.List[bool]
setup_done: bool
ctx: SC2Context
mission_id: int
can_read_game = False
last_received_update = 0
last_received_update: int = 0
def __init__(self, ctx: SC2Context, mission_id):
self.setup_done = False
self.ctx = ctx
self.mission_id = mission_id
self.boni = [False for _ in range(max_bonus)]
super(ArchipelagoBot, self).__init__()
async def on_step(self, iteration: int):
game_state = 0
if iteration == 0:
if not self.setup_done:
self.setup_done = True
start_items = calculate_items(self.ctx.items_received)
if self.ctx.difficulty_override >= 0:
difficulty = calc_difficulty(self.ctx.difficulty_override)
@@ -511,36 +533,10 @@ class ArchipelagoBot(sc2.bot_ai.BotAI):
self.last_received_update = len(self.ctx.items_received)
else:
if self.ctx.announcement_pos < len(self.ctx.announcements):
index = 0
message = ""
while index < len(self.ctx.announcements[self.ctx.announcement_pos]):
message += self.ctx.announcements[self.ctx.announcement_pos][index]["text"]
index += 1
index = 0
start_rem_pos = -1
# Remove unneeded [Color] tags
while index < len(message):
if message[index] == '[':
start_rem_pos = index
index += 1
elif message[index] == ']' and start_rem_pos > -1:
temp_msg = ""
if start_rem_pos > 0:
temp_msg = message[:start_rem_pos]
if index < len(message) - 1:
temp_msg += message[index + 1:]
message = temp_msg
index += start_rem_pos - index
start_rem_pos = -1
else:
index += 1
if not self.ctx.announcements.empty():
message = self.ctx.announcements.get(timeout=1)
await self.chat_send("SendMessage " + message)
self.ctx.announcement_pos += 1
self.ctx.announcements.task_done()
# Archipelago reads the health
for unit in self.all_own_units():
@@ -568,169 +564,97 @@ class ArchipelagoBot(sc2.bot_ai.BotAI):
if game_state & (1 << 1) and not self.mission_completed:
if self.mission_id != 29:
print("Mission Completed")
await self.ctx.send_msgs([
{"cmd": 'LocationChecks', "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id]}])
await self.ctx.send_msgs(
[{"cmd": 'LocationChecks',
"locations": [SC2WOL_LOC_ID_OFFSET + victory_modulo * self.mission_id]}])
self.mission_completed = True
else:
print("Game Complete")
await self.ctx.send_msgs([{"cmd": 'StatusUpdate', "status": ClientStatus.CLIENT_GOAL}])
self.mission_completed = True
if game_state & (1 << 2) and not self.first_bonus:
print("1st Bonus Collected")
await self.ctx.send_msgs(
[{"cmd": 'LocationChecks',
"locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 1]}])
self.first_bonus = True
if not self.second_bonus and game_state & (1 << 3):
print("2nd Bonus Collected")
await self.ctx.send_msgs(
[{"cmd": 'LocationChecks',
"locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 2]}])
self.second_bonus = True
if not self.third_bonus and game_state & (1 << 4):
print("3rd Bonus Collected")
await self.ctx.send_msgs(
[{"cmd": 'LocationChecks',
"locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 3]}])
self.third_bonus = True
if not self.fourth_bonus and game_state & (1 << 5):
print("4th Bonus Collected")
await self.ctx.send_msgs(
[{"cmd": 'LocationChecks',
"locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 4]}])
self.fourth_bonus = True
if not self.fifth_bonus and game_state & (1 << 6):
print("5th Bonus Collected")
await self.ctx.send_msgs(
[{"cmd": 'LocationChecks',
"locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 5]}])
self.fifth_bonus = True
if not self.sixth_bonus and game_state & (1 << 7):
print("6th Bonus Collected")
await self.ctx.send_msgs(
[{"cmd": 'LocationChecks',
"locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 6]}])
self.sixth_bonus = True
if not self.seventh_bonus and game_state & (1 << 8):
print("6th Bonus Collected")
await self.ctx.send_msgs(
[{"cmd": 'LocationChecks',
"locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 7]}])
self.seventh_bonus = True
if not self.eight_bonus and game_state & (1 << 9):
print("6th Bonus Collected")
await self.ctx.send_msgs(
[{"cmd": 'LocationChecks',
"locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 8]}])
self.eight_bonus = True
for x, completed in enumerate(self.boni):
if not completed and game_state & (1 << (x + 2)):
await self.ctx.send_msgs(
[{"cmd": 'LocationChecks',
"locations": [SC2WOL_LOC_ID_OFFSET + victory_modulo * self.mission_id + x + 1]}])
self.boni[x] = True
else:
await self.chat_send("LostConnection - Lost connection to game.")
def calc_objectives_completed(mission, missions_info, locations_done, unfinished_locations, ctx):
objectives_complete = 0
if missions_info[mission].extra_locations > 0:
for i in range(missions_info[mission].extra_locations):
if (missions_info[mission].id * 100 + SC2WOL_LOC_ID_OFFSET + i) in locations_done:
objectives_complete += 1
else:
unfinished_locations[mission].append(ctx.location_names[
missions_info[mission].id * 100 + SC2WOL_LOC_ID_OFFSET + i])
return objectives_complete
else:
return -1
def request_unfinished_missions(locations_done, location_table, ui, ctx):
if location_table:
def request_unfinished_missions(ctx: SC2Context):
if ctx.mission_req_table:
message = "Unfinished Missions: "
unlocks = initialize_blank_mission_dict(location_table)
unfinished_locations = initialize_blank_mission_dict(location_table)
unlocks = initialize_blank_mission_dict(ctx.mission_req_table)
unfinished_locations = initialize_blank_mission_dict(ctx.mission_req_table)
unfinished_missions = calc_unfinished_missions(locations_done, location_table, ctx, unlocks=unlocks,
unfinished_locations=unfinished_locations)
_, unfinished_missions = calc_unfinished_missions(ctx, unlocks=unlocks)
message += ", ".join(f"{mark_up_mission_name(mission, location_table, ui,unlocks)}[{location_table[mission].id}] " +
message += ", ".join(f"{mark_up_mission_name(ctx, mission, unlocks)}[{ctx.mission_req_table[mission].id}] " +
mark_up_objectives(
f"[{unfinished_missions[mission]}/{location_table[mission].extra_locations}]",
f"[{len(unfinished_missions[mission])}/"
f"{sum(1 for _ in ctx.locations_for_mission(mission))}]",
ctx, unfinished_locations, mission)
for mission in unfinished_missions)
if ui:
ui.log_panels['All'].on_message_markup(message)
ui.log_panels['Starcraft2'].on_message_markup(message)
if ctx.ui:
ctx.ui.log_panels['All'].on_message_markup(message)
ctx.ui.log_panels['Starcraft2'].on_message_markup(message)
else:
sc2_logger.info(message)
else:
sc2_logger.warning("No mission table found, you are likely not connected to a server.")
def calc_unfinished_missions(locations_done, locations, ctx, unlocks=None, unfinished_locations=None,
available_missions=[]):
def calc_unfinished_missions(ctx: SC2Context, unlocks=None):
unfinished_missions = []
locations_completed = []
if not unlocks:
unlocks = initialize_blank_mission_dict(locations)
unlocks = initialize_blank_mission_dict(ctx.mission_req_table)
if not unfinished_locations:
unfinished_locations = initialize_blank_mission_dict(locations)
if len(available_missions) > 0:
available_missions = []
available_missions.extend(calc_available_missions(locations_done, locations, unlocks))
available_missions = calc_available_missions(ctx, unlocks)
for name in available_missions:
if not locations[name].extra_locations == -1:
objectives_completed = calc_objectives_completed(name, locations, locations_done, unfinished_locations, ctx)
if objectives_completed < locations[name].extra_locations:
objectives = set(ctx.locations_for_mission(name))
if objectives:
objectives_completed = ctx.checked_locations & objectives
if len(objectives_completed) < len(objectives):
unfinished_missions.append(name)
locations_completed.append(objectives_completed)
else:
else: # infer that this is the final mission as it has no objectives
unfinished_missions.append(name)
locations_completed.append(-1)
return {unfinished_missions[i]: locations_completed[i] for i in range(len(unfinished_missions))}
return available_missions, dict(zip(unfinished_missions, locations_completed))
def is_mission_available(mission_id_to_check, locations_done, locations):
unfinished_missions = calc_available_missions(locations_done, locations)
def is_mission_available(ctx: SC2Context, mission_id_to_check):
unfinished_missions = calc_available_missions(ctx)
return any(mission_id_to_check == locations[mission].id for mission in unfinished_missions)
return any(mission_id_to_check == ctx.mission_req_table[mission].id for mission in unfinished_missions)
def mark_up_mission_name(mission, location_table, ui, unlock_table):
def mark_up_mission_name(ctx: SC2Context, mission, unlock_table):
"""Checks if the mission is required for game completion and adds '*' to the name to mark that."""
if location_table[mission].completion_critical:
if ui:
if ctx.mission_req_table[mission].completion_critical:
if ctx.ui:
message = "[color=AF99EF]" + mission + "[/color]"
else:
message = "*" + mission + "*"
else:
message = mission
if ui:
if ctx.ui:
unlocks = unlock_table[mission]
if len(unlocks) > 0:
pre_message = f"[ref={list(location_table).index(mission)}|Unlocks: "
pre_message += ", ".join(f"{unlock}({location_table[unlock].id})" for unlock in unlocks)
pre_message = f"[ref={list(ctx.mission_req_table).index(mission)}|Unlocks: "
pre_message += ", ".join(f"{unlock}({ctx.mission_req_table[unlock].id})" for unlock in unlocks)
pre_message += f"]"
message = pre_message + message + "[/ref]"
@@ -743,7 +667,7 @@ def mark_up_objectives(message, ctx, unfinished_locations, mission):
if ctx.ui:
locations = unfinished_locations[mission]
pre_message = f"[ref={list(ctx.mission_req_table).index(mission)+30}|"
pre_message = f"[ref={list(ctx.mission_req_table).index(mission) + 30}|"
pre_message += "<br>".join(location for location in locations)
pre_message += f"]"
formatted_message = pre_message + message + "[/ref]"
@@ -751,90 +675,91 @@ def mark_up_objectives(message, ctx, unfinished_locations, mission):
return formatted_message
def request_available_missions(locations_done, location_table, ui):
if location_table:
def request_available_missions(ctx: SC2Context):
if ctx.mission_req_table:
message = "Available Missions: "
# Initialize mission unlock table
unlocks = initialize_blank_mission_dict(location_table)
unlocks = initialize_blank_mission_dict(ctx.mission_req_table)
missions = calc_available_missions(locations_done, location_table, unlocks)
missions = calc_available_missions(ctx, unlocks)
message += \
", ".join(f"{mark_up_mission_name(mission, location_table, ui, unlocks)}[{location_table[mission].id}]"
", ".join(f"{mark_up_mission_name(ctx, mission, unlocks)}"
f"[{ctx.mission_req_table[mission].id}]"
for mission in missions)
if ui:
ui.log_panels['All'].on_message_markup(message)
ui.log_panels['Starcraft2'].on_message_markup(message)
if ctx.ui:
ctx.ui.log_panels['All'].on_message_markup(message)
ctx.ui.log_panels['Starcraft2'].on_message_markup(message)
else:
sc2_logger.info(message)
else:
sc2_logger.warning("No mission table found, you are likely not connected to a server.")
def calc_available_missions(locations_done, locations, unlocks=None):
def calc_available_missions(ctx: SC2Context, unlocks=None):
available_missions = []
missions_complete = 0
# Get number of missions completed
for loc in locations_done:
if loc % 100 == 0:
for loc in ctx.checked_locations:
if loc % victory_modulo == 0:
missions_complete += 1
for name in locations:
for name in ctx.mission_req_table:
# Go through the required missions for each mission and fill up unlock table used later for hover-over tooltips
if unlocks:
for unlock in locations[name].required_world:
unlocks[list(locations)[unlock-1]].append(name)
for unlock in ctx.mission_req_table[name].required_world:
unlocks[list(ctx.mission_req_table)[unlock - 1]].append(name)
if mission_reqs_completed(name, missions_complete, locations_done, locations):
if mission_reqs_completed(ctx, name, missions_complete):
available_missions.append(name)
return available_missions
def mission_reqs_completed(location_to_check, missions_complete, locations_done, locations):
def mission_reqs_completed(ctx: SC2Context, mission_name: str, missions_complete):
"""Returns a bool signifying if the mission has all requirements complete and can be done
Keyword arguments:
Arguments:
ctx -- instance of SC2Context
locations_to_check -- the mission string name to check
missions_complete -- an int of how many missions have been completed
locations_done -- a list of the location ids that have been complete
locations -- a dict of MissionInfo for mission requirements for this world"""
if len(locations[location_to_check].required_world) >= 1:
"""
if len(ctx.mission_req_table[mission_name].required_world) >= 1:
# A check for when the requirements are being or'd
or_success = False
# Loop through required missions
for req_mission in locations[location_to_check].required_world:
for req_mission in ctx.mission_req_table[mission_name].required_world:
req_success = True
# Check if required mission has been completed
if not (locations[list(locations)[req_mission-1]].id * 100 + SC2WOL_LOC_ID_OFFSET) in locations_done:
if not locations[location_to_check].or_requirements:
if not (ctx.mission_req_table[list(ctx.mission_req_table)[req_mission - 1]].id *
victory_modulo + SC2WOL_LOC_ID_OFFSET) in ctx.checked_locations:
if not ctx.mission_req_table[mission_name].or_requirements:
return False
else:
req_success = False
# Recursively check required mission to see if it's requirements are met, in case !collect has been done
if not mission_reqs_completed(list(locations)[req_mission-1], missions_complete, locations_done,
locations):
if not locations[location_to_check].or_requirements:
if not mission_reqs_completed(ctx, list(ctx.mission_req_table)[req_mission - 1], missions_complete):
if not ctx.mission_req_table[mission_name].or_requirements:
return False
else:
req_success = False
# If requirement check succeeded mark or as satisfied
if locations[location_to_check].or_requirements and req_success:
if ctx.mission_req_table[mission_name].or_requirements and req_success:
or_success = True
if locations[location_to_check].or_requirements:
if ctx.mission_req_table[mission_name].or_requirements:
# Return false if or requirements not met
if not or_success:
return False
# Check number of missions
if missions_complete >= locations[location_to_check].number:
if missions_complete >= ctx.mission_req_table[mission_name].number:
return True
else:
return False
@@ -929,7 +854,7 @@ class DllDirectory:
self.set(self._old)
@staticmethod
def get() -> str:
def get() -> typing.Optional[str]:
if sys.platform == "win32":
n = ctypes.windll.kernel32.GetDllDirectoryW(0, None)
buf = ctypes.create_unicode_buffer(n)

View File

@@ -35,7 +35,7 @@ class Version(typing.NamedTuple):
build: int
__version__ = "0.3.4"
__version__ = "0.3.5"
version_tuple = tuplize_version(__version__)
is_linux = sys.platform.startswith("linux")
@@ -619,7 +619,7 @@ def title_sorted(data: typing.Sequence, key=None, ignore: typing.Set = frozenset
def sorter(element: str) -> str:
parts = element.split(maxsplit=1)
if parts[0].lower() in ignore:
return parts[1]
return parts[1].lower()
else:
return element
return element.lower()
return sorted(data, key=lambda i: sorter(key(i)) if key else sorter(i))

View File

@@ -12,7 +12,7 @@ ModuleUpdate.update()
# in case app gets imported by something like gunicorn
import Utils
Utils.local_path.cached_path = os.path.dirname(__file__)
Utils.local_path.cached_path = os.path.dirname(__file__) or "." # py3.8 is not abs. remove "." when dropping 3.8
from WebHostLib import register, app as raw_app
from waitress import serve
@@ -104,7 +104,7 @@ def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]]
for games in data:
if 'Archipelago' in games['gameTitle']:
generic_data = data.pop(data.index(games))
sorted_data = [generic_data] + Utils.title_sorted(data, key=lambda entry: entry["gameTitle"].lower())
sorted_data = [generic_data] + Utils.title_sorted(data, key=lambda entry: entry["gameTitle"])
json.dump(sorted_data, json_target, indent=2, ensure_ascii=False)
return sorted_data

View File

@@ -1,7 +1,7 @@
{% extends 'pageWrapper.html' %}
{% block head %}
<title>Player Settings</title>
<title>Supported Games</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/supportedGames.css") }}" />
{% endblock %}

View File

@@ -13,9 +13,18 @@ These steps should be followed in order to establish a gameplay connection with
In the case that the client does not authenticate properly and receives a [ConnectionRefused](#ConnectionRefused) then the server will maintain the connection and allow for follow-up [Connect](#Connect) packet.
There are libraries available that implement this network protocol in [Python](https://github.com/ArchipelagoMW/Archipelago/blob/main/CommonClient.py), [Java](https://github.com/ArchipelagoMW/Archipelago.MultiClient.Java), [.Net](https://github.com/ArchipelagoMW/Archipelago.MultiClient.Net) and [C++](https://github.com/black-sliver/apclientpp)
There are also a number of community-supported libraries available that implement this network protocol to make integrating with Archipelago easier.
For Super Nintendo games there are clients available in either [Node](https://github.com/ArchipelagoMW/SuperNintendoClient) or [Python](https://github.com/ArchipelagoMW/Archipelago/blob/main/SNIClient.py), There are also game specific clients available for [The Legend of Zelda: Ocarina of Time](https://github.com/ArchipelagoMW/Z5Client) or [Final Fantasy 1](https://github.com/ArchipelagoMW/Archipelago/blob/main/FF1Client.py)
| Language/Runtime | Project | Remarks |
|-------------------------------|----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------|
| Python | [Archipelago CommonClient](https://github.com/ArchipelagoMW/Archipelago/blob/main/CommonClient.py) | |
| | [Archipelago SNIClient](https://github.com/ArchipelagoMW/Archipelago/blob/main/SNIClient.py) | For Super Nintendo Game Support; Utilizes [SNI](https://github.com/alttpo/sni). |
| JVM (Java / Kotlin) | [Archipelago.MultiClient.Java](https://github.com/ArchipelagoMW/Archipelago.MultiClient.Java) | |
| .NET (C# / C++ / F# / VB.NET) | [Archipelago.MultiClient.Net](https://www.nuget.org/packages/Archipelago.MultiClient.Net) | |
| C++ | [apclientpp](https://github.com/black-sliver/apclientpp) | almost-header-only |
| | [APCpp](https://github.com/N00byKing/APCpp) | CMake |
| JavaScript / TypeScript | [archipelago.js](https://www.npmjs.com/package/archipelago.js) | Browser and Node.js Supported |
| Haxe | [hxArchipelago](https://lib.haxe.org/p/hxArchipelago) | |
## Synchronizing Items
When the client receives a [ReceivedItems](#ReceivedItems) packet, if the `index` argument does not match the next index that the client expects then it is expected that the client will re-sync items with the server. This can be accomplished by sending the server a [Sync](#Sync) packet and then a [LocationChecks](#LocationChecks) packet.

View File

@@ -56,3 +56,8 @@ SNI is required to use SNIClient. If not integrated into the project, it has to
You can get the latest SNI release at [SNI Github releases](https://github.com/alttpo/sni/releases).
It should be dropped as "SNI" into the root folder of the project. Alternatively, you can point the sni setting in
host.yaml at your SNI folder.
## Running tests
Run `pip install pytest pytest-subtests`, then use your IDE to run tests or run `pytest` from the source folder.

View File

@@ -52,3 +52,13 @@ class TestIDs(unittest.TestCase):
else:
for location_id in world_type.location_id_to_name:
self.assertGreater(location_id, 0)
def testDuplicateItemIDs(self):
for gamename, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game=gamename):
self.assertEqual(len(world_type.item_id_to_name), len(world_type.item_name_to_id))
def testDuplicateLocationIDs(self):
for gamename, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game=gamename):
self.assertEqual(len(world_type.location_id_to_name), len(world_type.location_name_to_id))

View File

@@ -212,9 +212,7 @@ def parse_arguments(argv, no_defaults=False):
Alternatively, can be a ALttP Rom patched with a Link
sprite that will be extracted.
''')
parser.add_argument('--gui', help='Launch the GUI', action='store_true')
parser.add_argument('--enemizercli', default=defval('EnemizerCLI/EnemizerCLI.Core'))
parser.add_argument('--shufflebosses', default=defval('none'), choices=['none', 'basic', 'normal', 'chaos',
"singularity"])

View File

@@ -34,7 +34,7 @@ from worlds.alttp.Text import KingsReturn_texts, Sanctuary_texts, Kakariko_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, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen
from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen, parse_yaml
from worlds.alttp.Items import ItemFactory, item_table, item_name_groups, progression_items
from worlds.alttp.EntranceShuffle import door_addresses
from worlds.alttp.Options import smallkey_shuffle
@@ -551,18 +551,22 @@ class Sprite():
Sprite.base_data = Sprite.sprite + Sprite.palette + Sprite.glove_palette
def from_ap_sprite(self, filedata):
filedata = filedata.decode("utf-8-sig")
import yaml
obj = yaml.safe_load(filedata)
if obj["min_format_version"] > 1:
raise Exception("Sprite file requires an updated reader.")
self.author_name = obj["author"]
self.name = obj["name"]
if obj["data"]: # skip patching for vanilla content
data = bsdiff4.patch(Sprite.base_data, obj["data"])
self.sprite = data[:self.sprite_size]
self.palette = data[self.sprite_size:self.palette_size]
self.glove_palette = data[self.sprite_size + self.palette_size:]
# noinspection PyBroadException
try:
obj = parse_yaml(filedata.decode("utf-8-sig"))
if obj["min_format_version"] > 1:
raise Exception("Sprite file requires an updated reader.")
self.author_name = obj["author"]
self.name = obj["name"]
if obj["data"]: # skip patching for vanilla content
data = bsdiff4.patch(Sprite.base_data, obj["data"])
self.sprite = data[:self.sprite_size]
self.palette = data[self.sprite_size:self.palette_size]
self.glove_palette = data[self.sprite_size + self.palette_size:]
except Exception:
logger = logging.getLogger("apsprite")
logger.exception("Error parsing apsprite file")
self.valid = False
@property
def author_game_display(self) -> str:
@@ -659,7 +663,7 @@ class Sprite():
@staticmethod
def parse_zspr(filedata, expected_kind):
logger = logging.getLogger('ZSPR')
logger = logging.getLogger("ZSPR")
headerstr = "<4xBHHIHIHH6x"
headersize = struct.calcsize(headerstr)
if len(filedata) < headersize:
@@ -667,7 +671,7 @@ class Sprite():
version, csum, icsum, sprite_offset, sprite_size, palette_offset, palette_size, kind = struct.unpack_from(
headerstr, filedata)
if version not in [1]:
logger.error('Error parsing ZSPR file: Version %g not supported', version)
logger.error("Error parsing ZSPR file: Version %g not supported", version)
return None
if kind != expected_kind:
return None
@@ -676,36 +680,42 @@ class Sprite():
stream.seek(headersize)
def read_utf16le(stream):
"Decodes a null-terminated UTF-16_LE string of unknown size from a stream"
"""Decodes a null-terminated UTF-16_LE string of unknown size from a stream"""
raw = bytearray()
while True:
char = stream.read(2)
if char in [b'', b'\x00\x00']:
if char in [b"", b"\x00\x00"]:
break
raw += char
return raw.decode('utf-16_le')
return raw.decode("utf-16_le")
sprite_name = read_utf16le(stream)
author_name = read_utf16le(stream)
author_credits_name = stream.read().split(b"\x00", 1)[0].decode()
# noinspection PyBroadException
try:
sprite_name = read_utf16le(stream)
author_name = read_utf16le(stream)
author_credits_name = stream.read().split(b"\x00", 1)[0].decode()
# Ignoring the Author Rom name for the time being.
# Ignoring the Author Rom name for the time being.
real_csum = sum(filedata) % 0x10000
if real_csum != csum or real_csum ^ 0xFFFF != icsum:
logger.warning('ZSPR file has incorrect checksum. It may be corrupted.')
real_csum = sum(filedata) % 0x10000
if real_csum != csum or real_csum ^ 0xFFFF != icsum:
logger.warning("ZSPR file has incorrect checksum. It may be corrupted.")
sprite = filedata[sprite_offset:sprite_offset + sprite_size]
palette = filedata[palette_offset:palette_offset + palette_size]
sprite = filedata[sprite_offset:sprite_offset + sprite_size]
palette = filedata[palette_offset:palette_offset + palette_size]
if len(sprite) != sprite_size or len(palette) != palette_size:
logger.error('Error parsing ZSPR file: Unexpected end of file')
if len(sprite) != sprite_size or len(palette) != palette_size:
logger.error("Error parsing ZSPR file: Unexpected end of file")
return None
return sprite, palette, sprite_name, author_name, author_credits_name
except Exception:
logger.exception("Error parsing ZSPR file")
return None
return (sprite, palette, sprite_name, author_name, author_credits_name)
def decode_palette(self):
"Returns the palettes as an array of arrays of 15 colors"
"""Returns the palettes as an array of arrays of 15 colors"""
def array_chunk(arr, size):
return list(zip(*[iter(arr)] * size))

View File

@@ -4,6 +4,7 @@ import random
import threading
import typing
import Utils
from BaseClasses import Item, CollectionState, Tutorial
from .Dungeons import create_dungeons
from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect
@@ -136,6 +137,10 @@ class ALTTPWorld(World):
create_items = generate_itempool
enemizer_path: str = Utils.get_options()["generator"]["enemizer_path"] \
if os.path.isabs(Utils.get_options()["generator"]["enemizer_path"]) \
else Utils.local_path(Utils.get_options()["generator"]["enemizer_path"])
def __init__(self, *args, **kwargs):
self.dungeon_local_item_names = set()
self.dungeon_specific_item_names = set()
@@ -150,12 +155,12 @@ class ALTTPWorld(World):
raise FileNotFoundError(rom_file)
def generate_early(self):
if self.use_enemizer():
check_enemizer(self.enemizer_path)
player = self.player
world = self.world
if self.use_enemizer():
check_enemizer(world.enemizer)
# system for sharing ER layouts
self.er_seed = str(world.random.randint(0, 2 ** 64))
@@ -360,7 +365,7 @@ class ALTTPWorld(World):
patch_rom(world, rom, player, use_enemizer)
if use_enemizer:
patch_enemizer(world, player, rom, world.enemizer, output_directory)
patch_enemizer(world, player, rom, self.enemizer_path, output_directory)
if world.is_race:
patch_race_rom(rom, world, player)

View File

@@ -502,6 +502,10 @@ def patch_rom(world, rom, player, active_level_list):
# Make Swanky free
rom.write_byte(0x348C48, 0x00)
rom.write_bytes(0x34AB70, bytearray([0xEA, 0xEA]))
rom.write_bytes(0x34ABF7, bytearray([0xEA, 0xEA]))
rom.write_bytes(0x34ACD0, bytearray([0xEA, 0xEA]))
# Banana Bird Costs
if world.goal[player] == "banana_bird_hunt":
banana_bird_cost = math.floor(world.number_of_banana_birds[player] * world.percentage_of_banana_birds[player] / 100.0)

View File

@@ -249,6 +249,10 @@ script.on_event(defines.events.on_player_main_inventory_changed, update_player_e
function add_samples(force, name, count)
local function add_to_table(t)
if count <= 0 then
-- Fixes a bug with single craft, if a recipe gives 0 of a given item.
return
end
t[name] = (t[name] or 0) + count
end
-- Add to global table of earned samples for future new players

View File

@@ -1,3 +1,21 @@
-- Find out if more than one AP mod is loaded, and if so, error out.
function mod_is_AP(str)
-- lua string.match is way more restrictive than regex. Regex would be "^AP-W?\d{20}-P[1-9]\d*-.+$"
local result = string.match(str, "^AP%-W?%d%d%d%d%d%d%d%d%d%d%d%d%d%d%d%d%d%d%d%d%-P[1-9]%d-%-.+$")
if result ~= nil then
log("Archipelago Mod: " .. result .. " is loaded.")
end
return result ~= nil
end
local ap_mod_count = 0
for name, _ in pairs(mods) do
if mod_is_AP(name) then
ap_mod_count = ap_mod_count + 1
if ap_mod_count > 1 then
error("More than one Archipelago Factorio mod is loaded.")
end
end
end
data:extend({
{
type = "bool-setting",

View File

@@ -7,9 +7,9 @@ config file.
## What does randomization do to this game?
Recipes are removed from the crafting book and shuffled into the item pool. It can also optionally change which
Some recipes are locked from being able to be crafted and shuffled into the item pool. It can also optionally change which
structures appear in each dimension. Crafting recipes are re-learned when they are received from other players as item
checks, and occasionally when completing your own achievements.
checks, and occasionally when completing your own achievements. See below for which recipes are shuffled.
## What is considered a location check in minecraft?
@@ -25,3 +25,86 @@ inventory directly.
Victory is achieved when the player kills the Ender Dragon, enters the portal in The End, and completes the credits
sequence either by skipping it or watching hit play out.
## Which recipes are locked?
* Archery
* Bow
* Arrow
* Crossbow
* Brewing
* Blaze Powder
* Brewing Stand
* Enchanting
* Enchanting Table
* Bookshelf
* Bucket
* Flint & Steel
* All Beds
* Bottles
* Shield
* Fishing Rod
* Fishing Rod
* Carrot on a Stick
* Warped Fungus on a Stick
* Campfire
* Campfire
* Soul Campfire
* Spyglass
* Lead
* Progressive Weapons
* Tier I
* Stone Sword
* Stone Axe
* Tier II
* Iron Sword
* Iron Axe
* Tier III
* Diamond Sword
* Diamond Axe
* Progessive Tools
* Tier I
* Stone Shovel
* Stone Hoe
* Tier II
* Iron Shovel
* Iron Hoe
* Tier III
* Diamond Shovel
* Diamond Hoe
* Netherite Ingot
* Progressive Armor
* Tier I
* Iron Helmet
* Iron Chestplate
* Iron Leggings
* Iron Boots
* Tier II
* Diamond Helmet
* Diamond Chestplate
* Diamond Leggings
* Diamond Boots
* Progressive Resource Crafting
* Tier I
* Iron Ingot from Nuggets
* Iron Nugget
* Gold Ingot from Nuggets
* Gold Nugget
* Furnace
* Blast Furnace
* Tier II
* Redstone
* Redstone Block
* Glowstone
* Iron Ingot from Iron Block
* Iron Block
* Gold Ingot from Gold Block
* Gold Block
* Diamond
* Diamond Block
* Netherite Block
* Netherite Ingot from Netherite Block
* Anvil
* Emerald
* Emerald Block
* Copper Block

View File

@@ -1388,6 +1388,10 @@ def get_pool_core(world):
remove_junk_pool = list(remove_junk_pool) + ['Recovery Heart', 'Bombs (20)', 'Arrows (30)', 'Ice Trap']
junk_candidates = [item for item in pool if item in remove_junk_pool]
if len(pending_junk_pool) > len(junk_candidates):
excess = len(pending_junk_pool) - len(junk_candidates)
if world.triforce_hunt:
raise RuntimeError(f"Items in the pool for player {world.player} exceed locations. Add {excess} location(s) or remove {excess} triforce piece(s).")
while pending_junk_pool:
pending_item = pending_junk_pool.pop()
if not junk_candidates:

View File

@@ -49,7 +49,6 @@ class OOTItem(Item):
self.type = type
self.index = index
self.special = special or {}
self.looks_like_item = None
self.price = special.get('price', None) if special else None
self.internal = False

View File

@@ -158,12 +158,12 @@ class TriforceGoal(Range):
"""Number of Triforce pieces required to complete the game."""
display_name = "Required Triforce Pieces"
range_start = 1
range_end = 100
range_end = 80
default = 20
class ExtraTriforces(Range):
"""Percentage of additional Triforce pieces in the pool, separate from the item pool setting."""
"""Percentage of additional Triforce pieces in the pool. With high numbers, you may need to randomize additional locations to have enough items."""
display_name = "Percentage of Extra Triforce Pieces"
range_start = 0
range_end = 100

View File

@@ -1844,7 +1844,7 @@ def write_rom_item(rom, item_id, item):
def get_override_table(world):
return list(filter(lambda val: val != None, map(partial(get_override_entry, world.player), world.world.get_filled_locations(world.player))))
return list(filter(lambda val: val != None, map(partial(get_override_entry, world), world.world.get_filled_locations(world.player))))
override_struct = struct.Struct('>xBBBHBB') # match override_t in get_items.c
@@ -1852,10 +1852,10 @@ def get_override_table_bytes(override_table):
return b''.join(sorted(itertools.starmap(override_struct.pack, override_table)))
def get_override_entry(player_id, location):
def get_override_entry(ootworld, location):
scene = location.scene
default = location.default
player_id = 0 if player_id == location.item.player else min(location.item.player, 255)
player_id = 0 if ootworld.player == location.item.player else min(location.item.player, 255)
if location.item.game != 'Ocarina of Time':
# This is an AP sendable. It's guaranteed to not be None.
if location.item.advancement:
@@ -1869,7 +1869,7 @@ def get_override_entry(player_id, location):
if location.item.trap:
item_id = 0x7C # Ice Trap ID, to get "X is a fool" message
looks_like_item_id = location.item.looks_like_item.index
looks_like_item_id = ootworld.trap_appearances[location.address].index
else:
looks_like_item_id = 0
@@ -2091,7 +2091,8 @@ def get_locked_doors(rom, world):
return [0x00D4 + scene * 0x1C + 0x04 + flag_byte, flag_bits]
# If boss door, set the door's unlock flag
if (world.shuffle_bosskeys == 'remove' and scene != 0x0A) or (world.shuffle_ganon_bosskey == 'remove' and scene == 0x0A):
if (world.shuffle_bosskeys == 'remove' and scene != 0x0A) or (
world.shuffle_ganon_bosskey == 'remove' and scene == 0x0A and not world.triforce_hunt):
if actor_id == 0x002E and actor_type == 0x05:
return [0x00D4 + scene * 0x1C + 0x04 + flag_byte, flag_bits]
@@ -2109,23 +2110,20 @@ def place_shop_items(rom, world, shop_items, messages, locations, init_shop_id=F
rom.write_int16(location.address1, location.item.index)
else:
if location.item.trap:
item_display = location.item.looks_like_item
elif location.item.game != "Ocarina of Time":
item_display = location.item
if location.item.advancement:
item_display.index = 0xCB
else:
item_display.index = 0xCC
item_display.special = {}
item_display = world.trap_appearances[location.address]
else:
item_display = location.item
# bottles in shops should look like empty bottles
# so that that are different than normal shop refils
if 'shop_object' in item_display.special:
rom_item = read_rom_item(rom, item_display.special['shop_object'])
if location.item.trap or location.item.game == "Ocarina of Time":
if 'shop_object' in item_display.special:
rom_item = read_rom_item(rom, item_display.special['shop_object'])
else:
rom_item = read_rom_item(rom, item_display.index)
else:
rom_item = read_rom_item(rom, item_display.index)
display_index = 0xCB if location.item.advancement else 0xCC
rom_item = read_rom_item(rom, display_index)
shop_objs.add(rom_item['object_id'])
shop_id = world.current_shop_id

View File

@@ -178,6 +178,10 @@ class OOTWorld(World):
if self.skip_child_zelda:
self.shuffle_weird_egg = False
# Ganon boss key should not be in itempool in triforce hunt
if self.triforce_hunt:
self.shuffle_ganon_bosskey = 'remove'
# Determine skipped trials in GT
# This needs to be done before the logic rules in GT are parsed
trial_list = ['Forest', 'Fire', 'Water', 'Spirit', 'Shadow', 'Light']
@@ -803,9 +807,10 @@ class OOTWorld(World):
with i_o_limiter:
# Make traps appear as other random items
ice_traps = [loc.item for loc in self.get_locations() if loc.item.trap]
for trap in ice_traps:
trap.looks_like_item = self.create_item(self.world.slot_seeds[self.player].choice(self.fake_items).name)
trap_location_ids = [loc.address for loc in self.get_locations() if loc.item.trap]
self.trap_appearances = {}
for loc_id in trap_location_ids:
self.trap_appearances[loc_id] = self.create_item(self.world.slot_seeds[self.player].choice(self.fake_items).name)
# Seed hint RNG, used for ganon text lines also
self.hint_rng = self.world.slot_seeds[self.player]

View File

@@ -69,8 +69,8 @@ vanilla_mission_req_table = {
"Zero Hour": MissionInfo(3, 4, [2], "Mar Sara", completion_critical=True),
"Evacuation": MissionInfo(4, 4, [3], "Colonist"),
"Outbreak": MissionInfo(5, 3, [4], "Colonist"),
"Safe Haven": MissionInfo(6, 1, [5], "Colonist", number=7),
"Haven's Fall": MissionInfo(7, 1, [5], "Colonist", number=7),
"Safe Haven": MissionInfo(6, 4, [5], "Colonist", number=7),
"Haven's Fall": MissionInfo(7, 4, [5], "Colonist", number=7),
"Smash and Grab": MissionInfo(8, 5, [3], "Artifact", completion_critical=True),
"The Dig": MissionInfo(9, 4, [8], "Artifact", number=8, completion_critical=True),
"The Moebius Factor": MissionInfo(10, 9, [9], "Artifact", number=11, completion_critical=True),

View File

@@ -43,6 +43,7 @@ class SC2WoLWorld(World):
locked_locations: typing.List[str]
location_cache: typing.List[Location]
mission_req_table = {}
required_client_version = 0, 3, 5
def __init__(self, world: MultiWorld, player: int):
super(SC2WoLWorld, self).__init__(world, player)

View File

@@ -1,14 +0,0 @@
from worlds.sm.variaRandomizer.rando.Items import ItemManager
items_start_id = 83000
def gen_special_id():
special_id_value_start = 32
while True:
yield special_id_value_start
special_id_value_start += 1
gen_run = gen_special_id()
lookup_id_to_name = dict((items_start_id + (value.Id if value.Id != None else next(gen_run)), value.Name) for key, value in ItemManager.Items.items())
lookup_name_to_id = {item_name: item_id for item_id, item_name in lookup_id_to_name.items()}

View File

@@ -1,14 +0,0 @@
from worlds.sm.variaRandomizer.graph.location import locationsDict
locations_start_id = 82000
def gen_boss_id():
boss_id_value_start = 256
while True:
yield boss_id_value_start
boss_id_value_start += 1
gen_run = gen_boss_id()
lookup_id_to_name = dict((locations_start_id + (value.Id if value.Id != None else next(gen_run)), key) for key, value in locationsDict.items())
lookup_name_to_id = {location_name: location_id for location_id, location_name in lookup_id_to_name.items()}

View File

@@ -11,8 +11,6 @@ from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils
logger = logging.getLogger("Super Metroid")
from .Locations import lookup_name_to_id as locations_lookup_name_to_id
from .Items import lookup_name_to_id as items_lookup_name_to_id
from .Regions import create_regions
from .Rules import set_rules, add_entrance_rule
from .Options import sm_options
@@ -68,6 +66,8 @@ class SMWeb(WebWorld):
["Farrak Kilhn"]
)]
locations_start_id = 82000
items_start_id = 83000
class SMWorld(World):
"""
@@ -78,12 +78,11 @@ class SMWorld(World):
game: str = "Super Metroid"
topology_present = True
data_version = 1
data_version = 2
option_definitions = sm_options
item_names: Set[str] = frozenset(items_lookup_name_to_id)
location_names: Set[str] = frozenset(locations_lookup_name_to_id)
item_name_to_id = items_lookup_name_to_id
location_name_to_id = locations_lookup_name_to_id
item_name_to_id = {value.Name: items_start_id + value.Id for key, value in ItemManager.Items.items() if value.Id != None}
location_name_to_id = {key: locations_start_id + value.Id for key, value in locationsDict.items() if value.Id != None}
web = SMWeb()
remote_items: bool = False
@@ -701,8 +700,8 @@ class SMWorld(World):
dest.Name) for src, dest in self.variaRando.randoExec.areaGraph.InterAreaTransitions if src.Boss]))
def create_locations(self, player: int):
for name, id in locations_lookup_name_to_id.items():
self.locations[name] = SMLocation(player, name, id)
for name in locationsDict:
self.locations[name] = SMLocation(player, name, self.location_name_to_id.get(name, None))
def create_region(self, world: MultiWorld, player: int, name: str, locations=None, exits=None):

View File

@@ -2,14 +2,14 @@
; generated by asar
[labels]
B8:8026 :neg_1_1
B8:80C1 :neg_1_1
85:B9B4 :neg_1_2
85:B9E6 :neg_1_3
B8:C81F :neg_1_4
B8:C831 :neg_1_5
B8:C843 :neg_1_6
B8:800C :pos_1_0
B8:81DE :pos_1_1
B8:82D7 :pos_1_1
84:FA6B :pos_1_2
84:FA75 :pos_1_3
B8:C862 :pos_1_4
@@ -20,7 +20,7 @@ B8:C87C :pos_1_6
85:990F CLIPLEN_end
85:990C CLIPLEN_no_multi
85:FF1D CLIPSET
B8:80EF COLLECTTANK
B8:81E8 COLLECTTANK
85:FF45 MISCFX
84:8BF2 NORMAL
85:FF4E SETFX
@@ -38,6 +38,11 @@ CE:FF00 config_multiworld
CE:FF08 config_player_id
CE:FF06 config_remote_items
CE:FF02 config_sprite
B8:8119 copy_config_to_sram
B8:80FD copy_memory
B8:8117 copy_memory_done
B8:8109 copy_memory_even
B8:810F copy_memory_loop
84:F894 h_item
84:F8AD i_chozo_item
84:F8B4 i_hidden_item
@@ -46,11 +51,11 @@ B8:885C i_item_setup_shared
B8:8878 i_item_setup_shared_all_items
B8:8883 i_item_setup_shared_alwaysloaded
84:FA79 i_live_pickup
B8:817F i_live_pickup_multiworld
B8:81C4 i_live_pickup_multiworld_end
B8:819B i_live_pickup_multiworld_local_item_or_offworld
B8:81B0 i_live_pickup_multiworld_own_item
B8:81BC i_live_pickup_multiworld_own_item1
B8:8278 i_live_pickup_multiworld
B8:82BD i_live_pickup_multiworld_end
B8:8294 i_live_pickup_multiworld_local_item_or_offworld
B8:82A9 i_live_pickup_multiworld_own_item
B8:82B5 i_live_pickup_multiworld_own_item1
84:FA1E i_load_custom_graphics
84:FA39 i_load_custom_graphics_all_items
84:FA49 i_load_custom_graphics_alwaysloaded
@@ -85,22 +90,27 @@ B8:81BC i_live_pickup_multiworld_own_item1
85:B9CA message_write_placeholders_loop
85:B9DC message_write_placeholders_notfound
85:B9DF message_write_placeholders_value_ok
B8:8092 mw_display_item_sent
B8:80FF mw_handle_queue
B8:8178 mw_handle_queue_end
B8:8101 mw_handle_queue_loop
B8:8151 mw_handle_queue_new_remote_item
B8:816D mw_handle_queue_next
B8:8163 mw_handle_queue_perform_receive
B8:81C8 mw_hook_main_game
B8:818B mw_display_item_sent
B8:81F8 mw_handle_queue
B8:8271 mw_handle_queue_end
B8:81FA mw_handle_queue_loop
B8:824A mw_handle_queue_new_remote_item
B8:8266 mw_handle_queue_next
B8:825C mw_handle_queue_perform_receive
B8:82C1 mw_hook_main_game
B8:8011 mw_init
B8:8044 mw_init_end
B8:8066 mw_init_continuereset
B8:80EA mw_init_end
B8:8000 mw_init_memory
B8:8083 mw_load_sram
B8:80B0 mw_receive_item
B8:80E8 mw_receive_item_end
B8:8070 mw_save_sram
B8:8049 mw_write_message
B8:803B mw_init_reset_sram
B8:8051 mw_init_smstringdata
B8:8174 mw_load_sram
B8:8182 mw_load_sram_done
B8:8185 mw_load_sram_setnewgame
B8:81A9 mw_receive_item
B8:81E1 mw_receive_item_end
B8:8169 mw_save_sram
B8:8142 mw_write_message
84:F888 nonprog_item_eight_palette_indices
89:9200 offworld_graphics_data_item
89:9100 offworld_graphics_data_progression_item
@@ -125,7 +135,7 @@ B8:8049 mw_write_message
84:F96E p_visible_item_end
84:F95B p_visible_item_loop
84:F967 p_visible_item_trigger
B8:81DF patch_load_multiworld
B8:82D8 patch_load_multiworld
84:FA7E perform_item_pickup
84:F886 plm_graphics_entry_offworld_item
84:F87C plm_graphics_entry_offworld_progression_item
@@ -144,17 +154,19 @@ B8:C808 start_item_data_minor
B8:C818 start_item_data_reserve
B8:C856 update_graphic
84:F890 v_item
B8:80EF write_repeated_memory
B8:80F4 write_repeated_memory_loop
[source files]
0000 e25029c5 main.asm
0001 06780555 ../common/nofanfare.asm
0002 e76d1f83 ../common/multiworld.asm
0002 4f9a780e ../common/multiworld.asm
0003 613d24e1 ../common/itemextras.asm
0004 d6616c0c ../common/items.asm
0005 440b54fe ../common/startitem.asm
[rom checksum]
09b134c5
ad81eda1
[addr-to-line mapping]
ff:ffff 0000:00000001
@@ -204,330 +216,423 @@ ff:ffff 0000:00000001
84:8bf2 0001:00000152
84:8bf6 0001:00000153
84:8bf7 0001:00000153
b8:8000 0002:00000019
b8:8002 0002:0000001a
b8:8006 0002:0000001b
b8:8008 0002:0000001c
b8:800c 0002:00000020
b8:800e 0002:00000021
b8:8010 0002:00000022
b8:8011 0002:00000025
b8:8012 0002:00000025
b8:8013 0002:00000025
b8:8014 0002:00000025
b8:8000 0002:0000005a
b8:8002 0002:0000005b
b8:8006 0002:0000005c
b8:8008 0002:0000005d
b8:800c 0002:00000061
b8:800e 0002:00000062
b8:8010 0002:00000063
b8:8011 0002:00000066
b8:8012 0002:00000066
b8:8013 0002:00000066
b8:8014 0002:00000066
b8:8015 0000:00000013
b8:8017 0002:00000029
b8:801b 0002:0000002a
b8:801e 0002:0000002b
b8:8020 0002:0000002d
b8:8023 0002:0000002e
b8:8026 0002:00000031
b8:802a 0002:00000032
b8:802e 0002:00000033
b8:8032 0002:00000034
b8:8036 0002:00000035
b8:8037 0002:00000035
b8:8038 0002:00000036
b8:803b 0002:00000037
b8:803d 0002:00000039
b8:8040 0002:0000003a
b8:8044 0002:0000003d
b8:8045 0002:0000003d
b8:8046 0002:0000003d
b8:8047 0002:0000003d
b8:8048 0002:0000003e
b8:8049 0002:00000043
b8:804a 0002:00000043
b8:804b 0002:00000044
b8:804c 0002:00000044
b8:804d 0002:00000045
b8:8051 0002:00000046
b8:8054 0002:00000046
b8:8055 0002:00000047
b8:8056 0002:00000048
b8:805a 0002:00000049
b8:805b 0002:0000004a
b8:805f 0002:0000004b
b8:8060 0002:0000004c
b8:8064 0002:0000004e
b8:8068 0002:0000004f
b8:8069 0002:00000050
b8:806d 0002:00000051
b8:806e 0002:00000051
b8:806f 0002:00000052
b8:8070 0002:00000055
b8:8071 0002:00000055
b8:8072 0000:00000013
b8:8074 0002:00000057
b8:8078 0002:00000058
b8:807c 0002:00000059
b8:807d 0002:00000059
b8:807e 0002:0000005b
b8:807f 0002:0000005c
b8:8082 0002:0000005d
b8:8083 0002:00000060
b8:8084 0002:00000060
b8:8085 0000:00000013
b8:8087 0002:00000062
b8:808b 0002:00000063
b8:808f 0002:00000064
b8:8090 0002:00000064
b8:8091 0002:00000065
b8:8092 0002:0000006a
b8:8094 0002:0000006b
b8:8096 0002:0000006e
b8:8099 0002:0000006f
b8:809b 0002:00000070
b8:809e 0002:00000071
b8:80a0 0002:00000072
b8:80a3 0002:00000073
b8:80a7 0002:00000074
b8:80a9 0002:00000075
b8:80ab 0002:00000076
b8:80ad 0002:00000077
b8:80af 0002:00000078
b8:80b0 0002:0000007c
b8:80b1 0002:0000007c
b8:80b2 0002:0000007d
b8:80b5 0002:0000007e
b8:80b7 0002:0000007f
b8:80ba 0002:00000080
b8:80bc 0002:00000081
b8:80bd 0002:00000082
b8:80be 0002:00000084
b8:80c1 0002:00000085
b8:80c3 0002:00000086
b8:80c6 0002:00000087
b8:80c7 0002:00000088
b8:80ca 0002:00000089
b8:80cb 0002:00000089
b8:80cc 0002:0000008a
b8:80d0 0002:0000008b
b8:80d1 0002:0000008c
b8:80d4 0002:0000008d
b8:80d8 0002:0000008e
b8:80da 0002:00000090
b8:80dd 0002:00000091
b8:80df 0002:00000092
b8:80e2 0002:00000093
b8:80e4 0002:00000095
b8:80e8 0002:00000097
b8:80ea 0002:00000098
b8:80ec 0002:00000099
b8:80ed 0002:00000099
b8:80ee 0002:0000009a
b8:80ef 0002:000000a5
b8:80f0 0002:000000a6
b8:80f4 0002:000000a7
b8:80f5 0002:000000a8
b8:80f9 0002:000000a9
b8:80fa 0002:000000ab
b8:80fe 0002:000000ac
b8:80ff 0002:000000de
b8:8100 0002:000000de
b8:8101 0002:000000e1
b8:8105 0002:000000e2
b8:8109 0002:000000e3
b8:810b 0002:000000e5
b8:810d 0002:000000e5
b8:810e 0002:000000e8
b8:8112 0002:000000e9
b8:8114 0002:000000ea
b8:8118 0002:000000eb
b8:811a 0002:000000ec
b8:811e 0002:000000ed
b8:8121 0002:000000ee
b8:8123 0002:000000ef
b8:8125 0002:000000f0
b8:8129 0002:000000f1
b8:812b 0002:000000f2
b8:812d 0002:000000f3
b8:8130 0002:000000f4
b8:8133 0002:000000f5
b8:8135 0002:000000f6
b8:813d 0002:000000fa
b8:813e 0002:000000fb
b8:813f 0002:000000fc
b8:8143 0002:000000ff
b8:8147 0002:00000100
b8:814b 0002:00000101
b8:814d 0002:00000103
b8:814e 0002:00000104
b8:814f 0002:00000105
b8:8151 0002:0000010a
b8:8152 0002:0000010b
b8:8156 0002:0000010e
b8:815a 0002:0000010f
b8:815e 0002:00000110
b8:8162 0002:00000111
b8:8163 0002:00000115
b8:8165 0002:00000116
b8:8168 0002:00000117
b8:816a 0002:00000118
b8:816d 0002:0000011b
b8:8171 0002:0000011c
b8:8172 0002:0000011d
b8:8176 0002:0000011f
b8:8178 0002:00000122
b8:817a 0002:00000123
b8:817c 0002:00000124
b8:817d 0002:00000124
b8:817e 0002:00000125
b8:817f 0002:00000129
b8:8180 0002:00000129
b8:8181 0002:00000129
b8:8182 0002:0000012a
b8:8186 0002:0000012b
b8:8189 0002:0000012b
b8:818a 0002:0000012d
b8:818e 0002:0000012e
b8:818f 0002:0000012f
b8:8193 0002:00000130
b8:8196 0002:00000131
b8:8198 0002:00000133
b8:819b 0002:00000136
b8:819f 0002:00000137
b8:81a3 0002:00000138
b8:81a5 0002:0000013a
b8:81a9 0002:0000013b
b8:81aa 0002:0000013d
b8:81ae 0002:0000013e
b8:81b0 0002:00000141
b8:81b4 0002:00000142
b8:81b7 0002:00000143
b8:81b9 0002:00000144
b8:81bc 0002:00000147
b8:81bd 0002:00000148
b8:81be 0002:00000149
b8:81c2 0002:0000014a
b8:81c4 0002:0000014d
b8:81c5 0002:0000014d
b8:81c6 0002:0000014d
b8:81c7 0002:0000014e
b8:81c8 0002:00000152
b8:81cc 0002:00000153
b8:81d0 0002:00000154
b8:81d2 0002:00000155
b8:81d6 0002:00000156
b8:81d9 0002:00000157
b8:81db 0002:00000158
b8:81de 0002:0000015a
b8:81df 0002:0000015d
b8:81e3 0002:0000015e
b8:81e4 0002:0000015f
b8:81e7 0002:00000160
b8:81eb 0002:00000162
b8:81ec 0002:00000163
b8:81ed 0002:00000164
b8:81ee 0002:00000165
b8:81ef 0002:00000166
8b:914a 0002:0000016b
81:80f7 0002:0000016e
81:8027 0002:00000171
82:8bb3 0002:00000174
85:b9a3 0002:0000020e
85:b9a4 0002:0000020e
85:b9a5 0002:00000211
85:b9a7 0002:00000212
85:b9ad 0002:00000212
85:b9ae 0002:00000213
85:b9b1 0002:00000214
85:b9b2 0002:00000215
85:b9b3 0002:00000215
85:b9b4 0002:00000219
85:b9b7 0002:0000021a
85:b9bb 0002:0000021b
85:b9bd 0002:0000021b
85:b9bf 0002:0000021c
85:b9c2 0002:0000021d
85:b9c4 0002:0000021f
85:b9c5 0002:00000220
85:b9c7 0002:00000224
85:b9ca 0002:00000226
85:b9cd 0002:00000227
85:b9cf 0002:00000228
85:b9d1 0002:00000229
85:b9d5 0002:0000022a
85:b9d7 0002:0000022b
85:b9d9 0002:0000022c
85:b9da 0002:0000022d
85:b9dc 0002:0000022f
85:b9df 0002:00000231
85:b9e2 0002:00000231
85:b9e3 0002:00000232
85:b9e6 0002:00000234
85:b9ea 0002:00000235
85:b9ed 0002:00000236
85:b9ee 0002:00000237
85:b9ef 0002:00000237
85:b9f0 0002:00000238
85:b9f4 0002:00000239
85:b9f5 0002:0000023a
85:b9f9 0002:0000023b
85:b9fb 0002:0000023c
85:b9fc 0002:0000023d
85:b9fd 0002:0000023e
85:ba00 0002:0000023f
85:ba02 0002:00000240
85:ba04 0002:00000243
85:ba05 0002:00000243
85:ba06 0002:00000244
85:ba09 0002:00000245
85:ba8a 0002:00000253
85:ba8c 0002:00000254
85:ba8f 0002:00000255
85:ba92 0002:00000256
85:ba95 0002:0000025e
85:ba96 0002:0000025f
85:ba98 0002:00000260
85:ba9b 0002:00000261
85:ba9d 0002:00000262
85:ba9f 0002:00000263
85:baa2 0002:00000264
85:baa4 0002:00000265
85:baa7 0002:00000266
85:baa9 0002:00000269
85:baaa 0002:0000026a
85:baab 0002:0000026b
85:baac 0002:0000026c
85:baae 0002:0000026d
85:baaf 0002:0000026e
85:bab0 0002:0000026f
85:bab1 0002:00000274
85:bab4 0002:00000275
85:bab5 0002:00000276
85:bab8 0002:00000277
85:bab9 0002:00000278
85:baba 0002:00000279
85:babb 0002:0000027a
85:babc 0002:00000285
85:babd 0002:00000286
85:babf 0002:00000287
85:bac2 0002:00000288
85:bac4 0002:00000289
85:bac7 0002:0000028a
85:bac9 0002:0000028d
85:baca 0002:0000028e
85:bacb 0002:0000028f
85:bacd 0002:00000290
85:bace 0002:00000292
85:bacf 0002:00000293
85:bad1 0002:00000294
85:bad4 0002:00000295
85:bad6 0002:00000296
85:bad9 0002:00000297
85:badb 0002:00000298
85:badc 0002:0000029a
85:badd 0002:0000029b
85:badf 0002:0000029c
85:bae2 0002:0000029d
85:bae4 0002:0000029e
85:bae7 0002:0000029f
85:bae9 0002:000002a0
85:8246 0002:000002a5
85:8249 0002:000002a6
85:824b 0002:000002a7
85:82f9 0002:000002ab
b8:8017 0002:0000006a
b8:801b 0002:0000006b
b8:801e 0002:0000006c
b8:8020 0002:0000006d
b8:8024 0002:0000006e
b8:8028 0002:0000006f
b8:802a 0002:00000070
b8:802e 0002:00000071
b8:8032 0002:00000072
b8:8034 0002:00000074
b8:8038 0002:00000075
b8:803b 0002:00000078
b8:803c 0002:00000079
b8:803f 0002:0000007a
b8:8042 0002:0000007b
b8:8045 0002:0000007c
b8:8048 0002:0000007d
b8:8049 0002:0000007e
b8:804a 0002:0000007f
b8:804e 0002:00000080
b8:804f 0002:00000082
b8:8066 0002:00000086
b8:8068 0002:00000087
b8:8069 0002:00000088
b8:806a 0002:00000089
b8:806c 0002:0000008a
b8:806e 0002:0000008b
b8:8070 0002:0000008c
b8:8072 0002:0000008d
b8:8075 0002:0000008e
b8:8077 0002:0000008f
b8:807a 0002:00000090
b8:807d 0002:00000091
b8:807f 0002:00000092
b8:8083 0002:00000094
b8:8085 0002:00000095
b8:8087 0002:00000096
b8:8089 0002:00000097
b8:808b 0002:00000098
b8:808d 0002:00000099
b8:808f 0002:0000009a
b8:8092 0002:0000009b
b8:8094 0002:0000009c
b8:8097 0002:0000009d
b8:809a 0002:0000009e
b8:809c 0002:0000009f
b8:80a0 0002:000000a1
b8:80a3 0002:000000a2
b8:80a7 0002:000000a3
b8:80ab 0002:000000a4
b8:80af 0002:000000a5
b8:80b3 0002:000000a6
b8:80b7 0002:000000a8
b8:80bb 0002:000000b0
b8:80be 0002:000000b1
b8:80c1 0002:000000b3
b8:80c2 0002:000000b4
b8:80c3 0002:000000b5
b8:80c7 0002:000000b6
b8:80cb 0002:000000b7
b8:80cd 0002:000000c4
b8:80d1 0002:000000c5
b8:80d4 0002:000000c6
b8:80d6 0002:000000c7
b8:80da 0002:000000c8
b8:80dd 0002:000000c9
b8:80df 0002:000000ce
b8:80e2 0002:000000cf
b8:80e6 0002:000000d0
b8:80ea 0002:000000d3
b8:80eb 0002:000000d3
b8:80ec 0002:000000d3
b8:80ed 0002:000000d3
b8:80ee 0002:000000d4
b8:80ef 0002:000000db
b8:80f0 0002:000000dc
b8:80f1 0002:000000dd
b8:80f2 0002:000000de
b8:80f3 0002:000000df
b8:80f4 0002:000000e1
b8:80f7 0002:000000e2
b8:80f8 0002:000000e3
b8:80f9 0002:000000e4
b8:80fa 0002:000000e5
b8:80fc 0002:000000e7
b8:80fd 0002:000000ee
b8:80fe 0002:000000ef
b8:80ff 0002:000000f0
b8:8100 0002:000000f1
b8:8102 0002:000000f3
b8:8104 0002:000000f4
b8:8105 0002:000000f5
b8:8107 0002:000000f6
b8:8109 0002:000000f8
b8:810b 0002:000000f9
b8:810c 0002:000000fa
b8:810d 0002:000000fb
b8:810f 0002:000000fd
b8:8111 0002:000000fe
b8:8113 0002:000000ff
b8:8114 0002:00000100
b8:8115 0002:00000101
b8:8117 0002:00000103
b8:8118 0002:00000104
b8:8119 0002:00000108
b8:811d 0002:00000109
b8:8121 0002:0000010a
b8:8125 0002:0000010b
b8:8129 0002:0000010c
b8:812d 0002:0000010d
b8:8131 0002:0000010e
b8:8135 0002:0000010f
b8:8139 0002:00000110
b8:813d 0002:00000111
b8:8141 0002:00000112
b8:8142 0002:00000118
b8:8143 0002:00000118
b8:8144 0002:00000119
b8:8145 0002:00000119
b8:8146 0002:0000011a
b8:814a 0002:0000011b
b8:814d 0002:0000011b
b8:814e 0002:0000011c
b8:814f 0002:0000011d
b8:8153 0002:0000011e
b8:8154 0002:0000011f
b8:8158 0002:00000120
b8:8159 0002:00000121
b8:815d 0002:00000123
b8:8161 0002:00000124
b8:8162 0002:00000125
b8:8166 0002:00000126
b8:8167 0002:00000126
b8:8168 0002:00000127
b8:8169 0002:0000012c
b8:816a 0002:0000012c
b8:816b 0000:00000013
b8:816d 0002:0000012f
b8:816e 0002:0000012f
b8:816f 0002:00000131
b8:8170 0002:00000132
b8:8173 0002:00000133
b8:8174 0002:00000138
b8:8175 0002:00000138
b8:8176 0000:00000013
b8:8178 0002:0000013a
b8:817c 0002:0000013b
b8:8180 0002:0000013c
b8:8182 0002:0000013e
b8:8183 0002:0000013e
b8:8184 0002:0000013f
b8:8185 0002:00000147
b8:8189 0002:00000148
b8:818b 0002:0000014e
b8:818d 0002:0000014f
b8:818f 0002:00000152
b8:8192 0002:00000153
b8:8194 0002:00000154
b8:8197 0002:00000155
b8:8199 0002:00000156
b8:819c 0002:00000157
b8:81a0 0002:00000158
b8:81a2 0002:00000159
b8:81a4 0002:0000015a
b8:81a6 0002:0000015b
b8:81a8 0002:0000015c
b8:81a9 0002:00000160
b8:81aa 0002:00000160
b8:81ab 0002:00000161
b8:81ae 0002:00000162
b8:81b0 0002:00000163
b8:81b3 0002:00000164
b8:81b5 0002:00000165
b8:81b6 0002:00000166
b8:81b7 0002:00000168
b8:81ba 0002:00000169
b8:81bc 0002:0000016a
b8:81bf 0002:0000016b
b8:81c0 0002:0000016c
b8:81c3 0002:0000016d
b8:81c4 0002:0000016d
b8:81c5 0002:0000016e
b8:81c9 0002:0000016f
b8:81ca 0002:00000170
b8:81cd 0002:00000171
b8:81d1 0002:00000172
b8:81d3 0002:00000174
b8:81d6 0002:00000175
b8:81d8 0002:00000176
b8:81db 0002:00000177
b8:81dd 0002:00000179
b8:81e1 0002:0000017b
b8:81e3 0002:0000017c
b8:81e5 0002:0000017d
b8:81e6 0002:0000017d
b8:81e7 0002:0000017e
b8:81e8 0002:00000189
b8:81e9 0002:0000018a
b8:81ed 0002:0000018b
b8:81ee 0002:0000018c
b8:81f2 0002:0000018d
b8:81f3 0002:0000018f
b8:81f7 0002:00000190
b8:81f8 0002:000001c2
b8:81f9 0002:000001c2
b8:81fa 0002:000001c5
b8:81fe 0002:000001c6
b8:8202 0002:000001c7
b8:8204 0002:000001c9
b8:8206 0002:000001c9
b8:8207 0002:000001cc
b8:820b 0002:000001cd
b8:820d 0002:000001ce
b8:8211 0002:000001cf
b8:8213 0002:000001d0
b8:8217 0002:000001d1
b8:821a 0002:000001d2
b8:821c 0002:000001d3
b8:821e 0002:000001d4
b8:8222 0002:000001d5
b8:8224 0002:000001d6
b8:8226 0002:000001d7
b8:8229 0002:000001d8
b8:822c 0002:000001d9
b8:822e 0002:000001da
b8:8236 0002:000001de
b8:8237 0002:000001df
b8:8238 0002:000001e0
b8:823c 0002:000001e3
b8:8240 0002:000001e4
b8:8244 0002:000001e5
b8:8246 0002:000001e7
b8:8247 0002:000001e8
b8:8248 0002:000001e9
b8:824a 0002:000001ee
b8:824b 0002:000001ef
b8:824f 0002:000001f2
b8:8253 0002:000001f3
b8:8257 0002:000001f4
b8:825b 0002:000001f5
b8:825c 0002:000001f9
b8:825e 0002:000001fa
b8:8261 0002:000001fb
b8:8263 0002:000001fc
b8:8266 0002:000001ff
b8:826a 0002:00000200
b8:826b 0002:00000201
b8:826f 0002:00000203
b8:8271 0002:00000206
b8:8273 0002:00000207
b8:8275 0002:00000208
b8:8276 0002:00000208
b8:8277 0002:00000209
b8:8278 0002:0000020d
b8:8279 0002:0000020d
b8:827a 0002:0000020d
b8:827b 0002:0000020e
b8:827f 0002:0000020f
b8:8282 0002:0000020f
b8:8283 0002:00000211
b8:8287 0002:00000212
b8:8288 0002:00000213
b8:828c 0002:00000214
b8:828f 0002:00000215
b8:8291 0002:00000217
b8:8294 0002:0000021a
b8:8298 0002:0000021b
b8:829c 0002:0000021c
b8:829e 0002:0000021e
b8:82a2 0002:0000021f
b8:82a3 0002:00000221
b8:82a7 0002:00000222
b8:82a9 0002:00000225
b8:82ad 0002:00000226
b8:82b0 0002:00000227
b8:82b2 0002:00000228
b8:82b5 0002:0000022b
b8:82b6 0002:0000022c
b8:82b7 0002:0000022d
b8:82bb 0002:0000022e
b8:82bd 0002:00000231
b8:82be 0002:00000231
b8:82bf 0002:00000231
b8:82c0 0002:00000232
b8:82c1 0002:00000236
b8:82c5 0002:00000237
b8:82c9 0002:00000238
b8:82cb 0002:00000239
b8:82cf 0002:0000023a
b8:82d2 0002:0000023b
b8:82d4 0002:0000023c
b8:82d7 0002:0000023e
b8:82d8 0002:00000241
b8:82dc 0002:00000243
b8:82dd 0002:00000244
b8:82de 0002:00000245
b8:82df 0002:00000246
b8:82e0 0002:00000247
8b:914a 0002:0000024c
81:80f7 0002:0000024f
81:8027 0002:00000252
82:8bb3 0002:00000255
85:b9a3 0002:000002ef
85:b9a4 0002:000002ef
85:b9a5 0002:000002f2
85:b9a7 0002:000002f3
85:b9ad 0002:000002f3
85:b9ae 0002:000002f4
85:b9b1 0002:000002f5
85:b9b2 0002:000002f6
85:b9b3 0002:000002f6
85:b9b4 0002:000002fa
85:b9b7 0002:000002fb
85:b9bb 0002:000002fc
85:b9bd 0002:000002fc
85:b9bf 0002:000002fd
85:b9c2 0002:000002fe
85:b9c4 0002:00000300
85:b9c5 0002:00000301
85:b9c7 0002:00000305
85:b9ca 0002:00000307
85:b9cd 0002:00000308
85:b9cf 0002:00000309
85:b9d1 0002:0000030a
85:b9d5 0002:0000030b
85:b9d7 0002:0000030c
85:b9d9 0002:0000030d
85:b9da 0002:0000030e
85:b9dc 0002:00000310
85:b9df 0002:00000312
85:b9e2 0002:00000312
85:b9e3 0002:00000313
85:b9e6 0002:00000315
85:b9ea 0002:00000316
85:b9ed 0002:00000317
85:b9ee 0002:00000318
85:b9ef 0002:00000318
85:b9f0 0002:00000319
85:b9f4 0002:0000031a
85:b9f5 0002:0000031b
85:b9f9 0002:0000031c
85:b9fb 0002:0000031d
85:b9fc 0002:0000031e
85:b9fd 0002:0000031f
85:ba00 0002:00000320
85:ba02 0002:00000321
85:ba04 0002:00000324
85:ba05 0002:00000324
85:ba06 0002:00000325
85:ba09 0002:00000326
85:ba8a 0002:00000334
85:ba8c 0002:00000335
85:ba8f 0002:00000336
85:ba92 0002:00000337
85:ba95 0002:0000033f
85:ba96 0002:00000340
85:ba98 0002:00000341
85:ba9b 0002:00000342
85:ba9d 0002:00000343
85:ba9f 0002:00000344
85:baa2 0002:00000345
85:baa4 0002:00000346
85:baa7 0002:00000347
85:baa9 0002:0000034a
85:baaa 0002:0000034b
85:baab 0002:0000034c
85:baac 0002:0000034d
85:baae 0002:0000034e
85:baaf 0002:0000034f
85:bab0 0002:00000350
85:bab1 0002:00000355
85:bab4 0002:00000356
85:bab5 0002:00000357
85:bab8 0002:00000358
85:bab9 0002:00000359
85:baba 0002:0000035a
85:babb 0002:0000035b
85:babc 0002:00000366
85:babd 0002:00000367
85:babf 0002:00000368
85:bac2 0002:00000369
85:bac4 0002:0000036a
85:bac7 0002:0000036b
85:bac9 0002:0000036e
85:baca 0002:0000036f
85:bacb 0002:00000370
85:bacd 0002:00000371
85:bace 0002:00000373
85:bacf 0002:00000374
85:bad1 0002:00000375
85:bad4 0002:00000376
85:bad6 0002:00000377
85:bad9 0002:00000378
85:badb 0002:00000379
85:badc 0002:0000037b
85:badd 0002:0000037c
85:badf 0002:0000037d
85:bae2 0002:0000037e
85:bae4 0002:0000037f
85:bae7 0002:00000380
85:bae9 0002:00000381
85:8246 0002:00000386
85:8249 0002:00000387
85:824b 0002:00000388
85:82f9 0002:0000038c
b8:885c 0003:00000045
b8:885d 0003:00000045
b8:885e 0003:00000046

View File

@@ -4,7 +4,7 @@
"CLIPLEN_end": "85:990F",
"CLIPLEN_no_multi": "85:990C",
"CLIPSET": "85:FF1D",
"COLLECTTANK": "B8:80EF",
"COLLECTTANK": "B8:81E8",
"MISCFX": "85:FF45",
"NORMAL": "84:8BF2",
"SETFX": "85:FF4E",
@@ -22,6 +22,11 @@
"config_player_id": "CE:FF08",
"config_remote_items": "CE:FF06",
"config_sprite": "CE:FF02",
"copy_config_to_sram": "B8:8119",
"copy_memory": "B8:80FD",
"copy_memory_done": "B8:8117",
"copy_memory_even": "B8:8109",
"copy_memory_loop": "B8:810F",
"h_item": "84:F894",
"i_chozo_item": "84:F8AD",
"i_hidden_item": "84:F8B4",
@@ -30,11 +35,11 @@
"i_item_setup_shared_all_items": "B8:8878",
"i_item_setup_shared_alwaysloaded": "B8:8883",
"i_live_pickup": "84:FA79",
"i_live_pickup_multiworld": "B8:817F",
"i_live_pickup_multiworld_end": "B8:81C4",
"i_live_pickup_multiworld_local_item_or_offworld": "B8:819B",
"i_live_pickup_multiworld_own_item": "B8:81B0",
"i_live_pickup_multiworld_own_item1": "B8:81BC",
"i_live_pickup_multiworld": "B8:8278",
"i_live_pickup_multiworld_end": "B8:82BD",
"i_live_pickup_multiworld_local_item_or_offworld": "B8:8294",
"i_live_pickup_multiworld_own_item": "B8:82A9",
"i_live_pickup_multiworld_own_item1": "B8:82B5",
"i_load_custom_graphics": "84:FA1E",
"i_load_custom_graphics_all_items": "84:FA39",
"i_load_custom_graphics_alwaysloaded": "84:FA49",
@@ -69,22 +74,27 @@
"message_write_placeholders_loop": "85:B9CA",
"message_write_placeholders_notfound": "85:B9DC",
"message_write_placeholders_value_ok": "85:B9DF",
"mw_display_item_sent": "B8:8092",
"mw_handle_queue": "B8:80FF",
"mw_handle_queue_end": "B8:8178",
"mw_handle_queue_loop": "B8:8101",
"mw_handle_queue_new_remote_item": "B8:8151",
"mw_handle_queue_next": "B8:816D",
"mw_handle_queue_perform_receive": "B8:8163",
"mw_hook_main_game": "B8:81C8",
"mw_display_item_sent": "B8:818B",
"mw_handle_queue": "B8:81F8",
"mw_handle_queue_end": "B8:8271",
"mw_handle_queue_loop": "B8:81FA",
"mw_handle_queue_new_remote_item": "B8:824A",
"mw_handle_queue_next": "B8:8266",
"mw_handle_queue_perform_receive": "B8:825C",
"mw_hook_main_game": "B8:82C1",
"mw_init": "B8:8011",
"mw_init_end": "B8:8044",
"mw_init_continuereset": "B8:8066",
"mw_init_end": "B8:80EA",
"mw_init_memory": "B8:8000",
"mw_load_sram": "B8:8083",
"mw_receive_item": "B8:80B0",
"mw_receive_item_end": "B8:80E8",
"mw_save_sram": "B8:8070",
"mw_write_message": "B8:8049",
"mw_init_reset_sram": "B8:803B",
"mw_init_smstringdata": "B8:8051",
"mw_load_sram": "B8:8174",
"mw_load_sram_done": "B8:8182",
"mw_load_sram_setnewgame": "B8:8185",
"mw_receive_item": "B8:81A9",
"mw_receive_item_end": "B8:81E1",
"mw_save_sram": "B8:8169",
"mw_write_message": "B8:8142",
"nonprog_item_eight_palette_indices": "84:F888",
"offworld_graphics_data_item": "89:9200",
"offworld_graphics_data_progression_item": "89:9100",
@@ -109,7 +119,7 @@
"p_visible_item_end": "84:F96E",
"p_visible_item_loop": "84:F95B",
"p_visible_item_trigger": "84:F967",
"patch_load_multiworld": "B8:81DF",
"patch_load_multiworld": "B8:82D8",
"perform_item_pickup": "84:FA7E",
"plm_graphics_entry_offworld_item": "84:F886",
"plm_graphics_entry_offworld_progression_item": "84:F87C",
@@ -128,14 +138,24 @@
"start_item_data_reserve": "B8:C818",
"update_graphic": "B8:C856",
"v_item": "84:F890",
"write_repeated_memory": "B8:80EF",
"write_repeated_memory_loop": "B8:80F4",
"ITEM_RAM": "7E:09A2",
"SRAM_MW_ITEMS_RECV": "70:2000",
"SRAM_MW_ITEMS_RECV_RPTR": "70:2600",
"SRAM_MW_ITEMS_RECV_WPTR": "70:2602",
"SRAM_MW_ITEMS_RECV_SPTR": "70:2604",
"SRAM_MW_ITEMS_SENT_RPTR": "70:2680",
"SRAM_MW_ITEMS_SENT_WPTR": "70:2682",
"SRAM_MW_ITEMS_RECV_WCOUNT": "70:2602",
"ReceiveQueueCompletedCount_InRamThatGetsSavedToSaveSlot": "7e:d8ae",
"SRAM_MW_ITEMS_SENT_RCOUNT": "70:2680",
"SRAM_MW_ITEMS_SENT_WCOUNT": "70:2682",
"SRAM_MW_ITEMS_SENT": "70:2700",
"SRAM_MW_INITIALIZED": "70:26fe",
"SRAM_MW_SM": "70:3000",
"SRAM_MW_ROMTITLE": "70:3015",
"SRAM_MW_SEEDINT": "70:3060",
"SRAM_MW_INITIALIZED": "70:3064",
"SRAM_MW_CONFIG_ENABLED": "70:3070",
"SRAM_MW_CONFIG_CUSTOM_SPRITE": "70:3072",
"SRAM_MW_CONFIG_DEATHLINK": "70:3074",
"SRAM_MW_CONFIG_REMOTE_ITEMS": "70:3076",
"SRAM_MW_CONFIG_PLAYER_ID": "70:3078",
"varia_seedint_location": "df:ff00",
"CollectedItems": "7E:D86E"
}

View File

@@ -294,7 +294,18 @@ accessPoints = [
sm.canGetBackFromRidleyZone(),
sm.canPassWastelandDessgeegas(),
sm.canPassRedKiHunters())),
'RidleyRoomOut': Cache.ldeco(lambda sm: sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']))
'RidleyRoomOut': Cache.ldeco(lambda sm: sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main'])),
'Wasteland': Cache.ldeco(lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']),
sm.canGetBackFromRidleyZone(),
sm.canPassWastelandDessgeegas()))
}, internal=True),
AccessPoint('Wasteland', 'LowerNorfair', {
# no transition to firefleas to exlude pb of shame location when starting at firefleas top
'Ridley Zone': Cache.ldeco(lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']),
sm.traverse('WastelandLeft'),
sm.canGetBackFromRidleyZone(),
sm.canPassWastelandDessgeegas(),
sm.canPassNinjaPirates()))
}, internal=True),
AccessPoint('Three Muskateers Room Left', 'LowerNorfair', {
'Firefleas': Cache.ldeco(lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']),

View File

@@ -797,10 +797,10 @@ locationsDict["Power Bomb (lower Norfair above fire flea room)"].Available = (
lambda sm: SMBool(True)
)
locationsDict["Power Bomb (Power Bombs of shame)"].AccessFrom = {
'Ridley Zone': lambda sm: sm.canUsePowerBombs()
'Wasteland': lambda sm: sm.canUsePowerBombs()
}
locationsDict["Power Bomb (Power Bombs of shame)"].Available = (
lambda sm: sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main'])
lambda sm: SMBool(True)
)
locationsDict["Missile (lower Norfair near Wave Beam)"].AccessFrom = {
'Firefleas': lambda sm: SMBool(True)

View File

@@ -245,7 +245,7 @@ locBitFS_table = {
locWMotR_table = {
"Wing Mario Over the Rainbow Red Coins": 3626154,
"Wing Mario Over the Rainbow 1Up Block": 3626242
"Wing Mario Over the Rainbow 1Up Block": 3626243
}
locBitS_table = {
@@ -268,4 +268,4 @@ location_table = {**locBoB_table,**locWhomp_table,**locJRB_table,**locCCM_table,
**locWDW_table,**locTTM_table,**locTHI_table,**locTTC_table,**locRR_table, \
**loc100Coin_table,**locPSS_table,**locSA_table,**locBitDW_table,**locTotWC_table, \
**locCotMC_table, **locVCutM_table, **locBitFS_table, **locWMotR_table, **locBitS_table, \
**locSS_table}
**locSS_table}

View File

@@ -34,7 +34,7 @@ class SM64World(World):
item_name_to_id = item_table
location_name_to_id = location_table
data_version = 7
data_version = 8
required_client_version = (0, 3, 0)
area_connections: typing.Dict[int, int]

View File

@@ -1,3 +1,4 @@
import functools
from typing import Dict, Set, List
# EN Locale Creature Name to rough depth in meters found at
@@ -54,21 +55,27 @@ all_creatures: Dict[str, int] = {
"Sea Emperor Juvenile": 1700,
}
# be nice and make these require Stasis Rifle
aggressive: Set[str] = {
"Cave Crawler", # is very easy without Stasis Rifle, but included for consistency
"Crashfish",
"Biter",
"Bleeder",
"Blighter",
"Blood Crawler",
"Mesmer",
"Reaper Leviathan",
"Crabsquid",
"Warper",
"Crabsnake",
"Ampeel",
"Stalker",
"Sand Shark",
"Boneshark",
"Lava Lizard",
"Sea Dragon Leviathan",
"River Prowler",
"Ghost Leviathan Juvenile",
"Ghost Leviathan"
}
containment: Set[str] = { # creatures that have to be raised from eggs
@@ -94,6 +101,25 @@ creature_locations: Dict[str, int] = {
creature + suffix: creature_id for creature_id, creature in enumerate(all_creatures, start=34000)
}
all_creatures_presorted: List[str] = sorted(all_creatures)
all_creatures_presorted_without_containment = [name for name in all_creatures_presorted if name not in containment]
class Definitions:
"""Only compute lists if needed and then cache them."""
@functools.cached_property
def all_creatures_presorted(self) -> List[str]:
return sorted(all_creatures)
@functools.cached_property
def all_creatures_presorted_without_containment(self) -> List[str]:
return [name for name in self.all_creatures_presorted if name not in containment]
@functools.cached_property
def all_creatures_presorted_without_stasis(self) -> List[str]:
return [name for name in self.all_creatures_presorted if name not in aggressive or name in hatchable]
@functools.cached_property
def all_creatures_presorted_without_aggressive(self) -> List[str]:
return [name for name in self.all_creatures_presorted if name not in aggressive]
# only singleton needed
Definitions: Definitions = Definitions()

View File

@@ -15,7 +15,12 @@ class LocationDict(TypedDict, total=False):
need_propulsion_cannon: bool
events: List[str] = ["Neptune Launch", "Disable Quarantine", "Full Infection", "Repair Aurora Drive"]
events: List[str] = [
"Neptune Launch",
"Disable Quarantine",
"Full Infection",
"Repair Aurora Drive",
]
location_table: Dict[int, LocationDict] = {
33000: {'can_slip_through': False,

View File

@@ -1,7 +1,7 @@
import typing
from Options import Choice, Range, DeathLink
from .Creatures import all_creatures
from .Creatures import all_creatures, Definitions
class ItemPool(Choice):
@@ -46,14 +46,27 @@ class AggressiveScanLogic(Choice):
Containment: Removes Stasis Rifle as expected solution and expects Alien Containment instead.
Either: Creatures may be expected to be scanned via Stasis Rifle or Containment, whichever is found first.
None: Aggressive Creatures are assumed to not need any tools to scan.
Removed: No Creatures needing Stasis or Containment will be in the pool at all.
Note: Containment, Either and None adds Cuddlefish as an option for scans.
Note: Stasis, Either and None adds unhatchable aggressive species, such as Warper.
Note: This is purely a logic expectation, and does not affect gameplay, only placement."""
display_name = "Aggressive Creature Scan Logic"
option_stasis = 0
option_containment = 1
option_either = 2
option_none = 3
option_removed = 4
def get_pool(self) -> typing.List[str]:
if self == self.option_removed:
return Definitions.all_creatures_presorted_without_aggressive
elif self == self.option_stasis:
return Definitions.all_creatures_presorted_without_containment
elif self == self.option_containment:
return Definitions.all_creatures_presorted_without_stasis
else:
return Definitions.all_creatures_presorted
class SubnauticaDeathLink(DeathLink):

View File

@@ -1,8 +1,8 @@
from typing import TYPE_CHECKING, Dict, Callable
from typing import TYPE_CHECKING, Dict, Callable, Optional
from worlds.generic.Rules import set_rule, add_rule
from .Locations import location_table, LocationDict
from .Creatures import all_creatures, aggressive, suffix
from .Creatures import all_creatures, aggressive, suffix, hatchable, containment
from .Options import AggressiveScanLogic
import math
@@ -258,6 +258,15 @@ def set_creature_rule(world, player: int, creature_name: str) -> "Location":
return location
def get_aggression_rule(option: AggressiveScanLogic, creature_name: str) -> \
Optional[Callable[["CollectionState", int], bool]]:
"""Get logic rule for a creature scan location."""
if creature_name not in hatchable and option != option.option_none: # can only be done via stasis
return has_stasis_rifle
# otherwise allow option preference
return aggression_rules.get(option.value, None)
aggression_rules: Dict[int, Callable[["CollectionState", int], bool]] = {
AggressiveScanLogic.option_stasis: has_stasis_rifle,
AggressiveScanLogic.option_containment: has_containment,
@@ -274,14 +283,21 @@ def set_rules(subnautica_world: "SubnauticaWorld"):
set_location_rule(world, player, loc)
if subnautica_world.creatures_to_scan:
aggressive_rule = aggression_rules.get(world.creature_scan_logic[player], None)
option = world.creature_scan_logic[player]
for creature_name in subnautica_world.creatures_to_scan:
location = set_creature_rule(world, player, creature_name)
if creature_name in aggressive and aggressive_rule:
add_rule(location, lambda state: aggressive_rule(state, player))
if creature_name in containment: # there is no other way, hard-required containment
add_rule(location, lambda state: has_containment(state, player))
elif creature_name in aggressive:
rule = get_aggression_rule(option, creature_name)
if rule:
add_rule(location,
lambda state, loc_rule=get_aggression_rule(option, creature_name): loc_rule(state, player))
# Victory locations
set_rule(world.get_location("Neptune Launch", player), lambda state:
set_rule(world.get_location("Neptune Launch", player),
lambda state:
get_max_depth(state, player) >= 1444 and
has_mobile_vehicle_bay(state, player) and
state.has("Neptune Launch Platform", player) and

View File

@@ -52,14 +52,15 @@ class SubnauticaWorld(World):
self.create_item("Seaglide Fragment"),
self.create_item("Seaglide Fragment")
]
if self.world.creature_scan_logic[self.player] == Options.AggressiveScanLogic.option_stasis:
valid_creatures = Creatures.all_creatures_presorted_without_containment
self.world.creature_scans[self.player].value = min(len(
Creatures.all_creatures_presorted_without_containment),
self.world.creature_scans[self.player].value)
else:
valid_creatures = Creatures.all_creatures_presorted
self.creatures_to_scan = self.world.random.sample(valid_creatures,
scan_option: Options.AggressiveScanLogic = self.world.creature_scan_logic[self.player]
creature_pool = scan_option.get_pool()
self.world.creature_scans[self.player].value = min(
len(creature_pool),
self.world.creature_scans[self.player].value
)
self.creatures_to_scan = self.world.random.sample(creature_pool,
self.world.creature_scans[self.player].value)
def create_regions(self):