Compare commits

..

3 Commits

Author SHA1 Message Date
Fabian Dill
775f56036c fix type 2025-01-09 17:02:36 +01:00
Fabian Dill
39342ad5d5 review concerns 2025-01-09 17:01:19 +01:00
Fabian Dill
ce09144261 Core: add creation reason to filler 2025-01-08 00:30:03 +01:00
24 changed files with 146 additions and 302 deletions

View File

@@ -373,7 +373,8 @@ class MultiWorld():
items_to_add.append(AutoWorld.call_single(self, "create_item", item_player,
group["replacement_items"][player]))
else:
items_to_add.append(AutoWorld.call_single(self, "create_filler", item_player))
items_to_add.append(AutoWorld.call_single(self, "create_filler", item_player,
AutoWorld.FillerReason.item_link))
self.random.shuffle(items_to_add)
self.itempool.extend(items_to_add[:itemcount - len(self.itempool)])

10
Fill.py
View File

@@ -7,7 +7,7 @@ from collections import Counter, deque
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld
from Options import Accessibility
from worlds.AutoWorld import call_all
from worlds.AutoWorld import call_all, FillerReason
from worlds.generic.Rules import add_item_rule
@@ -316,7 +316,7 @@ def remaining_fill(multiworld: MultiWorld,
for item in unplaced_items:
logging.debug(f"Moved {item} to start_inventory to prevent fill failure.")
multiworld.push_precollected(item)
last_batch.append(multiworld.worlds[item.player].create_filler())
last_batch.append(multiworld.worlds[item.player].create_filler(FillerReason.panic_fill))
remaining_fill(multiworld, locations, unplaced_items, name + " Start Inventory Retry")
else:
raise FillError(f"No more spots to place {len(unplaced_items)} items. Remaining locations are invalid.\n"
@@ -521,7 +521,7 @@ def distribute_items_restrictive(multiworld: MultiWorld,
for item in progitempool:
logging.debug(f"Moved {item} to start_inventory to prevent fill failure.")
multiworld.push_precollected(item)
filleritempool.append(multiworld.worlds[item.player].create_filler())
filleritempool.append(multiworld.worlds[item.player].create_filler(FillerReason.panic_fill))
logging.warning(f"{len(progitempool)} items moved to start inventory,"
f" due to failure in Progression fill step.")
progitempool[:] = []
@@ -545,7 +545,7 @@ def distribute_items_restrictive(multiworld: MultiWorld,
inaccessible_location_rules(multiworld, multiworld.state, defaultlocations)
remaining_fill(multiworld, excludedlocations, filleritempool, "Remaining Excluded",
move_unplaceable_to_start_inventory=panic_method=="start_inventory")
move_unplaceable_to_start_inventory=panic_method == "start_inventory")
if excludedlocations:
raise FillError(
@@ -557,7 +557,7 @@ def distribute_items_restrictive(multiworld: MultiWorld,
restitempool = filleritempool + usefulitempool
remaining_fill(multiworld, defaultlocations, restitempool,
move_unplaceable_to_start_inventory=panic_method=="start_inventory")
move_unplaceable_to_start_inventory=panic_method == "start_inventory")
unplaced = restitempool
unfilled = defaultlocations

View File

@@ -181,7 +181,8 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
logger.warning(f"{player_name} tried to remove items from their pool that don't exist: {unfound_items}")
needed_items = target_per_player[player] - sum(unfound_items.values())
new_itempool += [multiworld.worlds[player].create_filler() for _ in range(needed_items)]
new_itempool += [multiworld.worlds[player].create_filler(AutoWorld.FillerReason.start_inventory_from_pool)
for _ in range(needed_items)]
assert len(multiworld.itempool) == len(new_itempool), "Item Pool amounts should not change."
multiworld.itempool[:] = new_itempool

View File

@@ -444,7 +444,7 @@ class Context:
self.slot_info = decoded_obj["slot_info"]
self.games = {slot: slot_info.game for slot, slot_info in self.slot_info.items()}
self.groups = {slot: set(slot_info.group_members) for slot, slot_info in self.slot_info.items()
self.groups = {slot: slot_info.group_members for slot, slot_info in self.slot_info.items()
if slot_info.type == SlotType.group}
self.clients = {0: {}}

View File

@@ -152,15 +152,8 @@ def home_path(*path: str) -> str:
if hasattr(home_path, 'cached_path'):
pass
elif sys.platform.startswith('linux'):
xdg_data_home = os.getenv('XDG_DATA_HOME', os.path.expanduser('~/.local/share'))
home_path.cached_path = xdg_data_home + '/Archipelago'
if not os.path.isdir(home_path.cached_path):
legacy_home_path = os.path.expanduser('~/Archipelago')
if os.path.isdir(legacy_home_path):
os.renames(legacy_home_path, home_path.cached_path)
os.symlink(home_path.cached_path, legacy_home_path)
else:
os.makedirs(home_path.cached_path, 0o700, exist_ok=True)
home_path.cached_path = os.path.expanduser('~/Archipelago')
os.makedirs(home_path.cached_path, 0o700, exist_ok=True)
else:
# not implemented
home_path.cached_path = local_path() # this will generate the same exceptions we got previously

View File

@@ -147,8 +147,3 @@
rectangle: self.x-2, self.y-2, self.width+4, self.height+4
<ServerToolTip>:
pos_hint: {'center_y': 0.5, 'center_x': 0.5}
<AutocompleteHintInput>
size_hint_y: None
height: dp(30)
multiline: False
write_tab: False

65
kvui.py
View File

@@ -40,7 +40,7 @@ from kivy.core.image import ImageLoader, ImageLoaderBase, ImageData
from kivy.base import ExceptionHandler, ExceptionManager
from kivy.clock import Clock
from kivy.factory import Factory
from kivy.properties import BooleanProperty, ObjectProperty, NumericProperty
from kivy.properties import BooleanProperty, ObjectProperty
from kivy.metrics import dp
from kivy.effects.scroll import ScrollEffect
from kivy.uix.widget import Widget
@@ -64,7 +64,6 @@ from kivy.uix.recycleboxlayout import RecycleBoxLayout
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
from kivy.animation import Animation
from kivy.uix.popup import Popup
from kivy.uix.dropdown import DropDown
from kivy.uix.image import AsyncImage
fade_in_animation = Animation(opacity=0, duration=0) + Animation(opacity=1, duration=0.25)
@@ -306,50 +305,6 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
""" Respond to the selection of items in the view. """
self.selected = is_selected
class AutocompleteHintInput(TextInput):
min_chars = NumericProperty(3)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.dropdown = DropDown()
self.dropdown.bind(on_select=lambda instance, x: setattr(self, 'text', x))
self.bind(on_text_validate=self.on_message)
def on_message(self, instance):
App.get_running_app().commandprocessor("!hint "+instance.text)
def on_text(self, instance, value):
if len(value) >= self.min_chars:
self.dropdown.clear_widgets()
ctx: context_type = App.get_running_app().ctx
if not ctx.game:
return
item_names = ctx.item_names._game_store[ctx.game].values()
def on_press(button: Button):
split_text = MarkupLabel(text=button.text).markup
return self.dropdown.select("".join(text_frag for text_frag in split_text
if not text_frag.startswith("[")))
lowered = value.lower()
for item_name in item_names:
try:
index = item_name.lower().index(lowered)
except ValueError:
pass # substring not found
else:
text = escape_markup(item_name)
text = text[:index] + "[b]" + text[index:index+len(value)]+"[/b]"+text[index+len(value):]
btn = Button(text=text, size_hint_y=None, height=dp(30), markup=True)
btn.bind(on_release=on_press)
self.dropdown.add_widget(btn)
if not self.dropdown.attach_to:
self.dropdown.open(self)
else:
self.dropdown.dismiss()
class HintLabel(RecycleDataViewBehavior, BoxLayout):
selected = BooleanProperty(False)
striped = BooleanProperty(False)
@@ -615,10 +570,8 @@ class GameManager(App):
# show Archipelago tab if other logging is present
self.tabs.add_widget(panel)
hint_panel = self.add_client_tab("Hints", HintLayout())
self.hint_log = HintLog(self.json_to_kivy_parser)
hint_panel = self.add_client_tab("Hints", HintLog(self.json_to_kivy_parser))
self.log_panels["Hints"] = hint_panel.content
hint_panel.content.add_widget(self.hint_log)
if len(self.logging_pairs) == 1:
self.tabs.default_tab_text = "Archipelago"
@@ -745,7 +698,7 @@ class GameManager(App):
def update_hints(self):
hints = self.ctx.stored_data.get(f"_read_hints_{self.ctx.team}_{self.ctx.slot}", [])
self.hint_log.refresh_hints(hints)
self.log_panels["Hints"].refresh_hints(hints)
# default F1 keybind, opens a settings menu, that seems to break the layout engine once closed
def open_settings(self, *largs):
@@ -800,17 +753,6 @@ class UILog(RecycleView):
element.height = element.texture_size[1]
class HintLayout(BoxLayout):
orientation = "vertical"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
boxlayout = BoxLayout(orientation="horizontal", size_hint_y=None, height=dp(30))
boxlayout.add_widget(Label(text="New Hint:", size_hint_x=None, size_hint_y=None, height=dp(30)))
boxlayout.add_widget(AutocompleteHintInput())
self.add_widget(boxlayout)
status_names: typing.Dict[HintStatus, str] = {
HintStatus.HINT_FOUND: "Found",
HintStatus.HINT_UNSPECIFIED: "Unspecified",
@@ -827,7 +769,6 @@ status_colors: typing.Dict[HintStatus, str] = {
}
class HintLog(RecycleView):
header = {
"receiving": {"text": "[u]Receiving Player[/u]"},

View File

@@ -7,7 +7,7 @@ schema>=0.7.7
kivy>=2.3.0
bsdiff4>=1.2.4
platformdirs>=4.2.2
certifi>=2024.12.14
certifi>=2024.8.30
cython>=3.0.11
cymem>=2.0.8
orjson>=3.10.7

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
import enum
import hashlib
import logging
import pathlib
@@ -18,6 +19,15 @@ if TYPE_CHECKING:
from . import GamesPackage
from settings import Group
class FillerReason(enum.StrEnum):
undefined = enum.auto()
item_link = enum.auto()
panic_fill = enum.auto()
start_inventory_from_pool = enum.auto()
world = enum.auto()
perf_logger = logging.getLogger("performance")
@@ -527,7 +537,7 @@ class World(metaclass=AutoWorldRegister):
return False
# following methods should not need to be overridden.
def create_filler(self) -> "Item":
def create_filler(self, reason: FillerReason = FillerReason.undefined) -> "Item":
return self.create_item(self.get_filler_item_name())
# convenience methods

View File

@@ -592,9 +592,9 @@ def global_rules(multiworld: MultiWorld, player: int):
lambda state: can_kill_most_things(state, player, 8) and has_fire_source(state, player) and state.multiworld.get_entrance('Ganons Tower Torch Rooms', player).parent_region.dungeon.bosses['middle'].can_defeat(state))
set_rule(multiworld.get_location('Ganons Tower - Mini Helmasaur Key Drop', player), lambda state: can_kill_most_things(state, player, 1))
set_rule(multiworld.get_location('Ganons Tower - Pre-Moldorm Chest', player),
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 7) and can_use_bombs(state, player))
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 7))
set_rule(multiworld.get_entrance('Ganons Tower Moldorm Door', player),
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 8) and can_use_bombs(state, player))
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 8))
set_rule(multiworld.get_entrance('Ganons Tower Moldorm Gap', player),
lambda state: state.has('Hookshot', player) and state.multiworld.get_entrance('Ganons Tower Moldorm Gap', player).parent_region.dungeon.bosses['top'].can_defeat(state))
set_defeat_dungeon_boss_rule(multiworld.get_location('Agahnim 2', player))

