Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
gaithern committed May 13, 2024
2 parents 6ed2154 + 3ae2072 commit 6109412
Show file tree
Hide file tree
Showing 77 changed files with 11,935 additions and 1,039 deletions.
70 changes: 65 additions & 5 deletions BaseClasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
from collections import Counter, deque
from collections.abc import Collection, MutableSequence
from enum import IntEnum, IntFlag
from typing import Any, Callable, Dict, Iterable, Iterator, List, NamedTuple, Optional, Set, Tuple, TypedDict, Union, \
Type, ClassVar
from typing import Any, Callable, Dict, Iterable, Iterator, List, Mapping, NamedTuple, Optional, Set, Tuple, \
TypedDict, Union, Type, ClassVar

import NetUtils
import Options
Expand Down Expand Up @@ -707,15 +707,53 @@ def has_any(self, items: Iterable[str], player: int) -> bool:
"""Returns True if at least one item name of items is in state at least once."""
return any(self.prog_items[player][item] for item in items)

def has_all_counts(self, item_counts: Mapping[str, int], player: int) -> bool:
"""Returns True if each item name is in the state at least as many times as specified."""
return all(self.prog_items[player][item] >= count for item, count in item_counts.items())

def has_any_count(self, item_counts: Mapping[str, int], player: int) -> bool:
"""Returns True if at least one item name is in the state at least as many times as specified."""
return any(self.prog_items[player][item] >= count for item, count in item_counts.items())

def count(self, item: str, player: int) -> int:
return self.prog_items[player][item]

def item_count(self, item: str, player: int) -> int:
Utils.deprecate("Use count instead.")
return self.count(item, player)

def has_from_list(self, items: Iterable[str], player: int, count: int) -> bool:
"""Returns True if the state contains at least `count` items matching any of the item names from a list."""
found: int = 0
player_prog_items = self.prog_items[player]
for item_name in items:
found += player_prog_items[item_name]
if found >= count:
return True
return False

def has_from_list_exclusive(self, items: Iterable[str], player: int, count: int) -> bool:
"""Returns True if the state contains at least `count` items matching any of the item names from a list.
Ignores duplicates of the same item."""
found: int = 0
player_prog_items = self.prog_items[player]
for item_name in items:
found += player_prog_items[item_name] > 0
if found >= count:
return True
return False

def count_from_list(self, items: Iterable[str], player: int) -> int:
"""Returns the cumulative count of items from a list present in state."""
return sum(self.prog_items[player][item_name] for item_name in items)

def count_from_list_exclusive(self, items: Iterable[str], player: int) -> int:
"""Returns the cumulative count of items from a list present in state. Ignores duplicates of the same item."""
return sum(self.prog_items[player][item_name] > 0 for item_name in items)

# item name group related
def has_group(self, item_name_group: str, player: int, count: int = 1) -> bool:
"""Returns True if the state contains at least `count` items present in a specified item group."""
found: int = 0
player_prog_items = self.prog_items[player]
for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]:
Expand All @@ -724,12 +762,34 @@ def has_group(self, item_name_group: str, player: int, count: int = 1) -> bool:
return True
return False

def count_group(self, item_name_group: str, player: int) -> int:
def has_group_exclusive(self, item_name_group: str, player: int, count: int = 1) -> bool:
"""Returns True if the state contains at least `count` items present in a specified item group.
Ignores duplicates of the same item.
"""
found: int = 0
player_prog_items = self.prog_items[player]
for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]:
found += player_prog_items[item_name]
return found
found += player_prog_items[item_name] > 0
if found >= count:
return True
return False

def count_group(self, item_name_group: str, player: int) -> int:
"""Returns the cumulative count of items from an item group present in state."""
player_prog_items = self.prog_items[player]
return sum(
player_prog_items[item_name]
for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]
)

def count_group_exclusive(self, item_name_group: str, player: int) -> int:
"""Returns the cumulative count of items from an item group present in state.
Ignores duplicates of the same item."""
player_prog_items = self.prog_items[player]
return sum(
player_prog_items[item_name] > 0
for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]
)

