Skip to content

Commit

Permalink
Merge branch 'frlg-stable' of https://github.com/vyneras/Archipelago
Browse files Browse the repository at this point in the history
…into frlg-stable
  • Loading branch information
vyneras committed Jan 25, 2025
2 parents eb309bc + 87fc5ac commit 1a2e81b
Show file tree
Hide file tree
Showing 10 changed files with 142 additions and 57 deletions.
24 changes: 20 additions & 4 deletions MultiServer.py
Original file line number Diff line number Diff line change
Expand Up @@ -1060,21 +1060,37 @@ def send_items_to(ctx: Context, team: int, target_slot: int, *items: NetworkItem

def register_location_checks(ctx: Context, team: int, slot: int, locations: typing.Iterable[int],
count_activity: bool = True):
slot_locations = ctx.locations[slot]
new_locations = set(locations) - ctx.location_checks[team, slot]
new_locations.intersection_update(ctx.locations[slot]) # ignore location IDs unknown to this multidata
new_locations.intersection_update(slot_locations) # ignore location IDs unknown to this multidata
if new_locations:
if count_activity:
ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc)

sortable: list[tuple[int, int, int, int]] = []
for location in new_locations:
item_id, target_player, flags = ctx.locations[slot][location]
# extract all fields to avoid runtime overhead in LocationStore
item_id, target_player, flags = slot_locations[location]
# sort/group by receiver and item
sortable.append((target_player, item_id, location, flags))

info_texts: list[dict[str, typing.Any]] = []
for target_player, item_id, location, flags in sorted(sortable):
new_item = NetworkItem(item_id, location, slot, flags)
send_items_to(ctx, team, target_player, new_item)

ctx.logger.info('(Team #%d) %s sent %s to %s (%s)' % (
team + 1, ctx.player_names[(team, slot)], ctx.item_names[ctx.slot_info[target_player].game][item_id],
ctx.player_names[(team, target_player)], ctx.location_names[ctx.slot_info[slot].game][location]))
info_text = json_format_send_event(new_item, target_player)
ctx.broadcast_team(team, [info_text])
if len(info_texts) >= 140:
# split into chunks that are close to compression window of 64K but not too big on the wire
# (roughly 1300-2600 bytes after compression depending on repetitiveness)
ctx.broadcast_team(team, info_texts)
info_texts.clear()
info_texts.append(json_format_send_event(new_item, target_player))
ctx.broadcast_team(team, info_texts)
del info_texts
del sortable

ctx.location_checks[team, slot] |= new_locations
send_new_items(ctx)
Expand Down
12 changes: 10 additions & 2 deletions worlds/LauncherComponents.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,22 @@ def __repr__(self):
processes = weakref.WeakSet()


def launch_subprocess(func: Callable, name: str = None, args: Tuple[str, ...] = ()) -> None:
def launch_subprocess(func: Callable, name: str | None = None, args: Tuple[str, ...] = ()) -> None:
global processes
import multiprocessing
process = multiprocessing.Process(target=func, name=name, args=args)
process.start()
processes.add(process)


def launch(func: Callable, name: str | None = None, args: Tuple[str, ...] = ()) -> None:
from Utils import is_kivy_running
if is_kivy_running():
launch_subprocess(func, name, args)
else:
func(*args)


class SuffixIdentifier:
suffixes: Iterable[str]

Expand All @@ -111,7 +119,7 @@ def __call__(self, path: str) -> bool:

def launch_textclient(*args):
import CommonClient
launch_subprocess(CommonClient.run_as_textclient, name="TextClient", args=args)
launch(CommonClient.run_as_textclient, name="TextClient", args=args)


def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, pathlib.Path]]:
Expand Down
10 changes: 7 additions & 3 deletions worlds/_bizhawk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ async def lock(ctx) -> None
async def unlock(ctx) -> None
async def get_hash(ctx) -> str
async def get_memory_size(ctx, domain: str) -> int
async def get_system(ctx) -> str
async def get_cores(ctx) -> dict[str, str]
async def ping(ctx) -> None
Expand Down Expand Up @@ -168,9 +169,10 @@ select dialog and they will be associated with BizHawkClient. This does not affe
associate the file extension with Archipelago.

`validate_rom` is called to figure out whether a given ROM belongs to your client. It will only be called when a ROM is
running on a system you specified in your `system` class variable. In most cases, that will be a single system and you
can be sure that you're not about to try to read from nonexistent domains or out of bounds. If you decide to claim this
ROM as yours, this is where you should do setup for things like `items_handling`.
running on a system you specified in your `system` class variable. Take extra care here, because your code will run
against ROMs that you have no control over. If you're reading an address deep in ROM, you might want to check the size
of ROM before you attempt to read it using `get_memory_size`. If you decide to claim this ROM as yours, this is where
you should do setup for things like `items_handling`.

