Skip to content

Commit

Permalink
Merge branch 'main' into mlss
Browse files Browse the repository at this point in the history
  • Loading branch information
jamesbrq authored Mar 15, 2024
2 parents 2ba5508 + f7da833 commit 9772bf5
Show file tree
Hide file tree
Showing 58 changed files with 14,918 additions and 3,777 deletions.
2 changes: 2 additions & 0 deletions CommonClient.py
Original file line number Diff line number Diff line change
Expand Up @@ -758,8 +758,10 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
elif cmd == 'ConnectionRefused':
errors = args["errors"]
if 'InvalidSlot' in errors:
ctx.disconnected_intentionally = True
ctx.event_invalid_slot()
elif 'InvalidGame' in errors:
ctx.disconnected_intentionally = True
ctx.event_invalid_game()
elif 'IncompatibleVersion' in errors:
raise Exception('Server reported your client version as incompatible. '
Expand Down
4 changes: 2 additions & 2 deletions Patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import ModuleUpdate
ModuleUpdate.update()

from worlds.Files import AutoPatchRegister, APPatch
from worlds.Files import AutoPatchRegister, APAutoPatchInterface


class RomMeta(TypedDict):
Expand All @@ -20,7 +20,7 @@ class RomMeta(TypedDict):
def create_rom_file(patch_file: str) -> Tuple[RomMeta, str]:
auto_handler = AutoPatchRegister.get_handler(patch_file)
if auto_handler:
handler: APPatch = auto_handler(patch_file)
handler: APAutoPatchInterface = auto_handler(patch_file)
target = os.path.splitext(patch_file)[0]+handler.result_file_ending
handler.patch(target)
return {"server": handler.server,
Expand Down
39 changes: 29 additions & 10 deletions worlds/Files.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import os
import threading

from typing import ClassVar, Dict, Tuple, Any, Optional, Union, BinaryIO
from typing import ClassVar, Dict, List, Literal, Tuple, Any, Optional, Union, BinaryIO

import bsdiff4

Expand Down Expand Up @@ -38,7 +38,7 @@ def get_handler(file: str) -> Optional[AutoPatchRegister]:
return None


current_patch_version: int = 5
container_version: int = 6


class InvalidDataError(Exception):
Expand All @@ -50,7 +50,7 @@ class InvalidDataError(Exception):

class APContainer:
"""A zipfile containing at least archipelago.json"""
version: int = current_patch_version
version: int = container_version
compression_level: int = 9
compression_method: int = zipfile.ZIP_DEFLATED
game: Optional[str] = None
Expand Down Expand Up @@ -124,14 +124,31 @@ def get_manifest(self) -> Dict[str, Any]:
"game": self.game,
# minimum version of patch system expected for patching to be successful
"compatible_version": 5,
"version": current_patch_version,
"version": container_version,
}


class APPatch(APContainer, abc.ABC, metaclass=AutoPatchRegister):
class APPatch(APContainer):
"""
An abstract `APContainer` that defines the requirements for an object
to be used by the `Patch.create_rom_file` function.
An `APContainer` that represents a patch file.
It includes the `procedure` key in the manifest to indicate that it is a patch.
Your implementation should inherit from this if your output file
represents a patch file, but will not be applied with AP's `Patch.py`
"""
procedure: Union[Literal["custom"], List[Tuple[str, List[Any]]]] = "custom"

def get_manifest(self) -> Dict[str, Any]:
manifest = super(APPatch, self).get_manifest()
manifest["procedure"] = self.procedure
manifest["compatible_version"] = 6
return manifest


class APAutoPatchInterface(APPatch, abc.ABC, metaclass=AutoPatchRegister):
"""
An abstract `APPatch` that defines the requirements for a patch
to be applied with AP's `Patch.py`
"""
result_file_ending: str = ".sfc"

Expand All @@ -140,14 +157,15 @@ def patch(self, target: str) -> None:
""" create the output file with the file name `target` """


class APDeltaPatch(APPatch):
"""An APPatch that additionally has delta.bsdiff4
containing a delta patch to get the desired file, often a rom."""
class APDeltaPatch(APAutoPatchInterface):
"""An implementation of `APAutoPatchInterface` that additionally
has delta.bsdiff4 containing a delta patch to get the desired file."""

hash: Optional[str] # base checksum of source file
patch_file_ending: str = ""
delta: Optional[bytes] = None
source_data: bytes
procedure = None # delete this line when APPP is added

def __init__(self, *args: Any, patched_path: str = "", **kwargs: Any) -> None:
self.patched_path = patched_path
Expand All @@ -158,6 +176,7 @@ def get_manifest(self) -> Dict[str, Any]:
manifest["base_checksum"] = self.hash
manifest["result_file_ending"] = self.result_file_ending
manifest["patch_file_ending"] = self.patch_file_ending
manifest["compatible_version"] = 5 # delete this line when APPP is added
return manifest

@classmethod
Expand Down
4 changes: 2 additions & 2 deletions worlds/adventure/Rom.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import Utils
from .Locations import AdventureLocation, LocationData
from settings import get_settings
from worlds.Files import APDeltaPatch, AutoPatchRegister, APContainer
from worlds.Files import APPatch, AutoPatchRegister

import bsdiff4

Expand Down Expand Up @@ -78,7 +78,7 @@ def get_dict(self):
return ret_dict


class AdventureDeltaPatch(APContainer, metaclass=AutoPatchRegister):
class AdventureDeltaPatch(APPatch, metaclass=AutoPatchRegister):
hash = ADVENTUREHASH
game = "Adventure"
patch_file_ending = ".apadvn"
Expand Down
6 changes: 3 additions & 3 deletions worlds/ffmq/Output.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from copy import deepcopy
from .Regions import object_id_table
from Utils import __version__
from worlds.Files import APContainer
from worlds.Files import APPatch
import pkgutil

settings_template = yaml.load(pkgutil.get_data(__name__, "data/settings.yaml"), yaml.Loader)
Expand Down Expand Up @@ -116,10 +116,10 @@ def tf(option):
APMQ.write_contents(zf)


class APMQFile(APContainer):
class APMQFile(APPatch):
game = "Final Fantasy Mystic Quest"

def get_manifest(self):
manifest = super().get_manifest()
manifest["patch_file_ending"] = ".apmq"
return manifest
return manifest
1 change: 1 addition & 0 deletions worlds/hk/Items.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class HKItemData(NamedTuple):
"GeoChests": lookup_type_to_names["Geo"],
"GeoRocks": lookup_type_to_names["Rock"],
"GrimmkinFlames": lookup_type_to_names["Flame"],
"Grimmchild": {"Grimmchild1", "Grimmchild2"},
"Grubs": lookup_type_to_names["Grub"],
"JournalEntries": lookup_type_to_names["Journal"],
"JunkPitChests": lookup_type_to_names["JunkPitChest"],
Expand Down
11 changes: 10 additions & 1 deletion worlds/hk/Options.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,15 @@ class ExtraPlatforms(DefaultOnToggle):
"""Places additional platforms to make traveling throughout Hallownest more convenient."""


class AddUnshuffledLocations(Toggle):
"""Adds non-randomized locations to the location pool, which allows syncing
of location state with co-op or automatic collection via collect.
Note: This will increase the number of location checks required to purchase
hints to the total maximum.
"""


class DeathLinkShade(Choice):
"""Sets whether to create a shade when you are killed by a DeathLink and how to handle your existing shade, if any.
Expand Down Expand Up @@ -488,7 +497,7 @@ class CostSanityHybridChance(Range):
**{
option.__name__: option
for option in (
StartLocation, Goal, WhitePalace, ExtraPlatforms, StartingGeo,
StartLocation, Goal, WhitePalace, ExtraPlatforms, AddUnshuffledLocations, StartingGeo,
DeathLink, DeathLinkShade, DeathLinkBreaksFragileCharms,
MinimumGeoPrice, MaximumGeoPrice,
MinimumGrubPrice, MaximumGrubPrice,
Expand Down
37 changes: 26 additions & 11 deletions worlds/hk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ def create_items(self):
randomized_starting_items.update(items)

# noinspection PyShadowingNames
def _add(item_name: str, location_name: str):
def _add(item_name: str, location_name: str, randomized: bool):
"""
Adds a pairing of an item and location, doing appropriate checks to see if it should be vanilla or not.
"""
Expand All @@ -252,7 +252,7 @@ def _add(item_name: str, location_name: str):
if item_name in junk_replace:
item_name = self.get_filler_item_name()

item = self.create_item(item_name)
item = self.create_item(item_name) if not vanilla or location_name == "Start" or self.multiworld.AddUnshuffledLocations[self.player] else self.create_event(item_name)

if location_name == "Start":
if item_name in randomized_starting_items:
Expand All @@ -277,30 +277,35 @@ def _add(item_name: str, location_name: str):

for option_key, option in hollow_knight_randomize_options.items():
randomized = getattr(self.multiworld, option_key)[self.player]
if all([not randomized, option_key in logicless_options, not self.multiworld.AddUnshuffledLocations[self.player]]):
continue
for item_name, location_name in zip(option.items, option.locations):
if item_name in junk_replace:
item_name = self.get_filler_item_name()

if (item_name == "Crystal_Heart" and self.multiworld.SplitCrystalHeart[self.player]) or \
(item_name == "Mothwing_Cloak" and self.multiworld.SplitMothwingCloak[self.player]):
_add("Left_" + item_name, location_name)
_add("Right_" + item_name, "Split_" + location_name)
_add("Left_" + item_name, location_name, randomized)
_add("Right_" + item_name, "Split_" + location_name, randomized)
continue
if item_name == "Mantis_Claw" and self.multiworld.SplitMantisClaw[self.player]:
_add("Left_" + item_name, "Left_" + location_name)
_add("Right_" + item_name, "Right_" + location_name)
_add("Left_" + item_name, "Left_" + location_name, randomized)
_add("Right_" + item_name, "Right_" + location_name, randomized)
continue
if item_name == "Shade_Cloak" and self.multiworld.SplitMothwingCloak[self.player]:
if self.multiworld.random.randint(0, 1):
item_name = "Left_Mothwing_Cloak"
else:
item_name = "Right_Mothwing_Cloak"
if item_name == "Grimmchild2" and self.multiworld.RandomizeGrimmkinFlames[self.player] and self.multiworld.RandomizeCharms[self.player]:
_add("Grimmchild1", location_name, randomized)
continue

_add(item_name, location_name)
_add(item_name, location_name, randomized)

if self.multiworld.RandomizeElevatorPass[self.player]:
randomized = True
_add("Elevator_Pass", "Elevator_Pass")
_add("Elevator_Pass", "Elevator_Pass", randomized)

for shop, locations in self.created_multi_locations.items():
for _ in range(len(locations), getattr(self.multiworld, shop_to_option[shop])[self.player].value):
Expand Down Expand Up @@ -475,6 +480,10 @@ def create_item(self, name: str) -> HKItem:
item_data = item_table[name]
return HKItem(name, item_data.advancement, item_data.id, item_data.type, self.player)

def create_event(self, name: str) -> HKItem:
item_data = item_table[name]
return HKItem(name, item_data.advancement, None, item_data.type, self.player)

def create_location(self, name: str, vanilla=False) -> HKLocation:
costs = None
basename = name
Expand All @@ -493,9 +502,15 @@ def create_location(self, name: str, vanilla=False) -> HKLocation:
name = f"{name}_{i}"

region = self.multiworld.get_region("Menu", self.player)
loc = HKLocation(self.player, name,
self.location_name_to_id[name], region, costs=costs, vanilla=vanilla,
basename=basename)

if vanilla and not self.multiworld.AddUnshuffledLocations[self.player]:
loc = HKLocation(self.player, name,
None, region, costs=costs, vanilla=vanilla,
basename=basename)
else:
loc = HKLocation(self.player, name,
self.location_name_to_id[name], region, costs=costs, vanilla=vanilla,
basename=basename)

if multi is not None:
multi.append(loc)
Expand Down
12 changes: 12 additions & 0 deletions worlds/kdl3/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ def pre_fill(self) -> None:
# randomize copy abilities
valid_abilities = list(copy_ability_access_table.keys())
enemies_to_set = list(self.copy_abilities.keys())
unplaced_abilities = set(key for key in copy_ability_access_table.keys()
if key not in ("No Ability", "Cutter Ability", "Burning Ability"))
# now for the edge cases
for abilities, enemies in enemy_restrictive:
available_enemies = list()
Expand All @@ -143,6 +145,7 @@ def pre_fill(self) -> None:
chosen_ability = self.random.choice(abilities)
self.copy_abilities[chosen_enemy] = chosen_ability
enemies_to_set.remove(chosen_enemy)
unplaced_abilities.discard(chosen_ability)
# two less restrictive ones, we need to ensure Cutter and Burning appear before their required stages
sand_canyon_5 = self.get_region("Sand Canyon 5 - 9")
# this is primarily for typing, but if this ever hits it's fine to crash
Expand All @@ -160,6 +163,13 @@ def pre_fill(self) -> None:
if burning_enemy:
self.copy_abilities[burning_enemy] = "Burning Ability"
enemies_to_set.remove(burning_enemy)
# ensure we place one of every ability
if unplaced_abilities and self.options.accessibility != self.options.accessibility.option_minimal:
# failsafe, on non-minimal we need to guarantee every copy ability exists
for ability in sorted(unplaced_abilities):
enemy = self.random.choice(enemies_to_set)
self.copy_abilities[enemy] = ability
enemies_to_set.remove(enemy)
# place remaining
for enemy in enemies_to_set:
self.copy_abilities[enemy] = self.random.choice(valid_abilities)
Expand Down Expand Up @@ -283,6 +293,8 @@ def generate_basic(self) -> None:
self.boss_butch_bosses = [True for _ in range(6)]
else:
self.boss_butch_bosses = [self.random.choice([True, False]) for _ in range(6)]
else:
self.boss_butch_bosses = [False for _ in range(6)]

def generate_output(self, output_directory: str):
rom_path = ""
Expand Down
8 changes: 4 additions & 4 deletions worlds/lingo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@

from BaseClasses import Item, ItemClassification, Tutorial
from worlds.AutoWorld import WebWorld, World
from .datatypes import Room, RoomEntrance
from .items import ALL_ITEM_TABLE, LingoItem
from .locations import ALL_LOCATION_TABLE
from .options import LingoOptions
from .player_logic import LingoPlayerLogic
from .regions import create_regions
from .static_logic import Room, RoomEntrance


class LingoWebWorld(WebWorld):
Expand Down Expand Up @@ -100,9 +100,9 @@ def create_item(self, name: str) -> Item:
item = ALL_ITEM_TABLE[name]

classification = item.classification
if hasattr(self, "options") and self.options.shuffle_paintings and len(item.painting_ids) > 0\
and len(item.door_ids) == 0 and all(painting_id not in self.player_logic.painting_mapping
for painting_id in item.painting_ids)\
if hasattr(self, "options") and self.options.shuffle_paintings and len(item.painting_ids) > 0 \
and not item.has_doors and all(painting_id not in self.player_logic.painting_mapping
for painting_id in item.painting_ids) \
and "pilgrim_painting2" not in item.painting_ids:
# If this is a "door" that just moves one or more paintings, and painting shuffle is on and those paintings
# go nowhere, then this item should not be progression. The Pilgrim Room painting is special and needs to be
Expand Down
5 changes: 5 additions & 0 deletions worlds/lingo/data/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# lingo data

The source of truth for the Lingo randomizer is (currently) the LL1.yaml and ids.yaml files located here. These files are used by the generator, the game client, and the tracker, in order to have logic that is consistent across them all.

The generator does not actually read in the yaml files. Instead, a compiled datafile called generated.dat is also located in this directory. If you update LL1.yaml and/or ids.yaml, you must also regenerate the datafile using `python worlds/lingo/utils/pickle_static_data.py`. A unit test will fail if you don't.
Binary file added worlds/lingo/data/generated.dat
Binary file not shown.
Loading

0 comments on commit 9772bf5

Please sign in to comment.