# Item related
def collect(self, item: Item, event: bool = False, location: Optional[Location] = None) -> bool:
Expand Down
2 changes: 2 additions & 0 deletions CommonClient.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,8 @@ class CommonContext:

finished_game: bool
ready: bool
team: typing.Optional[int]
slot: typing.Optional[int]
auth: typing.Optional[str]
seed_name: typing.Optional[str]

Expand Down
22 changes: 15 additions & 7 deletions Fill.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ def _log_fill_progress(name: str, placed: int, total_items: int) -> None:
logging.info(f"Current fill step ({name}) at {placed}/{total_items} items placed.")


def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item] = tuple()) -> CollectionState:
def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item] = tuple(),
locations: typing.Optional[typing.List[Location]] = None) -> CollectionState:
new_state = base_state.copy()
for item in itempool:
new_state.collect(item, True)
new_state.sweep_for_events()
new_state.sweep_for_events(locations=locations)
return new_state


Expand Down Expand Up @@ -66,7 +67,8 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
item_pool.pop(p)
break
maximum_exploration_state = sweep_from_pool(
base_state, item_pool + unplaced_items)
base_state, item_pool + unplaced_items, multiworld.get_filled_locations(item.player)
if single_player_placement else None)

has_beaten_game = multiworld.has_beaten_game(maximum_exploration_state)

Expand Down Expand Up @@ -112,7 +114,9 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati

location.item = None
placed_item.location = None
swap_state = sweep_from_pool(base_state, [placed_item, *item_pool] if unsafe else item_pool)
swap_state = sweep_from_pool(base_state, [placed_item, *item_pool] if unsafe else item_pool,
multiworld.get_filled_locations(item.player)
if single_player_placement else None)
# unsafe means swap_state assumes we can somehow collect placed_item before item_to_place
# by continuing to swap, which is not guaranteed. This is unsafe because there is no mechanic
# to clean that up later, so there is a chance generation fails.
Expand Down Expand Up @@ -170,7 +174,9 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati

if cleanup_required:
# validate all placements and remove invalid ones
state = sweep_from_pool(base_state, [])
state = sweep_from_pool(
base_state, [], multiworld.get_filled_locations(item.player)
if single_player_placement else None)
for placement in placements:
if multiworld.worlds[placement.item.player].options.accessibility != "minimal" and not placement.can_reach(state):
placement.item.location = None
Expand Down Expand Up @@ -456,14 +462,16 @@ def mark_for_locking(location: Location):

if prioritylocations:
# "priority fill"
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool, swap=False, on_place=mark_for_locking,
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool,
single_player_placement=multiworld.players == 1, swap=False, on_place=mark_for_locking,
name="Priority")
accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool)
defaultlocations = prioritylocations + defaultlocations

if progitempool:
# "advancement/progression fill"
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, name="Progression")
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, single_player_placement=multiworld.players == 1,
name="Progression")
if progitempool:
raise FillError(
f"Not enough locations for progression items. "
Expand Down
18 changes: 9 additions & 9 deletions Generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def main(args=None, callback=ERmain):
raise ValueError(f"File {fname} is invalid. Please fix your yaml.") from e

# sort dict for consistent results across platforms:
weights_cache = {key: value for key, value in sorted(weights_cache.items())}
weights_cache = {key: value for key, value in sorted(weights_cache.items(), key=lambda k: k[0].casefold())}
for filename, yaml_data in weights_cache.items():
if filename not in {args.meta_file_path, args.weights_file_path}:
for yaml in yaml_data:
Expand Down Expand Up @@ -353,7 +353,7 @@ def roll_meta_option(option_key, game: str, category_dict: Dict) -> Any:
if options[option_key].supports_weighting:
return get_choice(option_key, category_dict)
return category_dict[option_key]
raise Exception(f"Error generating meta option {option_key} for {game}.")
raise Options.OptionError(f"Error generating meta option {option_key} for {game}.")


def roll_linked_options(weights: dict) -> dict:
Expand Down Expand Up @@ -409,19 +409,19 @@ def roll_triggers(weights: dict, triggers: list) -> dict:


def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, option: type(Options.Option), plando_options: PlandoOptions):
if option_key in game_weights:
try:
try:
if option_key in game_weights:
if not option.supports_weighting:
player_option = option.from_any(game_weights[option_key])
else:
player_option = option.from_any(get_choice(option_key, game_weights))
setattr(ret, option_key, player_option)
except Exception as e:
raise Exception(f"Error generating option {option_key} in {ret.game}") from e
else:
player_option.verify(AutoWorldRegister.world_types[ret.game], ret.name, plando_options)
player_option = option.from_any(option.default) # call the from_any here to support default "random"
setattr(ret, option_key, player_option)
except Exception as e:
raise Options.OptionError(f"Error generating option {option_key} in {ret.game}") from e
else:
setattr(ret, option_key, option.from_any(option.default)) # call the from_any here to support default "random"
player_option.verify(AutoWorldRegister.world_types[ret.game], ret.name, plando_options)