`game_watcher` is the "main loop" of your client where you should be checking memory and sending new items to the ROM.
`BizHawkClient` will make sure that your `game_watcher` only runs when your client has validated the ROM, and will do
Expand Down Expand Up @@ -268,6 +270,8 @@ server connection before trying to interact with it.
- By default, the player will be asked to provide their slot name after connecting to the server and validating, and
that input will be used to authenticate with the `Connect` command. You can override `set_auth` in your own client to
set it automatically based on data in the ROM or on your client instance.
- Use `get_memory_size` inside `validate_rom` if you need to read at large addresses, in case some other game has a
smaller ROM size.
- You can override `on_package` in your client to watch raw packages, but don't forget you also have access to a
subclass of `CommonContext` and its API.
- You can import `BizHawkClientContext` for type hints using `typing.TYPE_CHECKING`. Importing it without conditions at
Expand Down
28 changes: 16 additions & 12 deletions worlds/pokemon_emerald/locations.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@
"EVENT_VISITED_SOUTHERN_ISLAND": 17,
}

BLACKLIST_OPTION_TO_VISITED_EVENT = {
"Slateport City": "EVENT_VISITED_SLATEPORT_CITY",
"Mauville City": "EVENT_VISITED_MAUVILLE_CITY",
"Verdanturf Town": "EVENT_VISITED_VERDANTURF_TOWN",
"Fallarbor Town": "EVENT_VISITED_FALLARBOR_TOWN",
"Lavaridge Town": "EVENT_VISITED_LAVARIDGE_TOWN",
"Fortree City": "EVENT_VISITED_FORTREE_CITY",
"Lilycove City": "EVENT_VISITED_LILYCOVE_CITY",
"Mossdeep City": "EVENT_VISITED_MOSSDEEP_CITY",
"Sootopolis City": "EVENT_VISITED_SOOTOPOLIS_CITY",
"Ever Grande City": "EVENT_VISITED_EVER_GRANDE_CITY",
}

class PokemonEmeraldLocation(Location):
game: str = "Pokemon Emerald"
Expand Down Expand Up @@ -129,18 +141,10 @@ def set_free_fly(world: "PokemonEmeraldWorld") -> None:
# If not enabled, set it to Littleroot Town by default
fly_location_name = "EVENT_VISITED_LITTLEROOT_TOWN"
if world.options.free_fly_location:
fly_location_name = world.random.choice([
"EVENT_VISITED_SLATEPORT_CITY",
"EVENT_VISITED_MAUVILLE_CITY",
"EVENT_VISITED_VERDANTURF_TOWN",
"EVENT_VISITED_FALLARBOR_TOWN",
"EVENT_VISITED_LAVARIDGE_TOWN",
"EVENT_VISITED_FORTREE_CITY",
"EVENT_VISITED_LILYCOVE_CITY",
"EVENT_VISITED_MOSSDEEP_CITY",
"EVENT_VISITED_SOOTOPOLIS_CITY",
"EVENT_VISITED_EVER_GRANDE_CITY",
])
blacklisted_locations = set(BLACKLIST_OPTION_TO_VISITED_EVENT[city] for city in world.options.free_fly_blacklist.value)
free_fly_locations = sorted(set(BLACKLIST_OPTION_TO_VISITED_EVENT.values()) - blacklisted_locations)
if free_fly_locations:
fly_location_name = world.random.choice(free_fly_locations)

world.free_fly_location_id = VISITED_EVENT_NAME_TO_ID[fly_location_name]

Expand Down
19 changes: 19 additions & 0 deletions worlds/pokemon_emerald/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,24 @@ class FreeFlyLocation(Toggle):
"""
display_name = "Free Fly Location"

class FreeFlyBlacklist(OptionSet):
"""
Disables specific locations as valid free fly locations.
Has no effect if Free Fly Location is disabled.
"""
display_name = "Free Fly Blacklist"
valid_keys = [
"Slateport City",
"Mauville City",
"Verdanturf Town",
"Fallarbor Town",
"Lavaridge Town",
"Fortree City",
"Lilycove City",
"Mossdeep City",
"Sootopolis City",
"Ever Grande City",
]

class HmRequirements(Choice):
"""
Expand Down Expand Up @@ -876,6 +894,7 @@ class PokemonEmeraldOptions(PerGameCommonOptions):
extra_bumpy_slope: ExtraBumpySlope
modify_118: ModifyRoute118
free_fly_location: FreeFlyLocation
free_fly_blacklist: FreeFlyBlacklist
hm_requirements: HmRequirements