View File

@@ -130,21 +130,19 @@ class TestGanonsTower(TestDungeon):
["Ganons Tower - Pre-Moldorm Chest", False, []],
["Ganons Tower - Pre-Moldorm Chest", False, [], ['Progressive Bow']],
["Ganons Tower - Pre-Moldorm Chest", False, [], ['Bomb Upgrade (50)']],
["Ganons Tower - Pre-Moldorm Chest", False, [], ['Big Key (Ganons Tower)']],
["Ganons Tower - Pre-Moldorm Chest", False, [], ['Lamp', 'Fire Rod']],
["Ganons Tower - Pre-Moldorm Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp']],
["Ganons Tower - Pre-Moldorm Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod']],
["Ganons Tower - Pre-Moldorm Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp']],
["Ganons Tower - Pre-Moldorm Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod']],
["Ganons Tower - Validation Chest", False, []],
["Ganons Tower - Validation Chest", False, [], ['Hookshot']],
["Ganons Tower - Validation Chest", False, [], ['Progressive Bow']],
["Ganons Tower - Validation Chest", False, [], ['Bomb Upgrade (50)']],
["Ganons Tower - Validation Chest", False, [], ['Big Key (Ganons Tower)']],
["Ganons Tower - Validation Chest", False, [], ['Lamp', 'Fire Rod']],
["Ganons Tower - Validation Chest", False, [], ['Progressive Sword', 'Hammer']],
["Ganons Tower - Validation Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Progressive Sword']],
["Ganons Tower - Validation Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Progressive Sword']],
["Ganons Tower - Validation Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Hammer']],
["Ganons Tower - Validation Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Hammer']],
["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Progressive Sword']],
["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Progressive Sword']],
["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Hammer']],
["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Hammer']],
])

View File

@@ -1718,7 +1718,7 @@ def create_regions(world):
connect(multiworld, player, "Vermilion City", "Vermilion City-Dock", lambda state: state.has("S.S. Ticket", player))
connect(multiworld, player, "Vermilion City", "Route 11")
connect(multiworld, player, "Route 12-N", "Route 12-S", lambda state: logic.can_surf(state, world, player))
connect(multiworld, player, "Route 12-W", "Route 11-E")
connect(multiworld, player, "Route 12-W", "Route 11-E", lambda state: state.has("Poke Flute", player))
connect(multiworld, player, "Route 12-W", "Route 12-N", lambda state: state.has("Poke Flute", player))
connect(multiworld, player, "Route 12-W", "Route 12-S", lambda state: state.has("Poke Flute", player))
connect(multiworld, player, "Route 12-S", "Route 12-Grass", lambda state: logic.can_cut(state, world, player), one_way=True)

View File

@@ -4,59 +4,18 @@ import os
import json
import Utils
from Utils import read_snes_rom
from worlds.Files import APPatchExtension, APProcedurePatch, APTokenMixin, APTokenTypes
from worlds.Files import APDeltaPatch
from .variaRandomizer.utils.utils import openFile
SMJUHASH = '21f3e98df4780ee1c667b84e57d88675'
SM_ROM_MAX_PLAYERID = 65535
SM_ROM_PLAYERDATA_COUNT = 202
class SMPatchExtensions(APPatchExtension):
game = "Super Metroid"
@staticmethod
def write_crc(caller: APProcedurePatch, rom: bytes) -> bytes:
def checksum_mirror_sum(start, length, mask = 0x800000):
while not(length & mask) and mask:
mask >>= 1
part1 = sum(start[:mask]) & 0xFFFF
part2 = 0
next_length = length - mask
if next_length:
part2 = checksum_mirror_sum(start[mask:], next_length, mask >> 1)
while (next_length < mask):
next_length += next_length
part2 += part2
return (part1 + part2) & 0xFFFF
def write_bytes(buffer, startaddress: int, values):
buffer[startaddress:startaddress + len(values)] = values
buffer = bytearray(rom)
crc = checksum_mirror_sum(buffer, len(buffer))
inv = crc ^ 0xFFFF
write_bytes(buffer, 0x7FDC, [inv & 0xFF, (inv >> 8) & 0xFF, crc & 0xFF, (crc >> 8) & 0xFF])
return bytes(buffer)
class SMProcedurePatch(APProcedurePatch, APTokenMixin):
class SMDeltaPatch(APDeltaPatch):
hash = SMJUHASH
game = "Super Metroid"
patch_file_ending = ".apsm"
procedure = [
("apply_tokens", ["token_data.bin"]),
("write_crc", [])
]
def write_tokens(self, patches):
for addr, data in patches.items():
self.write_token(APTokenTypes.WRITE, addr, bytes(data))
self.write_file("token_data.bin", self.get_token_binary())
@classmethod
def get_source_data(cls) -> bytes:
return get_base_rom_bytes()

View File

@@ -17,7 +17,7 @@ logger = logging.getLogger("Super Metroid")
from .Options import SMOptions
from .Client import SMSNIClient
from .Rom import SM_ROM_MAX_PLAYERID, SM_ROM_PLAYERDATA_COUNT, SMProcedurePatch, get_sm_symbols
from .Rom import get_base_rom_path, SM_ROM_MAX_PLAYERID, SM_ROM_PLAYERDATA_COUNT, SMDeltaPatch, get_sm_symbols
import Utils
from .variaRandomizer.logic.smboolmanager import SMBoolManager
@@ -40,7 +40,7 @@ class SMSettings(settings.Group):
"""File name of the v1.0 J rom"""
description = "Super Metroid (JU) ROM"
copy_to = "Super Metroid (JU).sfc"
md5s = [SMProcedurePatch.hash]
md5s = [SMDeltaPatch.hash]
rom_file: RomFile = RomFile(RomFile.copy_to)
@@ -120,6 +120,12 @@ class SMWorld(World):
self.locations = {}
super().__init__(world, player)
@classmethod
def stage_assert_generate(cls, multiworld: MultiWorld):
rom_file = get_base_rom_path()
if not os.path.exists(rom_file):
raise FileNotFoundError(rom_file)
def generate_early(self):
Logic.factory('vanilla')
@@ -796,19 +802,23 @@ class SMWorld(World):
romPatcher.end()
def generate_output(self, output_directory: str):
try:
patcher = self.variaRando.PatchRom(self.APPrePatchRom, self.APPostPatchRom)
self.rom_name = self.romName
self.variaRando.args.rom = get_base_rom_path()
outfilebase = self.multiworld.get_out_file_name_base(self.player)
outputFilename = os.path.join(output_directory, f"{outfilebase}.sfc")
patch = SMProcedurePatch(player=self.player, player_name=self.multiworld.player_name[self.player])
patch.write_tokens(patcher.romFile.getPatchDict())
rom_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}"
f"{patch.patch_file_ending}")
patch.write(rom_path)
try:
self.variaRando.PatchRom(outputFilename, self.APPrePatchRom, self.APPostPatchRom)
self.write_crc(outputFilename)
self.rom_name = self.romName
except:
raise
else:
patch = SMDeltaPatch(os.path.splitext(outputFilename)[0] + SMDeltaPatch.patch_file_ending, player=self.player,
player_name=self.multiworld.player_name[self.player], patched_path=outputFilename)
patch.write()
finally:
if os.path.exists(outputFilename):
os.unlink(outputFilename)
self.rom_name_available_event.set() # make sure threading continues and errors are collected
def checksum_mirror_sum(self, start, length, mask = 0x800000):

View File

@@ -680,7 +680,7 @@ class VariaRandomizer:
#dumpErrorMsg(args.output, self.randoExec.errorMsg)
raise Exception("Can't generate " + self.fileName + " with the given parameters: {}".format(self.randoExec.errorMsg))
def PatchRom(self, customPrePatchApply = None, customPostPatchApply = None) -> RomPatcher:
def PatchRom(self, outputFilename, customPrePatchApply = None, customPostPatchApply = None):
args = self.args
optErrMsgs = self.optErrMsgs
@@ -758,9 +758,9 @@ class VariaRandomizer:
# args.output is not None: generate local json named args.output
if args.rom is not None:
# patch local rom
# romFileName = args.rom
# shutil.copyfile(romFileName, outputFilename)
romPatcher = RomPatcher(settings=patcherSettings, magic=args.raceMagic, player=self.player)
romFileName = args.rom
shutil.copyfile(romFileName, outputFilename)
romPatcher = RomPatcher(settings=patcherSettings, romFileName=outputFilename, magic=args.raceMagic, player=self.player)
else:
romPatcher = RomPatcher(settings=patcherSettings, magic=args.raceMagic)
@@ -779,12 +779,24 @@ class VariaRandomizer:
#msg = randoExec.errorMsg
msg = ''
return romPatcher
if args.rom is None: # web mode
data = romPatcher.romFile.data
self.fileName = '{}.sfc'.format(self.fileName)
data["fileName"] = self.fileName
# error msg in json to be displayed by the web site
data["errorMsg"] = msg
# replaced parameters to update stats in database
if len(self.forcedArgs) > 0:
data["forcedArgs"] = self.forcedArgs
with open(outputFilename, 'w') as jsonFile:
json.dump(data, jsonFile)
else: # CLI mode
if msg != "":
print(msg)
except Exception as e:
import traceback
traceback.print_exc(file=sys.stdout)
raise Exception("Error patching: ({}: {})".format(type(e).__name__, e))
raise Exception("Error patching {}: ({}: {})".format(outputFilename, type(e).__name__, e))
#dumpErrorMsg(args.output, msg)
# if stuck == True:

View File

@@ -21,23 +21,10 @@ class IPS_Patch(object):
def toDict(self):
ret = {}
for record in self.records:
if record['address'] in ret.keys():
if 'rle_count' in record:
if len(ret[record['address']]) > record['rle_count']:
ret[record['address']][:record['rle_count']] = [int.from_bytes(record['data'],'little')]*record['rle_count']
else:
ret[record['address']] = [int.from_bytes(record['data'],'little')]*record['rle_count']
else:
size = len(record['data'])
if len(ret[record['address']]) > size:
ret[record['address']][:size] = [int(b) for b in record['data']]
else:
ret[record['address']] = [int(b) for b in record['data']]
if 'rle_count' in record:
ret[record['address']] = [int.from_bytes(record['data'],'little')]*record['rle_count']
else:
if 'rle_count' in record:
ret[record['address']] = [int.from_bytes(record['data'],'little')]*record['rle_count']
else:
ret[record['address']] = [int(b) for b in record['data']]
ret[record['address']] = [int(b) for b in record['data']]
return ret
@staticmethod

View File

@@ -86,67 +86,7 @@ class ROM(object):
self.seek(self.maxAddress + BANK_SIZE - off - 1)
self.writeByte(0xff)
assert (self.maxAddress % BANK_SIZE) == 0
class FakeROM(ROM):
# to have the same code for real ROM and the webservice
def __init__(self, data={}):
super(FakeROM, self).__init__()
self.data = data
self.ipsPatches = []
def write(self, bytes):
for byte in bytes:
self.data[self.address] = byte
self.inc()
def read(self, byteCount):
bytes = []
for i in range(byteCount):
bytes.append(self.data[self.address])
self.inc()
return bytes
def ipsPatch(self, ipsPatches):
self.ipsPatches += ipsPatches
# generate ips from self data
def ips(self):
groupedData = {}
startAddress = -1
prevAddress = -1
curData = []
for address in sorted(self.data):
if address == prevAddress + 1:
curData.append(self.data[address])
prevAddress = address
else:
if len(curData) > 0:
groupedData[startAddress] = curData
startAddress = address
prevAddress = address
curData = [self.data[startAddress]]
if startAddress != -1:
groupedData[startAddress] = curData
return IPS_Patch(groupedData)
# generate final IPS for web patching with first the IPS patches, then written data
def close(self):
self.mergedIPS = IPS_Patch()
for ips in self.ipsPatches:
self.mergedIPS.append(ips)
self.mergedIPS.append(self.ips())
#patchData = mergedIPS.encode()
#self.data = {}
#self.data["ips"] = base64.b64encode(patchData).decode()
#if mergedIPS.truncate_length is not None:
# self.data["truncate_length"] = mergedIPS.truncate_length
#self.data["max_size"] = mergedIPS.max_size
def getPatchDict(self):
return self.mergedIPS.toDict()
class RealROM(ROM):
def __init__(self, name):
super(RealROM, self).__init__()

View File

@@ -7,7 +7,7 @@ from ..utils.doorsmanager import DoorsManager, IndicatorFlag
from ..utils.objectives import Objectives
from ..graph.graph_utils import GraphUtils, getAccessPoint, locIdsByAreaAddresses, graphAreas
from ..logic.logic import Logic
from ..rom.rom import FakeROM, snes_to_pc, pc_to_snes
from ..rom.rom import RealROM, snes_to_pc, pc_to_snes
from ..rom.addresses import Addresses
from ..rom.rom_patches import RomPatches
from ..patches.patchaccess import PatchAccess
@@ -52,10 +52,10 @@ class RomPatcher:
def __init__(self, settings=None, romFileName=None, magic=None, player=0):
self.log = log.get('RomPatcher')
self.settings = settings
#self.romFileName = romFileName
self.romFileName = romFileName
self.patchAccess = PatchAccess()
self.race = None
self.romFile = FakeROM()
self.romFile = RealROM(romFileName)
#if magic is not None:
# from rom.race_mode import RaceModePatcher
# self.race = RaceModePatcher(self, magic)
@@ -312,7 +312,7 @@ class RomPatcher:
self.applyStartAP(self.settings["startLocation"], plms, doors)
self.applyPLMs(plms)
except Exception as e:
raise Exception("Error patching. ({})".format(e))
raise Exception("Error patching {}. ({})".format(self.romFileName, e))
def applyIPSPatch(self, patchName, patchDict=None, ipsDir=None):
if patchDict is None:
@@ -493,7 +493,6 @@ class RomPatcher:
def commitIPS(self):
self.romFile.ipsPatch(self.ipsPatches)
self.ipsPatches = []
def writeSeed(self, seed):
random.seed(seed)

View File

@@ -772,7 +772,6 @@ if 'unittest' in sys.modules.keys() or 'pytest' in sys.modules.keys():
class Mods(OptionSet):
"""List of mods that will be included in the shuffling."""
visibility = Visibility.all & ~Visibility.simple_ui
internal_name = "mods"
display_name = "Mods"
valid_keys = {ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack,

View File

@@ -1,19 +1,22 @@
from __future__ import annotations
import logging
import itertools
from typing import List, Dict, Any, cast
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification
from worlds.AutoWorld import World, WebWorld
from worlds.AutoWorld import World, WebWorld, FillerReason
from . import items
from . import locations
from . import creatures
from . import options
from .items import item_table, group_items
from .items import item_table, group_items, items_by_type, ItemType
from .rules import set_rules
logger = logging.getLogger("Subnautica")
class SubnauticaWeb(WebWorld):
class SubnaticaWeb(WebWorld):
tutorials = [Tutorial(
"Multiworld Setup Guide",
"A guide to setting up the Subnautica randomizer connected to an Archipelago Multiworld",
@@ -35,7 +38,7 @@ class SubnauticaWorld(World):
You must find a cure for yourself, build an escape rocket, and leave the planet.
"""
game = "Subnautica"
web = SubnauticaWeb()
web = SubnaticaWeb()
item_name_to_id = {data.name: item_id for item_id, data in items.item_table.items()}
location_name_to_id = all_locations
@@ -139,7 +142,7 @@ class SubnauticaWorld(World):
# resource bundle filler
for _ in range(extras):
item = self.create_filler()
item = self.create_filler(FillerReason.world)
item = cast(SubnauticaItem, item)
pool.append(item)

View File

@@ -90,10 +90,6 @@ class TunicWorld(World):
item_link_locations: Dict[int, Dict[str, List[Tuple[int, str]]]] = {}
player_item_link_locations: Dict[str, List[Location]]
using_ut: bool # so we can check if we're using UT only once
passthrough: Dict[str, Any]
ut_can_gen_without_yaml = True # class var that tells it to ignore the player yaml
def generate_early(self) -> None:
if self.options.logic_rules >= LogicRules.option_no_major_glitches:
self.options.laurels_zips.value = LaurelsZips.option_true
@@ -117,28 +113,23 @@ class TunicWorld(World):
# Universal tracker stuff, shouldn't do anything in standard gen
if hasattr(self.multiworld, "re_gen_passthrough"):
if "TUNIC" in self.multiworld.re_gen_passthrough:
self.using_ut = True
self.passthrough = self.multiworld.re_gen_passthrough["TUNIC"]
self.options.start_with_sword.value = self.passthrough["start_with_sword"]
self.options.keys_behind_bosses.value = self.passthrough["keys_behind_bosses"]
self.options.sword_progression.value = self.passthrough["sword_progression"]
self.options.ability_shuffling.value = self.passthrough["ability_shuffling"]
self.options.laurels_zips.value = self.passthrough["laurels_zips"]
self.options.ice_grappling.value = self.passthrough["ice_grappling"]
self.options.ladder_storage.value = self.passthrough["ladder_storage"]
self.options.ladder_storage_without_items = self.passthrough["ladder_storage_without_items"]
self.options.lanternless.value = self.passthrough["lanternless"]
self.options.maskless.value = self.passthrough["maskless"]
self.options.hexagon_quest.value = self.passthrough["hexagon_quest"]
self.options.entrance_rando.value = self.passthrough["entrance_rando"]
self.options.shuffle_ladders.value = self.passthrough["shuffle_ladders"]
passthrough = self.multiworld.re_gen_passthrough["TUNIC"]
self.options.start_with_sword.value = passthrough["start_with_sword"]
self.options.keys_behind_bosses.value = passthrough["keys_behind_bosses"]
self.options.sword_progression.value = passthrough["sword_progression"]
self.options.ability_shuffling.value = passthrough["ability_shuffling"]
self.options.laurels_zips.value = passthrough["laurels_zips"]
self.options.ice_grappling.value = passthrough["ice_grappling"]
self.options.ladder_storage.value = passthrough["ladder_storage"]
self.options.ladder_storage_without_items = passthrough["ladder_storage_without_items"]
self.options.lanternless.value = passthrough["lanternless"]
self.options.maskless.value = passthrough["maskless"]
self.options.hexagon_quest.value = passthrough["hexagon_quest"]
self.options.entrance_rando.value = passthrough["entrance_rando"]
self.options.shuffle_ladders.value = passthrough["shuffle_ladders"]
self.options.fixed_shop.value = self.options.fixed_shop.option_false
self.options.laurels_location.value = self.options.laurels_location.option_anywhere
self.options.combat_logic.value = self.passthrough["combat_logic"]
else:
self.using_ut = False
else:
self.using_ut = False
self.options.combat_logic.value = passthrough["combat_logic"]
@classmethod
def stage_generate_early(cls, multiworld: MultiWorld) -> None:
@@ -340,10 +331,12 @@ class TunicWorld(World):
self.ability_unlocks = randomize_ability_unlocks(self.random, self.options)
# stuff for universal tracker support, can be ignored for standard gen
if self.using_ut:
self.ability_unlocks["Pages 24-25 (Prayer)"] = self.passthrough["Hexagon Quest Prayer"]
self.ability_unlocks["Pages 42-43 (Holy Cross)"] = self.passthrough["Hexagon Quest Holy Cross"]
self.ability_unlocks["Pages 52-53 (Icebolt)"] = self.passthrough["Hexagon Quest Icebolt"]
if hasattr(self.multiworld, "re_gen_passthrough"):
if "TUNIC" in self.multiworld.re_gen_passthrough:
passthrough = self.multiworld.re_gen_passthrough["TUNIC"]
self.ability_unlocks["Pages 24-25 (Prayer)"] = passthrough["Hexagon Quest Prayer"]
self.ability_unlocks["Pages 42-43 (Holy Cross)"] = passthrough["Hexagon Quest Holy Cross"]
self.ability_unlocks["Pages 52-53 (Icebolt)"] = passthrough["Hexagon Quest Icebolt"]
# Ladders and Combat Logic uses ER rules with vanilla connections for easier maintenance
if self.options.entrance_rando or self.options.shuffle_ladders or self.options.combat_logic:

View File

@@ -1675,7 +1675,7 @@ def set_er_location_rules(world: "TunicWorld") -> None:
# Beneath the Vault
set_rule(world.get_location("Beneath the Fortress - Bridge"),
lambda state: has_melee(state, player) or state.has_any((laurels, fire_wand, ice_dagger, gun), player))
lambda state: has_melee(state, player) or state.has_any({laurels, fire_wand}, player))
# Quarry
set_rule(world.get_location("Quarry - [Central] Above Ladder Dash Chest"),

View File

@@ -177,7 +177,7 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
logic_tricks: Tuple[bool, int, int] = (laurels_zips, ice_grappling, ladder_storage)
# marking that you don't immediately have laurels
if laurels_location == "10_fairies" and not world.using_ut:
if laurels_location == "10_fairies" and not hasattr(world.multiworld, "re_gen_passthrough"):
has_laurels = False
shop_count = 6
@@ -191,8 +191,9 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
break
# If using Universal Tracker, restore portal_map. Could be cleaner, but it does not matter for UT even a little bit
if world.using_ut:
portal_map = portal_mapping.copy()
if hasattr(world.multiworld, "re_gen_passthrough"):
if "TUNIC" in world.multiworld.re_gen_passthrough:
portal_map = portal_mapping.copy()
# create separate lists for dead ends and non-dead ends
for portal in portal_map:
@@ -231,24 +232,25 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
plando_connections = world.seed_groups[world.options.entrance_rando.value]["plando"]
# universal tracker support stuff, don't need to care about region dependency
if world.using_ut:
plando_connections.clear()
# universal tracker stuff, won't do anything in normal gen
for portal1, portal2 in world.passthrough["Entrance Rando"].items():
portal_name1 = ""
portal_name2 = ""
if hasattr(world.multiworld, "re_gen_passthrough"):
if "TUNIC" in world.multiworld.re_gen_passthrough:
plando_connections.clear()
# universal tracker stuff, won't do anything in normal gen
for portal1, portal2 in world.multiworld.re_gen_passthrough["TUNIC"]["Entrance Rando"].items():
portal_name1 = ""
portal_name2 = ""
for portal in portal_mapping:
if portal.scene_destination() == portal1:
portal_name1 = portal.name
# connected_regions.update(add_dependent_regions(portal.region, logic_rules))
if portal.scene_destination() == portal2:
portal_name2 = portal.name
# connected_regions.update(add_dependent_regions(portal.region, logic_rules))
# shops have special handling
if not portal_name2 and portal2 == "Shop, Previous Region_":
portal_name2 = "Shop Portal"
plando_connections.append(PlandoConnection(portal_name1, portal_name2, "both"))
for portal in portal_mapping:
if portal.scene_destination() == portal1:
portal_name1 = portal.name
# connected_regions.update(add_dependent_regions(portal.region, logic_rules))
if portal.scene_destination() == portal2:
portal_name2 = portal.name
# connected_regions.update(add_dependent_regions(portal.region, logic_rules))
# shops have special handling
if not portal_name2 and portal2 == "Shop, Previous Region_":
portal_name2 = "Shop Portal"
plando_connections.append(PlandoConnection(portal_name1, portal_name2, "both"))
non_dead_end_regions = set()
for region_name, region_info in world.er_regions.items():
@@ -360,7 +362,7 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
# if we have plando connections, our connected regions may change somewhat
connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_tricks)
if fixed_shop and not world.using_ut:
if fixed_shop and not hasattr(world.multiworld, "re_gen_passthrough"):
portal1 = None
for portal in two_plus:
if portal.scene_destination() == "Overworld Redux, Windmill_":
@@ -390,7 +392,7 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
fail_count = 0
while len(connected_regions) < len(non_dead_end_regions):
# if this is universal tracker, just break immediately and move on
if world.using_ut:
if hasattr(world.multiworld, "re_gen_passthrough"):
break
# if the connected regions length stays unchanged for too long, it's stuck in a loop
# should, hopefully, only ever occur if someone plandos connections poorly
@@ -443,8 +445,9 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
random_object.shuffle(two_plus)
# for universal tracker, we want to skip shop gen
if world.using_ut:
shop_count = 0
if hasattr(world.multiworld, "re_gen_passthrough"):
if "TUNIC" in world.multiworld.re_gen_passthrough:
shop_count = 0
for i in range(shop_count):
portal1 = two_plus.pop()
@@ -459,7 +462,7 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
# connect dead ends to random non-dead ends
# none of the key events are in dead ends, so we don't need to do gate_before_switch
while len(dead_ends) > 0:
if world.using_ut:
if hasattr(world.multiworld, "re_gen_passthrough"):
break
portal1 = two_plus.pop()
portal2 = dead_ends.pop()
@@ -467,7 +470,7 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
# then randomly connect the remaining portals to each other
# every region is accessible, so gate_before_switch is not necessary
while len(two_plus) > 1:
if world.using_ut:
if hasattr(world.multiworld, "re_gen_passthrough"):
break
portal1 = two_plus.pop()
portal2 = two_plus.pop()

View File

@@ -323,7 +323,7 @@ def set_location_rules(world: "TunicWorld") -> None:
# Beneath the Vault
set_rule(world.get_location("Beneath the Fortress - Bridge"),
lambda state: has_melee(state, player) or state.has_any((laurels, fire_wand, ice_dagger, gun), player))
lambda state: has_melee(state, player) or state.has_any({laurels, fire_wand}, player))
set_rule(world.get_location("Beneath the Fortress - Obscured Behind Waterfall"),
lambda state: has_melee(state, player) and has_lantern(state, world))