Compare commits

...

13 Commits

Author SHA1 Message Date
NewSoupVi
83d8bd584b Revert "DS3: Make your own region cache (#4040)"
This reverts commit 2751ccdaab.
2024-10-11 03:03:32 +02:00
Exempt-Medic
2751ccdaab DS3: Make your own region cache (#4040)
* Make your own region cache

* Using a string
2024-10-11 03:02:31 +02:00
black-sliver
6287bc27a6 WebHost: Fix too-many-players error not showing (#4033)
* WebHost: fix 'too many players' error not showing

* WebHost, Tests: add basic tests for generate endpoint

* WebHost: hopefully make CodeQL happy with MAX_ROLL redirect
2024-10-05 18:14:22 +02:00
palex00
97f2c25924 [KH2] Adds more options to slot data #4031 2024-10-05 02:13:04 +02:00
Bryce Wilson
e5a0ef799f Pokemon Emerald: Update changelog (#4003) 2024-10-04 21:28:43 +02:00
Silvris
216e0603e1 KDL3: Fix webhost not giving a patch #4023 2024-10-04 21:27:23 +02:00
Fabian Dill
05a67386c6 Core: use shlex splitting instead of whitespace splitting for client and server commands (#4011) 2024-10-02 03:09:43 +02:00
NewSoupVi
0ec9039ca6 The Witness: Small code refactor (cast_not_none) (#3798)
* cast not none

* ruff

* Missed a spot
2024-10-02 00:02:17 +02:00
Aaron Wagener
f06f95d03d Core: move race_mode to read_data instead of stored_data (#4020)
* move race_mode to read_data

* add race_mode to docs
2024-10-01 23:55:34 +02:00
Mysteryem
5a853dfccd Tests: Fix indentation in TestTwoPlayerMulti (#4010)
The "filling multiworld" subtest was at the wrong indentation, so was
only running for the last world_type.

"games" has additionally been added to the subtest to help better
identify failures.

Now that the subtest is actually being run for each world type, this
adds about 20 seconds to the duration of the test on my machine.
2024-10-01 21:30:45 +02:00
Alex Nordstrom
23469fa5c3 LADX: ghost fills ammo to initial max (#4005)
* ghost fills ammo to max

* Revert "ghost fills ammo to max"

This reverts commit 68804fef14.

* fill to first max
2024-10-01 21:09:23 +02:00
Bryce Wilson
dc1da4e88b Pokemon Emerald: Another wonder trade fix (#4014)
* Pokemon Emerald: Another guarded write on wonder trades

* Pokemon Emerald: Reorder sending wonder trade and erasing data

In case the guarded write fails
2024-10-01 21:08:43 +02:00
Aaron Wagener
67f6b458d7 Core: add race mode to multidata and datastore (#4017)
* add race mode to multidata and datastore

* have commonclient check race mode on connect and add it to the tooltip ui
2024-10-01 21:08:13 +02:00
17 changed files with 130 additions and 26 deletions

View File

@@ -454,6 +454,7 @@ class CommonContext:
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:

View File

@@ -338,6 +338,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
"seed_name": multiworld.seed_name,
"spheres": spheres,
"datapackage": data_package,
"race_mode": int(multiworld.is_race),
}
AutoWorld.call_all(multiworld, "modify_multidata", multidata)

View File

@@ -15,6 +15,7 @@ import math
import operator
import pickle
import random
import shlex
import threading
import time
import typing
@@ -427,6 +428,8 @@ class Context:
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},"
@@ -1150,7 +1153,7 @@ class CommandProcessor(metaclass=CommandMeta):
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)

View File

@@ -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()}),

View File

@@ -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.

View File

@@ -243,6 +243,9 @@ class ServerLabel(HovererableLabel):
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."

View File

@@ -71,7 +71,7 @@ class TestTwoPlayerMulti(MultiworldTestBase):
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")

View File

@@ -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")

View File

@@ -325,7 +325,7 @@ class KDL3World(World):
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

View File

@@ -101,7 +101,18 @@ class KH2World(World):
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,

View File

@@ -81,23 +81,23 @@ talking:
; 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:

View File

@@ -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.

View File

@@ -545,11 +545,12 @@ class PokemonEmeraldClient(BizHawkClient):
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:

View File

@@ -14,7 +14,7 @@ from .data import static_items as static_witness_items
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
@@ -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
@@ -336,7 +336,7 @@ class WitnessWorld(World):
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

View File

@@ -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")
@@ -13,6 +13,11 @@ T = TypeVar("T")
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] = []

View File

@@ -15,7 +15,7 @@ from .data.item_definition_classes import (
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
@@ -200,7 +200,7 @@ class WitnessPlayerItems:
"""
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
]
@@ -211,8 +211,8 @@ class WitnessPlayerItems:
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

View File

@@ -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
@@ -7,6 +7,7 @@ from test.general import gen_steps, setup_multiworld
from test.multiworld.test_multiworlds import MultiworldTestBase
from .. import WitnessWorld
from ..data.utils import cast_not_none
class WitnessTestBase(WorldTestBase):
@@ -32,7 +33,7 @@ class WitnessTestBase(WorldTestBase):
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.