turbo_a: TurboA
Expand Down
2 changes: 1 addition & 1 deletion worlds/shivers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ def pre_fill(self) -> None:

storage_items += [self.create_item("Empty") for _ in range(3)]

state = self.multiworld.get_all_state(True)
state = self.multiworld.get_all_state(False)

self.random.shuffle(storage_locs)
self.random.shuffle(storage_items)
Expand Down
27 changes: 27 additions & 0 deletions worlds/sm64ex/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,17 @@ class SM64World(World):
filler_count: int
star_costs: typing.Dict[str, int]

# Spoiler specific variable(s)
star_costs_spoiler_key_maxlen = len(max([
'First Floor Big Star Door',
'Basement Big Star Door',
'Second Floor Big Star Door',
'MIPS 1',
'MIPS 2',
'Endless Stairs',
], key=len))


def generate_early(self):
max_stars = 120
if (not self.options.enable_coin_stars):
Expand Down Expand Up @@ -238,3 +249,19 @@ def extend_hint_information(self, hint_data: typing.Dict[int, typing.Dict[int, s
for location in region.locations:
er_hint_data[location.address] = entrance_name
hint_data[self.player] = er_hint_data

def write_spoiler(self, spoiler_handle: typing.TextIO) -> None:
# Write calculated star costs to spoiler.
star_cost_spoiler_header = '\n\n' + self.player_name + ' Star Costs for Super Mario 64:\n\n'
spoiler_handle.write(star_cost_spoiler_header)
# - Reformat star costs dictionary in spoiler to be a bit more readable.
star_costs_spoiler = {}
star_costs_copy = self.star_costs.copy()
star_costs_spoiler['First Floor Big Star Door'] = star_costs_copy['FirstBowserDoorCost']
star_costs_spoiler['Basement Big Star Door'] = star_costs_copy['BasementDoorCost']
star_costs_spoiler['Second Floor Big Star Door'] = star_costs_copy['SecondFloorDoorCost']
star_costs_spoiler['MIPS 1'] = star_costs_copy['MIPS1Cost']
star_costs_spoiler['MIPS 2'] = star_costs_copy['MIPS2Cost']
star_costs_spoiler['Endless Stairs'] = star_costs_copy['StarsToFinish']
for star, cost in star_costs_spoiler.items():
spoiler_handle.write(f"{star:{self.star_costs_spoiler_key_maxlen}s} = {cost}\n")
30 changes: 19 additions & 11 deletions worlds/tunic/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Dict, List, Any, Tuple, TypedDict, ClassVar, Union, Set
from typing import Dict, List, Any, Tuple, TypedDict, ClassVar, Union, Set, TextIO
from logging import warning
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, MultiWorld, CollectionState
from .items import (item_name_to_id, item_table, item_name_groups, fool_tiers, filler_items, slot_data_item_names,
Expand Down Expand Up @@ -242,10 +242,18 @@ def stage_generate_early(cls, multiworld: MultiWorld) -> None:

def create_item(self, name: str, classification: ItemClassification = None) -> TunicItem:
item_data = item_table[name]
# if item_data.combat_ic is None, it'll take item_data.classification instead
itemclass: ItemClassification = ((item_data.combat_ic if self.options.combat_logic else None)
# evaluate alternate classifications based on options
# it'll choose whichever classification isn't None first in this if else tree
itemclass: ItemClassification = (classification
or (item_data.combat_ic if self.options.combat_logic else None)
or (ItemClassification.progression | ItemClassification.useful
if name == "Glass Cannon" and self.options.grass_randomizer
and not self.options.start_with_sword else None)
or (ItemClassification.progression | ItemClassification.useful
if name == "Shield" and self.options.ladder_storage
and not self.options.ladder_storage_without_items else None)
or item_data.classification)
return TunicItem(name, classification or itemclass, self.item_name_to_id[name], self.player)
return TunicItem(name, itemclass, self.item_name_to_id[name], self.player)

def create_items(self) -> None:
tunic_items: List[TunicItem] = []
Expand Down Expand Up @@ -278,8 +286,6 @@ def create_items(self) -> None:

if self.options.grass_randomizer:
items_to_create["Grass"] = len(grass_location_table)
tunic_items.append(self.create_item("Glass Cannon", ItemClassification.progression))
items_to_create["Glass Cannon"] = 0
for grass_location in excluded_grass_locations:
self.get_location(grass_location).place_locked_item(self.create_item("Grass"))
items_to_create["Grass"] -= len(excluded_grass_locations)
Expand Down Expand Up @@ -351,11 +357,6 @@ def remove_filler(amount: int) -> None:
tunic_items.append(self.create_item(page, ItemClassification.progression | ItemClassification.useful))
items_to_create[page] = 0

# logically relevant if you have ladder storage enabled
if self.options.ladder_storage and not self.options.ladder_storage_without_items:
tunic_items.append(self.create_item("Shield", ItemClassification.progression))
items_to_create["Shield"] = 0

if self.options.maskless:
tunic_items.append(self.create_item("Scavenger Mask", ItemClassification.useful))
items_to_create["Scavenger Mask"] = 0
Expand Down Expand Up @@ -502,6 +503,13 @@ def remove(self, state: CollectionState, item: Item) -> bool:
state.tunic_need_to_reset_combat_from_remove[self.player] = True
return change

def write_spoiler_header(self, spoiler_handle: TextIO):
if self.options.hexagon_quest and self.options.ability_shuffling:
spoiler_handle.write("\nAbility Unlocks (Hexagon Quest):\n")
for ability in self.ability_unlocks:
# Remove parentheses for better readability
spoiler_handle.write(f'{ability[ability.find("(")+1:ability.find(")")]}: {self.ability_unlocks[ability]} Gold Questagons\n')

def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]) -> None:
if self.options.entrance_rando:
hint_data.update({self.player: {}})
Expand Down
1 change: 1 addition & 0 deletions worlds/tunic/items.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class TunicItemData(NamedTuple):
"Gun": TunicItemData(IC.progression | IC.useful, 1, 30, "Weapons"),
"Shield": TunicItemData(IC.useful, 1, 31, combat_ic=IC.progression | IC.useful),
"Dath Stone": TunicItemData(IC.useful, 1, 32),
"Torch": TunicItemData(IC.useful, 0, 156),
"Hourglass": TunicItemData(IC.useful, 1, 33),
"Old House Key": TunicItemData(IC.progression, 1, 34, "Keys"),
"Key": TunicItemData(IC.progression, 2, 35, "Keys"),
Expand Down
46 changes: 22 additions & 24 deletions worlds/tunic/regions.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
from typing import Dict, Set

tunic_regions: Dict[str, Set[str]] = {
"Menu": {"Overworld"},
"Overworld": {"Overworld Holy Cross", "East Forest", "Dark Tomb", "Beneath the Well", "West Garden",
tunic_regions: dict[str, tuple[str]] = {
"Menu": ("Overworld",),
"Overworld": ("Overworld Holy Cross", "East Forest", "Dark Tomb", "Beneath the Well", "West Garden",
"Ruined Atoll", "Eastern Vault Fortress", "Beneath the Vault", "Quarry Back", "Quarry", "Swamp",
"Spirit Arena"},
"Overworld Holy Cross": set(),
"East Forest": set(),
"Dark Tomb": {"West Garden"},
"Beneath the Well": set(),
"West Garden": set(),
"Ruined Atoll": {"Frog's Domain", "Library"},
"Frog's Domain": set(),
"Library": set(),
"Eastern Vault Fortress": {"Beneath the Vault"},
"Beneath the Vault": {"Eastern Vault Fortress"},
"Quarry Back": {"Quarry"},
"Quarry": {"Monastery", "Lower Quarry"},
"Monastery": set(),
"Lower Quarry": {"Rooted Ziggurat"},
"Rooted Ziggurat": set(),
"Swamp": {"Cathedral"},
"Cathedral": set(),
"Spirit Arena": set()
"Spirit Arena"),
"Overworld Holy Cross": tuple(),
"East Forest": tuple(),
"Dark Tomb": ("West Garden",),
"Beneath the Well": tuple(),
"West Garden": tuple(),
"Ruined Atoll": ("Frog's Domain", "Library"),
"Frog's Domain": tuple(),
"Library": tuple(),
"Eastern Vault Fortress": ("Beneath the Vault",),
"Beneath the Vault": ("Eastern Vault Fortress",),
"Quarry Back": ("Quarry",),
"Quarry": ("Monastery", "Lower Quarry"),
"Monastery": tuple(),
"Lower Quarry": ("Rooted Ziggurat",),
"Rooted Ziggurat": tuple(),
"Swamp": ("Cathedral",),
"Cathedral": tuple(),
"Spirit Arena": tuple()
}

0 comments on commit 1a2e81b

Please sign in to comment.