mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-10 01:23:48 -07:00
Compare commits
11 Commits
core_fille
...
NewSoupVi-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eaf352daaf | ||
|
|
29b34ca9fd | ||
|
|
d97ee5d209 | ||
|
|
c2bd9df0f7 | ||
|
|
112bfe0933 | ||
|
|
96b500679d | ||
|
|
258ea10c52 | ||
|
|
043ba418ec | ||
|
|
894a8571ee | ||
|
|
874197d940 | ||
|
|
d3ed40cd4d |
@@ -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: slot_info.group_members 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()
|
||||
if slot_info.type == SlotType.group}
|
||||
|
||||
self.clients = {0: {}}
|
||||
|
||||
11
Utils.py
11
Utils.py
@@ -152,8 +152,15 @@ def home_path(*path: str) -> str:
|
||||
if hasattr(home_path, 'cached_path'):
|
||||
pass
|
||||
elif sys.platform.startswith('linux'):
|
||||
home_path.cached_path = os.path.expanduser('~/Archipelago')
|
||||
os.makedirs(home_path.cached_path, 0o700, exist_ok=True)
|
||||
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)
|
||||
else:
|
||||
# not implemented
|
||||
home_path.cached_path = local_path() # this will generate the same exceptions we got previously
|
||||
|
||||
@@ -147,3 +147,8 @@
|
||||
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
65
kvui.py
@@ -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
|
||||
from kivy.properties import BooleanProperty, ObjectProperty, NumericProperty
|
||||
from kivy.metrics import dp
|
||||
from kivy.effects.scroll import ScrollEffect
|
||||
from kivy.uix.widget import Widget
|
||||
@@ -64,6 +64,7 @@ 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)
|
||||
@@ -305,6 +306,50 @@ 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)
|
||||
@@ -570,8 +615,10 @@ class GameManager(App):
|
||||
# show Archipelago tab if other logging is present
|
||||
self.tabs.add_widget(panel)
|
||||
|
||||
hint_panel = self.add_client_tab("Hints", HintLog(self.json_to_kivy_parser))
|
||||
hint_panel = self.add_client_tab("Hints", HintLayout())
|
||||
self.hint_log = 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"
|
||||
@@ -698,7 +745,7 @@ class GameManager(App):
|
||||
|
||||
def update_hints(self):
|
||||
hints = self.ctx.stored_data.get(f"_read_hints_{self.ctx.team}_{self.ctx.slot}", [])
|
||||
self.log_panels["Hints"].refresh_hints(hints)
|
||||
self.hint_log.refresh_hints(hints)
|
||||
|
||||
# default F1 keybind, opens a settings menu, that seems to break the layout engine once closed
|
||||
def open_settings(self, *largs):
|
||||
@@ -753,6 +800,17 @@ 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",
|
||||
@@ -769,6 +827,7 @@ status_colors: typing.Dict[HintStatus, str] = {
|
||||
}
|
||||
|
||||
|
||||
|
||||
class HintLog(RecycleView):
|
||||
header = {
|
||||
"receiving": {"text": "[u]Receiving Player[/u]"},
|
||||
|
||||
@@ -7,7 +7,7 @@ schema>=0.7.7
|
||||
kivy>=2.3.0
|
||||
bsdiff4>=1.2.4
|
||||
platformdirs>=4.2.2
|
||||
certifi>=2024.8.30
|
||||
certifi>=2024.12.14
|
||||
cython>=3.0.11
|
||||
cymem>=2.0.8
|
||||
orjson>=3.10.7
|
||||
|
||||
@@ -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))
|
||||
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 7) and can_use_bombs(state, player))
|
||||
set_rule(multiworld.get_entrance('Ganons Tower Moldorm Door', player),
|
||||
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 8))
|
||||
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 8) and can_use_bombs(state, player))
|
||||
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))
|
||||
|
||||
@@ -130,19 +130,21 @@ 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, ['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 - 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 - 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, ['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']],
|
||||
["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']],
|
||||
])
|
||||
@@ -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", lambda state: state.has("Poke Flute", player))
|
||||
connect(multiworld, player, "Route 12-W", "Route 11-E")
|
||||
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)
|
||||
|
||||
@@ -4,18 +4,59 @@ import os
|
||||
import json
|
||||
import Utils
|
||||
from Utils import read_snes_rom
|
||||
from worlds.Files import APDeltaPatch
|
||||
from worlds.Files import APPatchExtension, APProcedurePatch, APTokenMixin, APTokenTypes
|
||||
from .variaRandomizer.utils.utils import openFile
|
||||
|
||||
SMJUHASH = '21f3e98df4780ee1c667b84e57d88675'
|
||||
SM_ROM_MAX_PLAYERID = 65535
|
||||
SM_ROM_PLAYERDATA_COUNT = 202
|
||||
|
||||
class SMDeltaPatch(APDeltaPatch):
|
||||
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):
|
||||
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()
|
||||
|
||||
@@ -17,7 +17,7 @@ logger = logging.getLogger("Super Metroid")
|
||||
|
||||
from .Options import SMOptions
|
||||
from .Client import SMSNIClient
|
||||
from .Rom import get_base_rom_path, SM_ROM_MAX_PLAYERID, SM_ROM_PLAYERDATA_COUNT, SMDeltaPatch, get_sm_symbols
|
||||
from .Rom import SM_ROM_MAX_PLAYERID, SM_ROM_PLAYERDATA_COUNT, SMProcedurePatch, 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 = [SMDeltaPatch.hash]
|
||||
md5s = [SMProcedurePatch.hash]
|
||||
|
||||
rom_file: RomFile = RomFile(RomFile.copy_to)
|
||||
|
||||
@@ -120,12 +120,6 @@ 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')
|
||||
|
||||
@@ -802,23 +796,19 @@ class SMWorld(World):
|
||||
romPatcher.end()
|
||||
|
||||
def generate_output(self, output_directory: str):
|
||||
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")
|
||||
|
||||
try:
|
||||
self.variaRando.PatchRom(outputFilename, self.APPrePatchRom, self.APPostPatchRom)
|
||||
self.write_crc(outputFilename)
|
||||
patcher = self.variaRando.PatchRom(self.APPrePatchRom, self.APPostPatchRom)
|
||||
self.rom_name = self.romName
|
||||
|
||||
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)
|
||||
|
||||
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):
|
||||
|
||||
@@ -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, outputFilename, customPrePatchApply = None, customPostPatchApply = None):
|
||||
def PatchRom(self, customPrePatchApply = None, customPostPatchApply = None) -> RomPatcher:
|
||||
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, romFileName=outputFilename, magic=args.raceMagic, player=self.player)
|
||||
# romFileName = args.rom
|
||||
# shutil.copyfile(romFileName, outputFilename)
|
||||
romPatcher = RomPatcher(settings=patcherSettings, magic=args.raceMagic, player=self.player)
|
||||
else:
|
||||
romPatcher = RomPatcher(settings=patcherSettings, magic=args.raceMagic)
|
||||
|
||||
@@ -779,24 +779,12 @@ class VariaRandomizer:
|
||||
#msg = randoExec.errorMsg
|
||||
msg = ''
|
||||
|
||||
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)
|
||||
return romPatcher
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
traceback.print_exc(file=sys.stdout)
|
||||
raise Exception("Error patching {}: ({}: {})".format(outputFilename, type(e).__name__, e))
|
||||
raise Exception("Error patching: ({}: {})".format(type(e).__name__, e))
|
||||
#dumpErrorMsg(args.output, msg)
|
||||
|
||||
# if stuck == True:
|
||||
|
||||
@@ -21,10 +21,23 @@ class IPS_Patch(object):
|
||||
def toDict(self):
|
||||
ret = {}
|
||||
for record in self.records:
|
||||
if 'rle_count' in record:
|
||||
ret[record['address']] = [int.from_bytes(record['data'],'little')]*record['rle_count']
|
||||
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']]
|
||||
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:
|
||||
ret[record['address']] = [int(b) for b in record['data']]
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -86,7 +86,67 @@ 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__()
|
||||
|
||||
@@ -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 RealROM, snes_to_pc, pc_to_snes
|
||||
from ..rom.rom import FakeROM, 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 = RealROM(romFileName)
|
||||
self.romFile = FakeROM()
|
||||
#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(self.romFileName, e))
|
||||
raise Exception("Error patching. ({})".format(e))
|
||||
|
||||
def applyIPSPatch(self, patchName, patchDict=None, ipsDir=None):
|
||||
if patchDict is None:
|
||||
@@ -493,6 +493,7 @@ class RomPatcher:
|
||||
|
||||
def commitIPS(self):
|
||||
self.romFile.ipsPatch(self.ipsPatches)
|
||||
self.ipsPatches = []
|
||||
|
||||
def writeSeed(self, seed):
|
||||
random.seed(seed)
|
||||
|
||||
@@ -772,6 +772,7 @@ 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,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import itertools
|
||||
from typing import List, Dict, Any, cast
|
||||
|
||||
@@ -10,13 +9,11 @@ from . import items
|
||||
from . import locations
|
||||
from . import creatures
|
||||
from . import options
|
||||
from .items import item_table, group_items, items_by_type, ItemType
|
||||
from .items import item_table, group_items
|
||||
from .rules import set_rules
|
||||
|
||||
logger = logging.getLogger("Subnautica")
|
||||
|
||||
|
||||
class SubnaticaWeb(WebWorld):
|
||||
class SubnauticaWeb(WebWorld):
|
||||
tutorials = [Tutorial(
|
||||
"Multiworld Setup Guide",
|
||||
"A guide to setting up the Subnautica randomizer connected to an Archipelago Multiworld",
|
||||
@@ -38,7 +35,7 @@ class SubnauticaWorld(World):
|
||||
You must find a cure for yourself, build an escape rocket, and leave the planet.
|
||||
"""
|
||||
game = "Subnautica"
|
||||
web = SubnaticaWeb()
|
||||
web = SubnauticaWeb()
|
||||
|
||||
item_name_to_id = {data.name: item_id for item_id, data in items.item_table.items()}
|
||||
location_name_to_id = all_locations
|
||||
|
||||
@@ -90,6 +90,10 @@ 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
|
||||
@@ -113,23 +117,28 @@ 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:
|
||||
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.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"]
|
||||
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 = passthrough["combat_logic"]
|
||||
self.options.combat_logic.value = self.passthrough["combat_logic"]
|
||||
else:
|
||||
self.using_ut = False
|
||||
else:
|
||||
self.using_ut = False
|
||||
|
||||
@classmethod
|
||||
def stage_generate_early(cls, multiworld: MultiWorld) -> None:
|
||||
@@ -331,12 +340,10 @@ 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 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"]
|
||||
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"]
|
||||
|
||||
# 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:
|
||||
|
||||
@@ -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}, player))
|
||||
lambda state: has_melee(state, player) or state.has_any((laurels, fire_wand, ice_dagger, gun), player))
|
||||
|
||||
# Quarry
|
||||
set_rule(world.get_location("Quarry - [Central] Above Ladder Dash Chest"),
|
||||
|
||||
@@ -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 hasattr(world.multiworld, "re_gen_passthrough"):
|
||||
if laurels_location == "10_fairies" and not world.using_ut:
|
||||
has_laurels = False
|
||||
|
||||
shop_count = 6
|
||||
@@ -191,9 +191,8 @@ 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 hasattr(world.multiworld, "re_gen_passthrough"):
|
||||
if "TUNIC" in world.multiworld.re_gen_passthrough:
|
||||
portal_map = portal_mapping.copy()
|
||||
if world.using_ut:
|
||||
portal_map = portal_mapping.copy()
|
||||
|
||||
# create separate lists for dead ends and non-dead ends
|
||||
for portal in portal_map:
|
||||
@@ -232,25 +231,24 @@ 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 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 = ""
|
||||
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 = ""
|
||||
|
||||
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():
|
||||
@@ -362,7 +360,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 hasattr(world.multiworld, "re_gen_passthrough"):
|
||||
if fixed_shop and not world.using_ut:
|
||||
portal1 = None
|
||||
for portal in two_plus:
|
||||
if portal.scene_destination() == "Overworld Redux, Windmill_":
|
||||
@@ -392,7 +390,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 hasattr(world.multiworld, "re_gen_passthrough"):
|
||||
if world.using_ut:
|
||||
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
|
||||
@@ -445,9 +443,8 @@ 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 hasattr(world.multiworld, "re_gen_passthrough"):
|
||||
if "TUNIC" in world.multiworld.re_gen_passthrough:
|
||||
shop_count = 0
|
||||
if world.using_ut:
|
||||
shop_count = 0
|
||||
|
||||
for i in range(shop_count):
|
||||
portal1 = two_plus.pop()
|
||||
@@ -462,7 +459,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 hasattr(world.multiworld, "re_gen_passthrough"):
|
||||
if world.using_ut:
|
||||
break
|
||||
portal1 = two_plus.pop()
|
||||
portal2 = dead_ends.pop()
|
||||
@@ -470,7 +467,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 hasattr(world.multiworld, "re_gen_passthrough"):
|
||||
if world.using_ut:
|
||||
break
|
||||
portal1 = two_plus.pop()
|
||||
portal2 = two_plus.pop()
|
||||
|
||||
@@ -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}, player))
|
||||
lambda state: has_melee(state, player) or state.has_any((laurels, fire_wand, ice_dagger, gun), player))
|
||||
set_rule(world.get_location("Beneath the Fortress - Obscured Behind Waterfall"),
|
||||
lambda state: has_melee(state, player) and has_lantern(state, world))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user