def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.bosses):
Expand Down
48 changes: 35 additions & 13 deletions MultiServer.py
Original file line number Diff line number Diff line change
Expand Up @@ -688,7 +688,7 @@ def notify_hints(self, team: int, hints: typing.List[NetUtils.Hint], only_new: b
clients = self.clients[team].get(slot)
if not clients:
continue
client_hints = [datum[1] for datum in sorted(hint_data, key=lambda x: x[0].finding_player == slot)]
client_hints = [datum[1] for datum in sorted(hint_data, key=lambda x: x[0].finding_player != slot)]
for client in clients:
async_start(self.send_msgs(client, client_hints))

Expand Down Expand Up @@ -803,14 +803,25 @@ async def on_client_disconnected(ctx: Context, client: Client):
await on_client_left(ctx, client)


_non_game_messages = {"HintGame": "hinting", "Tracker": "tracking", "TextOnly": "viewing"}
""" { tag: ui_message } """


async def on_client_joined(ctx: Context, client: Client):
if ctx.client_game_state[client.team, client.slot] == ClientStatus.CLIENT_UNKNOWN:
update_client_status(ctx, client, ClientStatus.CLIENT_CONNECTED)
version_str = '.'.join(str(x) for x in client.version)
verb = "tracking" if "Tracker" in client.tags else "playing"

for tag, verb in _non_game_messages.items():
if tag in client.tags:
final_verb = verb
break
else:
final_verb = "playing"

ctx.broadcast_text_all(
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) "
f"{verb} {ctx.games[client.slot]} has joined. "
f"{final_verb} {ctx.games[client.slot]} has joined. "
f"Client({version_str}), {client.tags}.",
{"type": "Join", "team": client.team, "slot": client.slot, "tags": client.tags})
ctx.notify_client(client, "Now that you are connected, "
Expand All @@ -825,8 +836,19 @@ async def on_client_left(ctx: Context, client: Client):
if len(ctx.clients[client.team][client.slot]) < 1:
update_client_status(ctx, client, ClientStatus.CLIENT_UNKNOWN)
ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc)

version_str = '.'.join(str(x) for x in client.version)

for tag, verb in _non_game_messages.items():
if tag in client.tags:
final_verb = f"stopped {verb}"
break
else:
final_verb = "left"

ctx.broadcast_text_all(
"%s (Team #%d) has left the game" % (ctx.get_aliased_name(client.team, client.slot), client.team + 1),
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has {final_verb} the game. "
f"Client({version_str}), {client.tags}.",
{"type": "Part", "team": client.team, "slot": client.slot})


Expand Down Expand Up @@ -1507,15 +1529,13 @@ def get_hints(self, input_text: str, for_location: bool = False) -> bool:

if hints:
new_hints = set(hints) - self.ctx.hints[self.client.team, self.client.slot]
old_hints = set(hints) - new_hints
if old_hints:
self.ctx.notify_hints(self.client.team, list(old_hints))
if not new_hints:
self.output("Hint was previously used, no points deducted.")
old_hints = list(set(hints) - new_hints)
if old_hints and not new_hints:
self.ctx.notify_hints(self.client.team, old_hints)
self.output("Hint was previously used, no points deducted.")
if new_hints:
found_hints = [hint for hint in new_hints if hint.found]
not_found_hints = [hint for hint in new_hints if not hint.found]

