Skip to content

Commit

Permalink
Merge branch 'main' into civ6-1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
hesto2 authored Oct 8, 2024
2 parents 7ae7940 + 6287bc2 commit 05a3c76
Show file tree
Hide file tree
Showing 17 changed files with 130 additions and 26 deletions.
1 change: 1 addition & 0 deletions CommonClient.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,7 @@ async def send_connect(self, **kwargs: typing.Any) -> None:
if kwargs:
payload.update(kwargs)
await self.send_msgs([payload])
await self.send_msgs([{"cmd": "Get", "keys": ["_read_race_mode"]}])

async def console_input(self) -> str:
if self.ui:
Expand Down
1 change: 1 addition & 0 deletions Main.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@ def precollect_hint(location):
"seed_name": multiworld.seed_name,
"spheres": spheres,
"datapackage": data_package,
"race_mode": int(multiworld.is_race),
}
AutoWorld.call_all(multiworld, "modify_multidata", multidata)

Expand Down
5 changes: 4 additions & 1 deletion MultiServer.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import operator
import pickle
import random
import shlex
import threading
import time
import typing
Expand Down Expand Up @@ -427,6 +428,8 @@ def _load(self, decoded_obj: dict, game_data_packages: typing.Dict[str, typing.A
use_embedded_server_options: bool):

self.read_data = {}
# there might be a better place to put this.
self.read_data["race_mode"] = lambda: decoded_obj.get("race_mode", 0)
mdata_ver = decoded_obj["minimum_versions"]["server"]
if mdata_ver > version_tuple:
raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver},"
Expand Down Expand Up @@ -1150,7 +1153,7 @@ def __call__(self, raw: str) -> typing.Optional[bool]:
if not raw:
return
try:
command = raw.split()
command = shlex.split(raw, comments=False)
basecommand = command[0]
if basecommand[0] == self.marker:
method = self.commands.get(basecommand[1:].lower(), None)
Expand Down
1 change: 1 addition & 0 deletions WebHostLib/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ def start_generation(options: Dict[str, Union[dict, str]], meta: Dict[str, Any])
elif len(gen_options) > app.config["MAX_ROLL"]:
flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players. "
f"If you have a larger group, please generate it yourself and upload it.")
return redirect(url_for(request.endpoint, **(request.view_args or {})))
elif len(gen_options) >= app.config["JOB_THRESHOLD"]:
gen = Generation(
options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}),
Expand Down
1 change: 1 addition & 0 deletions docs/network protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ Some special keys exist with specific return data, all of them have the prefix `
| item_name_groups_{game_name} | dict\[str, list\[str\]\] | item_name_groups belonging to the requested game. |
| location_name_groups_{game_name} | dict\[str, list\[str\]\] | location_name_groups belonging to the requested game. |
| client_status_{team}_{slot} | [ClientStatus](#ClientStatus) | The current game status of the requested player. |
| race_mode | int | 0 if race mode is disabled, and 1 if it's enabled. |

### Set
Used to write data to the server's data storage, that data can then be shared across worlds or just saved for later. Values for keys in the data storage can be retrieved with a [Get](#Get) package, or monitored with a [SetNotify](#SetNotify) package.
Expand Down
3 changes: 3 additions & 0 deletions kvui.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,9 @@ def get_text(self):
f"\nYou currently have {ctx.hint_points} points."
elif ctx.hint_cost == 0:
text += "\n!hint is free to use."
if ctx.stored_data and "_read_race_mode" in ctx.stored_data:
text += "\nRace mode is enabled." \
if ctx.stored_data["_read_race_mode"] else "\nRace mode is disabled."
else:
text += f"\nYou are not authenticated yet."

Expand Down
8 changes: 4 additions & 4 deletions test/multiworld/test_multiworlds.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def test_two_player_single_game_fills(self) -> None:
for world in self.multiworld.worlds.values():
world.options.accessibility.value = Accessibility.option_full
self.assertSteps(gen_steps)
with self.subTest("filling multiworld", seed=self.multiworld.seed):
distribute_items_restrictive(self.multiworld)
call_all(self.multiworld, "post_fill")
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
with self.subTest("filling multiworld", games=world_type.game, seed=self.multiworld.seed):
distribute_items_restrictive(self.multiworld)
call_all(self.multiworld, "post_fill")
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
73 changes: 73 additions & 0 deletions test/webhost/test_generate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import zipfile
from io import BytesIO

from flask import url_for

from . import TestBase


class TestGenerate(TestBase):
def test_valid_yaml(self) -> None:
"""
Verify that posting a valid yaml will start generating a game.
"""
with self.app.app_context(), self.app.test_request_context():
yaml_data = """
name: Player1
game: Archipelago
Archipelago: {}
"""
response = self.client.post(url_for("generate"),
data={"file": (BytesIO(yaml_data.encode("utf-8")), "test.yaml")},
follow_redirects=True)
self.assertEqual(response.status_code, 200)
self.assertTrue("/seed/" in response.request.path or
"/wait/" in response.request.path,
f"Response did not properly redirect ({response.request.path})")

def test_empty_zip(self) -> None:
"""
Verify that posting an empty zip will give an error.
"""
with self.app.app_context(), self.app.test_request_context():
zip_data = BytesIO()
zipfile.ZipFile(zip_data, "w").close()
zip_data.seek(0)
self.assertGreater(len(zip_data.read()), 0)
zip_data.seek(0)
response = self.client.post(url_for("generate"),
data={"file": (zip_data, "test.zip")},
follow_redirects=True)
self.assertIn("user-message", response.text,
"Request did not call flash()")
self.assertIn("not find any valid files", response.text,
"Response shows unexpected error")
self.assertIn("generate-game-form", response.text,
"Response did not get user back to the form")

def test_too_many_players(self) -> None:
"""
Verify that posting too many players will give an error.
"""
max_roll = self.app.config["MAX_ROLL"]
# validate that max roll has a sensible value, otherwise we probably changed how it works
self.assertIsInstance(max_roll, int)
self.assertGreater(max_roll, 1)
self.assertLess(max_roll, 100)
# create a yaml with max_roll+1 players and watch it fail
with self.app.app_context(), self.app.test_request_context():
yaml_data = "---\n".join([
f"name: Player{n}\n"
"game: Archipelago\n"
"Archipelago: {}\n"
for n in range(1, max_roll + 2)
])
response = self.client.post(url_for("generate"),
data={"file": (BytesIO(yaml_data.encode("utf-8")), "test.yaml")},
follow_redirects=True)
self.assertIn("user-message", response.text,
"Request did not call flash()")
self.assertIn("limited to", response.text,
"Response shows unexpected error")
self.assertIn("generate-game-form", response.text,
"Response did not get user back to the form")
2 changes: 1 addition & 1 deletion worlds/kdl3/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ def generate_basic(self) -> None:

def generate_output(self, output_directory: str) -> None:
try:
patch = KDL3ProcedurePatch()
patch = KDL3ProcedurePatch(player=self.player, player_name=self.player_name)
patch_rom(self, patch)

self.rom_name = patch.name
Expand Down
13 changes: 12 additions & 1 deletion worlds/kh2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,18 @@ def fill_slot_data(self) -> dict:
if ability in self.goofy_ability_dict and self.goofy_ability_dict[ability] >= 1:
self.goofy_ability_dict[ability] -= 1

slot_data = self.options.as_dict("Goal", "FinalXemnas", "LuckyEmblemsRequired", "BountyRequired")
slot_data = self.options.as_dict(
"Goal",
"FinalXemnas",
"LuckyEmblemsRequired",
"BountyRequired",
"FightLogic",
"FinalFormLogic",
"AutoFormLogic",
"LevelDepth",
"DonaldGoofyStatsanity",
"CorSkipToggle"
)
slot_data.update({
"hitlist": [], # remove this after next update
"PoptrackerVersionCheck": 4.3,
Expand Down
12 changes: 6 additions & 6 deletions worlds/ladx/LADXR/patches/owl.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,23 +81,23 @@ def removeOwlEvents(rom):
; Give powder
ld a, [$DB4C]
cp $10
cp $20
jr nc, doNotGivePowder
ld a, $10
ld a, $20
ld [$DB4C], a
doNotGivePowder:
ld a, [$DB4D]
cp $10
cp $30
jr nc, doNotGiveBombs
ld a, $10
ld a, $30
ld [$DB4D], a
doNotGiveBombs:
ld a, [$DB45]
cp $10
cp $30
jr nc, doNotGiveArrows
ld a, $10
ld a, $30
ld [$DB45], a
doNotGiveArrows:
Expand Down
3 changes: 3 additions & 0 deletions worlds/pokemon_emerald/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@

### Fixes

- Fixed a rare issue where receiving a wonder trade could partially corrupt the save data, preventing the player from
receiving new items.
- Fixed the client spamming the "goal complete" status update to the server instead of sending it once.
- Fixed a logic issue where the "Mauville City - Coin Case from Lady in House" location only required a Harbor Mail if
the player randomized NPC gifts.
- The Dig tutor has its compatibility percentage raised to 50% if the player's TM/tutor compatibility is set lower.
Expand Down
7 changes: 4 additions & 3 deletions worlds/pokemon_emerald/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -545,11 +545,12 @@ async def handle_wonder_trade(self, ctx: "BizHawkClientContext", guards: Dict[st
if trade_is_sent == 0 and wonder_trade_pokemon_data[19] == 2:
# Game has wonder trade data to send. Send it to data storage, remove it from the game's memory,
# and mark that the game is waiting on receiving a trade
Utils.async_start(self.wonder_trade_send(ctx, pokemon_data_to_json(wonder_trade_pokemon_data)))
await bizhawk.write(ctx.bizhawk_ctx, [
success = await bizhawk.guarded_write(ctx.bizhawk_ctx, [
(sb1_address + 0x377C, bytes(0x50), "System Bus"),
(sb1_address + 0x37CC, [1], "System Bus"),
])
], [guards["SAVE BLOCK 1"]])
if success:
Utils.async_start(self.wonder_trade_send(ctx, pokemon_data_to_json(wonder_trade_pokemon_data)))
elif trade_is_sent != 0 and wonder_trade_pokemon_data[19] != 2:
# Game is waiting on receiving a trade.
if self.queued_received_trade is not None:
Expand Down
6 changes: 3 additions & 3 deletions worlds/witness/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from .data import static_locations as static_witness_locations
from .data import static_logic as static_witness_logic
from .data.item_definition_classes import DoorItemDefinition, ItemData
from .data.utils import get_audio_logs
from .data.utils import cast_not_none, get_audio_logs
from .hints import CompactHintData, create_all_hints, make_compact_hint_data, make_laser_hints
from .locations import WitnessPlayerLocations
from .options import TheWitnessOptions, witness_option_groups
Expand Down Expand Up @@ -55,7 +55,7 @@ class WitnessWorld(World):

item_name_to_id = {
# ITEM_DATA doesn't have any event items in it
name: cast(int, data.ap_code) for name, data in static_witness_items.ITEM_DATA.items()
name: cast_not_none(data.ap_code) for name, data in static_witness_items.ITEM_DATA.items()
}
location_name_to_id = static_witness_locations.ALL_LOCATIONS_TO_ID
item_name_groups = static_witness_items.ITEM_GROUPS
Expand Down Expand Up @@ -336,7 +336,7 @@ def fill_slot_data(self) -> Dict[str, Any]:
for item_name, hint in laser_hints.items():
item_def = cast(DoorItemDefinition, static_witness_logic.ALL_ITEMS[item_name])
self.laser_ids_to_hints[int(item_def.panel_id_hexes[0], 16)] = make_compact_hint_data(hint, self.player)
already_hinted_locations.add(cast(Location, hint.location))
already_hinted_locations.add(cast_not_none(hint.location))

# Audio Log Hints

Expand Down
7 changes: 6 additions & 1 deletion worlds/witness/data/utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from math import floor
from pkgutil import get_data
from random import Random
from typing import Any, Collection, Dict, FrozenSet, Iterable, List, Set, Tuple, TypeVar
from typing import Any, Collection, Dict, FrozenSet, Iterable, List, Optional, Set, Tuple, TypeVar

T = TypeVar("T")

Expand All @@ -13,6 +13,11 @@
WitnessRule = FrozenSet[FrozenSet[str]]


def cast_not_none(value: Optional[T]) -> T:
assert value is not None
return value


def weighted_sample(world_random: Random, population: List[T], weights: List[float], k: int) -> List[T]:
positions = range(len(population))
indices: List[int] = []
Expand Down
8 changes: 4 additions & 4 deletions worlds/witness/player_items.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
ProgressiveItemDefinition,
WeightedItemDefinition,
)
from .data.utils import build_weighted_int_list
from .data.utils import build_weighted_int_list, cast_not_none
from .locations import WitnessPlayerLocations
from .player_logic import WitnessPlayerLogic

Expand Down Expand Up @@ -200,7 +200,7 @@ def get_symbol_ids_not_in_pool(self) -> List[int]:
"""
return [
# data.ap_code is guaranteed for a symbol definition
cast(int, data.ap_code) for name, data in static_witness_items.ITEM_DATA.items()
cast_not_none(data.ap_code) for name, data in static_witness_items.ITEM_DATA.items()
if name not in self.item_data.keys() and data.definition.category is ItemCategory.SYMBOL
]

Expand All @@ -211,8 +211,8 @@ def get_progressive_item_ids_in_pool(self) -> Dict[int, List[int]]:
if isinstance(item.definition, ProgressiveItemDefinition):
# Note: we need to reference the static table here rather than the player-specific one because the child
# items were removed from the pool when we pruned out all progression items not in the options.
output[cast(int, item.ap_code)] = [cast(int, static_witness_items.ITEM_DATA[child_item].ap_code)
for child_item in item.definition.child_item_names]
output[cast_not_none(item.ap_code)] = [cast_not_none(static_witness_items.ITEM_DATA[child_item].ap_code)
for child_item in item.definition.child_item_names]
return output


5 changes: 3 additions & 2 deletions worlds/witness/test/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, ClassVar, Dict, Iterable, List, Mapping, Union, cast
from typing import Any, ClassVar, Dict, Iterable, List, Mapping, Union

from BaseClasses import CollectionState, Entrance, Item, Location, Region

Expand All @@ -7,6 +7,7 @@
from test.multiworld.test_multiworlds import MultiworldTestBase

from .. import WitnessWorld
from ..data.utils import cast_not_none


class WitnessTestBase(WorldTestBase):
Expand All @@ -32,7 +33,7 @@ def assert_dependency_on_event_item(self, spot: Union[Location, Region, Entrance
event_items = [item for item in self.multiworld.get_items() if item.name == item_name]
self.assertTrue(event_items, f"Event item {item_name} does not exist.")

event_locations = [cast(Location, event_item.location) for event_item in event_items]
event_locations = [cast_not_none(event_item.location) for event_item in event_items]

# Checking for an access dependency on an event item requires a bit of extra work,
# as state.remove forces a sweep, which will pick up the event item again right after we tried to remove it.
Expand Down

0 comments on commit 05a3c76

Please sign in to comment.