if not not_found_hints: # everything's been found, no need to pay
can_pay = 1000
elif cost:
Expand All @@ -1527,7 +1547,7 @@ def get_hints(self, input_text: str, for_location: bool = False) -> bool:
# By popular vote, make hints prefer non-local placements
not_found_hints.sort(key=lambda hint: int(hint.receiving_player != hint.finding_player))

hints = found_hints
hints = found_hints + old_hints
while can_pay > 0:
if not not_found_hints:
break
Expand All @@ -1537,6 +1557,7 @@ def get_hints(self, input_text: str, for_location: bool = False) -> bool:
self.ctx.hints_used[self.client.team, self.client.slot] += 1
points_available = get_client_points(self.ctx, self.client)

self.ctx.notify_hints(self.client.team, hints)
if not_found_hints:
if hints and cost and int((points_available // cost) == 0):
self.output(
Expand All @@ -1550,7 +1571,6 @@ def get_hints(self, input_text: str, for_location: bool = False) -> bool:
self.output(f"You can't afford the hint. "
f"You have {points_available} points and need at least "
f"{self.ctx.get_hint_cost(self.client.slot)}.")
self.ctx.notify_hints(self.client.team, hints)
self.ctx.save()
return True

Expand Down Expand Up @@ -1631,7 +1651,9 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
else:
team, slot = ctx.connect_names[args['name']]
game = ctx.games[slot]
ignore_game = ("TextOnly" in args["tags"] or "Tracker" in args["tags"]) and not args.get("game")

ignore_game = not args.get("game") and any(tag in _non_game_messages for tag in args["tags"])

if not ignore_game and args['game'] != game:
errors.add('InvalidGame')
minver = min_client_version if ignore_game else ctx.minimum_client_versions[slot]
Expand Down
4 changes: 4 additions & 0 deletions Options.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
import pathlib


class OptionError(ValueError):
pass


class Visibility(enum.IntFlag):
none = 0b0000
template = 0b0001
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ Currently, the following games are supported:
* Castlevania 64
* A Short Hike
* Yoshi's Island
* Mario & Luigi: Superstar Saga

For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
Expand Down
2 changes: 1 addition & 1 deletion SNIClient.py
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,7 @@ async def snes_write(ctx: SNIContext, write_list: typing.List[typing.Tuple[int,
PutAddress_Request: SNESRequest = {"Opcode": "PutAddress", "Operands": [], 'Space': 'SNES'}
try:
for address, data in write_list:
PutAddress_Request['Operands'] = [hex(address)[2:], hex(min(len(data), 256))[2:]]
PutAddress_Request['Operands'] = [hex(address)[2:], hex(len(data))[2:]]
if ctx.snes_socket is not None:
await ctx.snes_socket.send(dumps(PutAddress_Request))
await ctx.snes_socket.send(data)
Expand Down
1 change: 1 addition & 0 deletions WebHostLib/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ def host_room(room: UUID):
if cmd:
Command(room=room, commandtext=cmd)
commit()
return redirect(url_for("host_room", room=room.id))

now = datetime.datetime.utcnow()
# indicate that the page should reload to get the assigned port
Expand Down
5 changes: 5 additions & 0 deletions data/lua/connector_mmbn3.lua
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ local IsItemable = function()
end

local is_game_complete = function()
-- If the Cannary Byte is 0xFF, then the save RAM is untrustworthy
if memory.read_u8(canary_byte) == 0xFF then
return game_complete
end

-- If on the title screen don't read RAM, RAM can't be trusted yet
if IsOnTitle() then return game_complete end

Expand Down
3 changes: 3 additions & 0 deletions docs/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@
/worlds/lufia2ac/ @el-u
/worlds/lufia2ac/docs/ @wordfcuk @el-u

# Mario & Luigi: Superstar Saga
/worlds/mlss/ @jamesbrq

# Meritous
/worlds/meritous/ @FelicitusNeko

Expand Down
Loading

0 comments on commit 6109412

Please sign in to comment.