From b3a2473853645931021c8d98a53419bd50424f1b Mon Sep 17 00:00:00 2001
From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
Date: Fri, 7 Jun 2024 23:47:02 -0400
Subject: [PATCH 01/31] Docs: Fixing subject-verb agreement (#3491)
---
WebHostLib/templates/weightedOptions/macros.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/WebHostLib/templates/weightedOptions/macros.html b/WebHostLib/templates/weightedOptions/macros.html
index a6e4545fda..2682f9e8bc 100644
--- a/WebHostLib/templates/weightedOptions/macros.html
+++ b/WebHostLib/templates/weightedOptions/macros.html
@@ -34,7 +34,7 @@
Normal range: {{ option.range_start }} - {{ option.range_end }}
{% if option.special_range_names %}
- The following values has special meaning, and may fall outside the normal range.
+ The following values have special meanings, and may fall outside the normal range.
{% for name, value in option.special_range_names.items() %}
{{ value }}: {{ name }}
From 39deef5d09cc8c9bf1060e47c2843fa1d998bc44 Mon Sep 17 00:00:00 2001
From: Chris Wilson
Date: Sat, 8 Jun 2024 04:54:14 -0400
Subject: [PATCH 02/31] Fix Choice and TextChoice options crashing WebHost if
the option's default value is "random" (#3458)
---
WebHostLib/templates/weightedOptions/macros.html | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/WebHostLib/templates/weightedOptions/macros.html b/WebHostLib/templates/weightedOptions/macros.html
index 2682f9e8bc..55a56e3285 100644
--- a/WebHostLib/templates/weightedOptions/macros.html
+++ b/WebHostLib/templates/weightedOptions/macros.html
@@ -18,7 +18,11 @@
{% for id, name in option.name_lookup.items() %}
{% if name != 'random' %}
- {{ RangeRow(option_name, option, option.get_option_name(id), name, False, name if option.get_option_name(option.default)|lower == name|lower else None) }}
+ {% if option.default != 'random' %}
+ {{ RangeRow(option_name, option, option.get_option_name(id), name, False, name if option.get_option_name(option.default)|lower == name else None) }}
+ {% else %}
+ {{ RangeRow(option_name, option, option.get_option_name(id), name) }}
+ {% endif %}
{% endif %}
{% endfor %}
{{ RandomRow(option_name, option) }}
@@ -92,7 +96,11 @@
{% for id, name in option.name_lookup.items() %}
{% if name != 'random' %}
- {{ RangeRow(option_name, option, option.get_option_name(id), name, False, name if option.get_option_name(option.default)|lower == name else None) }}
+ {% if option.default != 'random' %}
+ {{ RangeRow(option_name, option, option.get_option_name(id), name, False, name if option.get_option_name(option.default)|lower == name else None) }}
+ {% else %}
+ {{ RangeRow(option_name, option, option.get_option_name(id), name) }}
+ {% endif %}
{% endif %}
{% endfor %}
{{ RandomRow(option_name, option) }}
From 89d584e47442e9c3f71ded587715e21202993875 Mon Sep 17 00:00:00 2001
From: Fabian Dill
Date: Sat, 8 Jun 2024 11:07:14 +0200
Subject: [PATCH 03/31] WebHost: allow getting checksum-specific datapackage
via /api/datapackage/ (#3451)
* WebHost: allow getting checksum-specific datapackage via /api/datapackage/
* match import style of /api/generate
---
WebHostLib/api/__init__.py | 20 +-------------------
WebHostLib/api/datapackage.py | 32 ++++++++++++++++++++++++++++++++
2 files changed, 33 insertions(+), 19 deletions(-)
create mode 100644 WebHostLib/api/datapackage.py
diff --git a/WebHostLib/api/__init__.py b/WebHostLib/api/__init__.py
index 22d1f19f6b..4003243a28 100644
--- a/WebHostLib/api/__init__.py
+++ b/WebHostLib/api/__init__.py
@@ -5,7 +5,6 @@ from uuid import UUID
from flask import Blueprint, abort, url_for
import worlds.Files
-from .. import cache
from ..models import Room, Seed
api_endpoints = Blueprint('api', __name__, url_prefix="/api")
@@ -49,21 +48,4 @@ def room_info(room: UUID):
}
-@api_endpoints.route('/datapackage')
-@cache.cached()
-def get_datapackage():
- from worlds import network_data_package
- return network_data_package
-
-
-@api_endpoints.route('/datapackage_checksum')
-@cache.cached()
-def get_datapackage_checksums():
- from worlds import network_data_package
- version_package = {
- game: game_data["checksum"] for game, game_data in network_data_package["games"].items()
- }
- return version_package
-
-
-from . import generate, user # trigger registration
+from . import generate, user, datapackage # trigger registration
diff --git a/WebHostLib/api/datapackage.py b/WebHostLib/api/datapackage.py
new file mode 100644
index 0000000000..3fb472d95d
--- /dev/null
+++ b/WebHostLib/api/datapackage.py
@@ -0,0 +1,32 @@
+from flask import abort
+
+from Utils import restricted_loads
+from WebHostLib import cache
+from WebHostLib.models import GameDataPackage
+from . import api_endpoints
+
+
+@api_endpoints.route('/datapackage')
+@cache.cached()
+def get_datapackage():
+ from worlds import network_data_package
+ return network_data_package
+
+
+@api_endpoints.route('/datapackage/')
+@cache.memoize(timeout=3600)
+def get_datapackage_by_checksum(checksum: str):
+ package = GameDataPackage.get(checksum=checksum)
+ if package:
+ return restricted_loads(package.data)
+ return abort(404)
+
+
+@api_endpoints.route('/datapackage_checksum')
+@cache.cached()
+def get_datapackage_checksums():
+ from worlds import network_data_package
+ version_package = {
+ game: game_data["checksum"] for game, game_data in network_data_package["games"].items()
+ }
+ return version_package
From a0653cdfe0d2524856f6afc5620ed331dcb328ed Mon Sep 17 00:00:00 2001
From: qwint
Date: Sat, 8 Jun 2024 10:31:27 -0500
Subject: [PATCH 04/31] HK: adds split movement items to skills item group
(#3462)
---
worlds/hk/Items.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/worlds/hk/Items.py b/worlds/hk/Items.py
index 0d4ab3d55f..8515465826 100644
--- a/worlds/hk/Items.py
+++ b/worlds/hk/Items.py
@@ -64,3 +64,4 @@ item_name_groups = ({
})
item_name_groups['Horizontal'] = item_name_groups['Cloak'] | item_name_groups['CDash']
item_name_groups['Vertical'] = item_name_groups['Claw'] | {'Monarch_Wings'}
+item_name_groups['Skills'] |= item_name_groups['Vertical'] | item_name_groups['Horizontal']
From 302017c69e80b20c9471b6c8e5882e49671b2370 Mon Sep 17 00:00:00 2001
From: black-sliver <59490463+black-sliver@users.noreply.github.com>
Date: Sat, 8 Jun 2024 17:51:09 +0200
Subject: [PATCH 05/31] Test: hosting: handle writes during start_room (#3492)
Note: maybe we'd also want to add such handling to WebHost itself,
but this is out of scope for getting hosting test to work.
---
test/hosting/webhost.py | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/test/hosting/webhost.py b/test/hosting/webhost.py
index e1e31ae466..4db605e8c1 100644
--- a/test/hosting/webhost.py
+++ b/test/hosting/webhost.py
@@ -66,12 +66,19 @@ def create_room(app_client: "FlaskClient", seed: str, auto_start: bool = False)
def start_room(app_client: "FlaskClient", room_id: str, timeout: float = 30) -> str:
from time import sleep
+ import pony.orm
+
poll_interval = .2
print(f"Starting room {room_id}")
no_timeout = timeout <= 0
while no_timeout or timeout > 0:
- response = app_client.get(f"/room/{room_id}")
+ try:
+ response = app_client.get(f"/room/{room_id}")
+ except pony.orm.core.OptimisticCheckError:
+ # hoster wrote to room during our transaction
+ continue
+
assert response.status_code == 200, f"Starting room for {room_id} failed: status {response.status_code}"
match = re.search(r"/connect ([\w:.\-]+)", response.text)
if match:
From 0d9fce29c69caf0eb8e4f4eab1f1961a550c6d0c Mon Sep 17 00:00:00 2001
From: Fabian Dill
Date: Sat, 8 Jun 2024 19:58:58 +0200
Subject: [PATCH 06/31] Core: load frozen decompressed worlds (#3488)
---
worlds/__init__.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/worlds/__init__.py b/worlds/__init__.py
index 83ee96131a..a0859290f9 100644
--- a/worlds/__init__.py
+++ b/worlds/__init__.py
@@ -107,8 +107,9 @@ for folder in (folder for folder in (user_folder, local_folder) if folder):
if not entry.name.startswith(("_", ".")):
file_name = entry.name if relative else os.path.join(folder, entry.name)
if entry.is_dir():
- init_file_path = os.path.join(entry.path, '__init__.py')
- if os.path.isfile(init_file_path):
+ if os.path.isfile(os.path.join(entry.path, '__init__.py')):
+ world_sources.append(WorldSource(file_name, relative=relative))
+ elif os.path.isfile(os.path.join(entry.path, '__init__.pyc')):
world_sources.append(WorldSource(file_name, relative=relative))
else:
logging.warning(f"excluding {entry.name} from world sources because it has no __init__.py")
From 76804d295b7c33efb4671938c042a1d1e3b770e6 Mon Sep 17 00:00:00 2001
From: Fabian Dill
Date: Sat, 8 Jun 2024 20:04:17 +0200
Subject: [PATCH 07/31] Core: explicitly import importlib.util (#3224)
---
worlds/__init__.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/worlds/__init__.py b/worlds/__init__.py
index a0859290f9..8d784a5ba4 100644
--- a/worlds/__init__.py
+++ b/worlds/__init__.py
@@ -1,4 +1,5 @@
import importlib
+import importlib.util
import logging
import os
import sys
From c478e55d7a56f2485e903a38f1919df45c4a379e Mon Sep 17 00:00:00 2001
From: Fabian Dill
Date: Sun, 9 Jun 2024 03:13:27 +0200
Subject: [PATCH 08/31] Generate: improve logging capture (#3484)
---
Generate.py | 38 +++++++++++++++++++++++---------------
Utils.py | 1 +
2 files changed, 24 insertions(+), 15 deletions(-)
diff --git a/Generate.py b/Generate.py
index 67988bf8b3..0cef081120 100644
--- a/Generate.py
+++ b/Generate.py
@@ -1,10 +1,12 @@
from __future__ import annotations
import argparse
+import copy
import logging
import os
import random
import string
+import sys
import urllib.parse
import urllib.request
from collections import Counter
@@ -15,21 +17,16 @@ import ModuleUpdate
ModuleUpdate.update()
-import copy
import Utils
import Options
from BaseClasses import seeddigits, get_seed, PlandoOptions
-from Main import main as ERmain
-from settings import get_settings
from Utils import parse_yamls, version_tuple, __version__, tuplize_version
-from worlds.alttp.EntranceRandomizer import parse_arguments
-from worlds.AutoWorld import AutoWorldRegister
-from worlds import failed_world_loads
def mystery_argparse():
- options = get_settings()
- defaults = options.generator
+ from settings import get_settings
+ settings = get_settings()
+ defaults = settings.generator
parser = argparse.ArgumentParser(description="CMD Generation Interface, defaults come from host.yaml.")
parser.add_argument('--weights_file_path', default=defaults.weights_file_path,
@@ -41,7 +38,7 @@ def mystery_argparse():
parser.add_argument('--seed', help='Define seed number to generate.', type=int)
parser.add_argument('--multi', default=defaults.players, type=lambda value: max(int(value), 1))
parser.add_argument('--spoiler', type=int, default=defaults.spoiler)
- parser.add_argument('--outputpath', default=options.general_options.output_path,
+ parser.add_argument('--outputpath', default=settings.general_options.output_path,
help="Path to output folder. Absolute or relative to cwd.") # absolute or relative to cwd
parser.add_argument('--race', action='store_true', default=defaults.race)
parser.add_argument('--meta_file_path', default=defaults.meta_file_path)
@@ -61,20 +58,21 @@ def mystery_argparse():
if not os.path.isabs(args.meta_file_path):
args.meta_file_path = os.path.join(args.player_files_path, args.meta_file_path)
args.plando: PlandoOptions = PlandoOptions.from_option_string(args.plando)
- return args, options
+ return args
def get_seed_name(random_source) -> str:
return f"{random_source.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits)
-def main(args=None, callback=ERmain):
+def main(args=None):
if not args:
- args, options = mystery_argparse()
- else:
- options = get_settings()
+ args = mystery_argparse()
seed = get_seed(args.seed)
+ # __name__ == "__main__" check so unittests that already imported worlds don't trip this.
+ if __name__ == "__main__" and "worlds" in sys.modules:
+ raise Exception("Worlds system should not be loaded before logging init.")
Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level)
random.seed(seed)
seed_name = get_seed_name(random)
@@ -143,6 +141,9 @@ def main(args=None, callback=ERmain):
raise Exception(f"No weights found. "
f"Provide a general weights file ({args.weights_file_path}) or individual player files. "
f"A mix is also permitted.")
+
+ from worlds.AutoWorld import AutoWorldRegister
+ from worlds.alttp.EntranceRandomizer import parse_arguments
erargs = parse_arguments(['--multi', str(args.multi)])
erargs.seed = seed
erargs.plando_options = args.plando
@@ -234,7 +235,8 @@ def main(args=None, callback=ERmain):
with open(os.path.join(args.outputpath if args.outputpath else ".", f"generate_{seed_name}.yaml"), "wt") as f:
yaml.dump(important, f)
- return callback(erargs, seed)
+ from Main import main as ERmain
+ return ERmain(erargs, seed)
def read_weights_yamls(path) -> Tuple[Any, ...]:
@@ -359,6 +361,8 @@ def update_weights(weights: dict, new_weights: dict, update_type: str, name: str
def roll_meta_option(option_key, game: str, category_dict: Dict) -> Any:
+ from worlds import AutoWorldRegister
+
if not game:
return get_choice(option_key, category_dict)
if game in AutoWorldRegister.world_types:
@@ -436,10 +440,13 @@ def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str,
except Exception as e:
raise Options.OptionError(f"Error generating option {option_key} in {ret.game}") from e
else:
+ from worlds import AutoWorldRegister
player_option.verify(AutoWorldRegister.world_types[ret.game], ret.name, plando_options)
def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.bosses):
+ from worlds import AutoWorldRegister
+
if "linked_options" in weights:
weights = roll_linked_options(weights)
@@ -466,6 +473,7 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
ret.game = get_choice("game", weights)
if ret.game not in AutoWorldRegister.world_types:
+ from worlds import failed_world_loads
picks = Utils.get_fuzzy_results(ret.game, list(AutoWorldRegister.world_types) + failed_world_loads, limit=1)[0]
if picks[0] in failed_world_loads:
raise Exception(f"No functional world found to handle game {ret.game}. "
diff --git a/Utils.py b/Utils.py
index a7fd7f4f33..f89330cf7c 100644
--- a/Utils.py
+++ b/Utils.py
@@ -553,6 +553,7 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, wri
f"Archipelago ({__version__}) logging initialized"
f" on {platform.platform()}"
f" running Python {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
+ f"{' (frozen)' if is_frozen() else ''}"
)
From 2198a70251bae82114c9eb4691a04800ca4a610f Mon Sep 17 00:00:00 2001
From: Phaneros <31861583+MatthewMarinets@users.noreply.github.com>
Date: Sat, 8 Jun 2024 19:08:47 -0700
Subject: [PATCH 09/31] Core: CommonClient: command history and echo (#3236)
* client: Added command history access with up/down and command echo in common client
* client: Changed command echo colour to orange
* client: removed star import from typing
* client: updated code style to match style guideline
* client: adjusted ordering of calling parent constructor in command prompt input constructor
* client: Fixed issues identified by beauxq in PR; fixed some typing issues
* client: PR comments; replaced command history list with deque
---
CommonClient.py | 5 ++++
NetUtils.py | 3 ++-
data/client.kv | 1 +
kvui.py | 62 +++++++++++++++++++++++++++++++++++++++++---
worlds/sc2/Client.py | 1 -
5 files changed, 67 insertions(+), 5 deletions(-)
diff --git a/CommonClient.py b/CommonClient.py
index 8af822cba5..8f1e64c059 100644
--- a/CommonClient.py
+++ b/CommonClient.py
@@ -493,6 +493,11 @@ class CommonContext:
"""Gets called before sending a Say to the server from the user.
Returned text is sent, or sending is aborted if None is returned."""
return text
+
+ def on_ui_command(self, text: str) -> None:
+ """Gets called by kivy when the user executes a command starting with `/` or `!`.
+ The command processor is still called; this is just intended for command echoing."""
+ self.ui.print_json([{"text": text, "type": "color", "color": "orange"}])
def update_permissions(self, permissions: typing.Dict[str, int]):
for permission_name, permission_flag in permissions.items():
diff --git a/NetUtils.py b/NetUtils.py
index 076fdc3ba4..f8d698c74f 100644
--- a/NetUtils.py
+++ b/NetUtils.py
@@ -198,7 +198,8 @@ class JSONtoTextParser(metaclass=HandlerMeta):
"slateblue": "6D8BE8",
"plum": "AF99EF",
"salmon": "FA8072",
- "white": "FFFFFF"
+ "white": "FFFFFF",
+ "orange": "FF7700",
}
def __init__(self, ctx):
diff --git a/data/client.kv b/data/client.kv
index bf98fa1517..dc8a5c9c9d 100644
--- a/data/client.kv
+++ b/data/client.kv
@@ -13,6 +13,7 @@
plum: "AF99EF" # typically progression item
salmon: "FA8072" # typically trap item
white: "FFFFFF" # not used, if you want to change the generic text color change color in Label
+ orange: "FF7700" # Used for command echo
:
color: "FFFFFF"
:
diff --git a/kvui.py b/kvui.py
index e9e495aef3..500203a881 100644
--- a/kvui.py
+++ b/kvui.py
@@ -3,6 +3,7 @@ import logging
import sys
import typing
import re
+from collections import deque
if sys.platform == "win32":
import ctypes
@@ -380,6 +381,57 @@ class ConnectBarTextInput(TextInput):
return super(ConnectBarTextInput, self).insert_text(s, from_undo=from_undo)
+def is_command_input(string: str) -> bool:
+ return len(string) > 0 and string[0] in "/!"
+
+
+class CommandPromptTextInput(TextInput):
+ MAXIMUM_HISTORY_MESSAGES = 50
+
+ def __init__(self, **kwargs) -> None:
+ super().__init__(**kwargs)
+ self._command_history_index = -1
+ self._command_history: typing.Deque[str] = deque(maxlen=CommandPromptTextInput.MAXIMUM_HISTORY_MESSAGES)
+
+ def update_history(self, new_entry: str) -> None:
+ self._command_history_index = -1
+ if is_command_input(new_entry):
+ self._command_history.appendleft(new_entry)
+
+ def keyboard_on_key_down(
+ self,
+ window,
+ keycode: typing.Tuple[int, str],
+ text: typing.Optional[str],
+ modifiers: typing.List[str]
+ ) -> bool:
+ """
+ :param window: The kivy window object
+ :param keycode: A tuple of (keycode, keyname). Keynames are always lowercase
+ :param text: The text printed by this key, not accounting for modifiers, or `None` if no text.
+ Seems to pretty naively interpret the keycode as unicode, so numlock can return odd characters.
+ :param modifiers: A list of string modifiers, like `ctrl` or `numlock`
+ """
+ if keycode[1] == 'up':
+ self._change_to_history_text_if_available(self._command_history_index + 1)
+ return True
+ if keycode[1] == 'down':
+ self._change_to_history_text_if_available(self._command_history_index - 1)
+ return True
+ return super().keyboard_on_key_down(window, keycode, text, modifiers)
+
+ def _change_to_history_text_if_available(self, new_index: int) -> None:
+ if new_index < -1:
+ return
+ if new_index >= len(self._command_history):
+ return
+ self._command_history_index = new_index
+ if new_index == -1:
+ self.text = ""
+ return
+ self.text = self._command_history[self._command_history_index]
+
+
class MessageBox(Popup):
class MessageBoxLabel(Label):
def __init__(self, **kwargs):
@@ -415,7 +467,7 @@ class GameManager(App):
self.commandprocessor = ctx.command_processor(ctx)
self.icon = r"data/icon.png"
self.json_to_kivy_parser = KivyJSONtoTextParser(ctx)
- self.log_panels = {}
+ self.log_panels: typing.Dict[str, Widget] = {}
# keep track of last used command to autofill on click
self.last_autofillable_command = "hint"
@@ -499,7 +551,7 @@ class GameManager(App):
info_button = Button(size=(dp(100), dp(30)), text="Command:", size_hint_x=None)
info_button.bind(on_release=self.command_button_action)
bottom_layout.add_widget(info_button)
- self.textinput = TextInput(size_hint_y=None, height=dp(30), multiline=False, write_tab=False)
+ self.textinput = CommandPromptTextInput(size_hint_y=None, height=dp(30), multiline=False, write_tab=False)
self.textinput.bind(on_text_validate=self.on_message)
self.textinput.text_validate_unfocus = False
bottom_layout.add_widget(self.textinput)
@@ -557,14 +609,18 @@ class GameManager(App):
self.ctx.exit_event.set()
- def on_message(self, textinput: TextInput):
+ def on_message(self, textinput: CommandPromptTextInput):
try:
input_text = textinput.text.strip()
textinput.text = ""
+ textinput.update_history(input_text)
if self.ctx.input_requests > 0:
self.ctx.input_requests -= 1
self.ctx.input_queue.put_nowait(input_text)
+ elif is_command_input(input_text):
+ self.ctx.on_ui_command(input_text)
+ self.commandprocessor(input_text)
elif input_text:
self.commandprocessor(input_text)
diff --git a/worlds/sc2/Client.py b/worlds/sc2/Client.py
index ac9ccfffcd..c97902fbcb 100644
--- a/worlds/sc2/Client.py
+++ b/worlds/sc2/Client.py
@@ -122,7 +122,6 @@ class ColouredMessage:
class StarcraftClientProcessor(ClientCommandProcessor):
ctx: SC2Context
- echo_commands = True
def formatted_print(self, text: str) -> None:
"""Prints with kivy formatting to the GUI, and also prints to command-line and to all logs"""
From 5f8a8e6dade04007e729d36a329508e4a74f2eb4 Mon Sep 17 00:00:00 2001
From: jamesbrq
Date: Sun, 9 Jun 2024 10:54:07 -0400
Subject: [PATCH 10/31] Update Rom.py (#3498)
---
worlds/mlss/Rom.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/worlds/mlss/Rom.py b/worlds/mlss/Rom.py
index 08921500da..7cbbe88751 100644
--- a/worlds/mlss/Rom.py
+++ b/worlds/mlss/Rom.py
@@ -306,8 +306,7 @@ def write_tokens(world: "MLSSWorld", patch: MLSSProcedurePatch) -> None:
if world.options.scale_stats:
patch.write_token(APTokenTypes.WRITE, 0xD00002, bytes([0x1]))
- if world.options.xp_multiplier:
- patch.write_token(APTokenTypes.WRITE, 0xD00003, bytes([world.options.xp_multiplier.value]))
+ patch.write_token(APTokenTypes.WRITE, 0xD00003, bytes([world.options.xp_multiplier.value]))
if world.options.tattle_hp:
patch.write_token(APTokenTypes.WRITE, 0xD00000, bytes([0x1]))
From 84a6d50ae7453f87af3dcc67a6cda32ea4767c9c Mon Sep 17 00:00:00 2001
From: Phaneros <31861583+MatthewMarinets@users.noreply.github.com>
Date: Sun, 9 Jun 2024 07:55:05 -0700
Subject: [PATCH 11/31] sc2: Fixed sc2 client's /received command breaking
after PR 1933 merged (#3497)
---
worlds/sc2/Client.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/worlds/sc2/Client.py b/worlds/sc2/Client.py
index c97902fbcb..e6696b782d 100644
--- a/worlds/sc2/Client.py
+++ b/worlds/sc2/Client.py
@@ -107,10 +107,10 @@ class ColouredMessage:
def coloured(self, text: str, colour: str) -> 'ColouredMessage':
add_json_text(self.parts, text, type="color", color=colour)
return self
- def location(self, location_id: int, player_id: int = 0) -> 'ColouredMessage':
+ def location(self, location_id: int, player_id: int) -> 'ColouredMessage':
add_json_location(self.parts, location_id, player_id)
return self
- def item(self, item_id: int, player_id: int = 0, flags: int = 0) -> 'ColouredMessage':
+ def item(self, item_id: int, player_id: int, flags: int = 0) -> 'ColouredMessage':
add_json_item(self.parts, item_id, player_id, flags)
return self
def player(self, player_id: int) -> 'ColouredMessage':
@@ -256,7 +256,7 @@ class StarcraftClientProcessor(ClientCommandProcessor):
for item in received_items_of_this_type:
print_faction_title()
has_printed_faction_title = True
- (ColouredMessage('* ').item(item.item, flags=item.flags)
+ (ColouredMessage('* ').item(item.item, self.ctx.slot, flags=item.flags)
(" from ").location(item.location, self.ctx.slot)
(" by ").player(item.player)
).send(self.ctx)
@@ -277,7 +277,7 @@ class StarcraftClientProcessor(ClientCommandProcessor):
received_items_of_this_type = items_received.get(child_item, [])
for item in received_items_of_this_type:
filter_match_count += len(received_items_of_this_type)
- (ColouredMessage(' * ').item(item.item, flags=item.flags)
+ (ColouredMessage(' * ').item(item.item, self.ctx.slot, flags=item.flags)
(" from ").location(item.location, self.ctx.slot)
(" by ").player(item.player)
).send(self.ctx)
From 0a912808e37ea67064252dc4edb83340074731ba Mon Sep 17 00:00:00 2001
From: Phaneros <31861583+MatthewMarinets@users.noreply.github.com>
Date: Sun, 9 Jun 2024 17:05:39 -0700
Subject: [PATCH 12/31] SC2: update inno_setup.iss to remove old sc2wol world
folder (#3495)
---
inno_setup.iss | 3 +++
1 file changed, 3 insertions(+)
diff --git a/inno_setup.iss b/inno_setup.iss
index a0f4944d98..f2e850e07f 100644
--- a/inno_setup.iss
+++ b/inno_setup.iss
@@ -89,6 +89,9 @@ Type: files; Name: "{app}\ArchipelagoPokemonClient.exe"
Type: files; Name: "{app}\data\lua\connector_pkmn_rb.lua"
Type: filesandordirs; Name: "{app}\lib\worlds\rogue-legacy"
Type: dirifempty; Name: "{app}\lib\worlds\rogue-legacy"
+Type: files; Name: "{app}\lib\worlds\sc2wol.apworld"
+Type: filesandordirs; Name: "{app}\lib\worlds\sc2wol"
+Type: dirifempty; Name: "{app}\lib\worlds\sc2wol"
Type: filesandordirs; Name: "{app}\lib\worlds\bk_sudoku"
Type: dirifempty; Name: "{app}\lib\worlds\bk_sudoku"
Type: files; Name: "{app}\ArchipelagoLauncher(DEBUG).exe"
From 35617bdac517149a2b8b4af3f1ae7e8381f2dc45 Mon Sep 17 00:00:00 2001
From: Aaron Wagener
Date: Mon, 10 Jun 2024 02:28:28 -0500
Subject: [PATCH 13/31] Tests: Add checksum validation to the postgen
datapackage test (#3456)
* Tests: Add checksum validation to the postgen datapackage test
* add a special case for the test world datapackage rather than hidden
* add the test world to the datapackage instead of special casing around it
---------
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
---
test/general/__init__.py | 5 +++++
test/general/test_ids.py | 2 ++
2 files changed, 7 insertions(+)
diff --git a/test/general/__init__.py b/test/general/__init__.py
index 1d4fc80c3e..8afd849765 100644
--- a/test/general/__init__.py
+++ b/test/general/__init__.py
@@ -2,6 +2,7 @@ from argparse import Namespace
from typing import List, Optional, Tuple, Type, Union
from BaseClasses import CollectionState, Item, ItemClassification, Location, MultiWorld, Region
+from worlds import network_data_package
from worlds.AutoWorld import World, call_all
gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill")
@@ -60,6 +61,10 @@ class TestWorld(World):
hidden = True
+# add our test world to the data package, so we can test it later
+network_data_package["games"][TestWorld.game] = TestWorld.get_data_package_data()
+
+
def generate_test_multiworld(players: int = 1) -> MultiWorld:
"""
Generates a multiworld using a special Test Case World class, and seed of 0.
diff --git a/test/general/test_ids.py b/test/general/test_ids.py
index e4010af394..e51a070c1f 100644
--- a/test/general/test_ids.py
+++ b/test/general/test_ids.py
@@ -1,6 +1,7 @@
import unittest
from Fill import distribute_items_restrictive
+from worlds import network_data_package
from worlds.AutoWorld import AutoWorldRegister, call_all
from . import setup_solo_multiworld
@@ -84,3 +85,4 @@ class TestIDs(unittest.TestCase):
f"{loc_name} is not a valid item name for location_name_to_id")
self.assertIsInstance(loc_id, int,
f"{loc_id} for {loc_name} should be an int")
+ self.assertEqual(datapackage["checksum"], network_data_package["games"][gamename]["checksum"])
From 484082616f3aba18da31f579e965c0f2f1d5227e Mon Sep 17 00:00:00 2001
From: JusticePS <5125765+JusticePS@users.noreply.github.com>
Date: Mon, 10 Jun 2024 15:42:01 -0700
Subject: [PATCH 14/31] Adventure: Update to use new options api (#3326)
---
AdventureClient.py | 8 ++++----
worlds/adventure/Options.py | 37 ++++++++++++++++++------------------
worlds/adventure/Regions.py | 5 +++--
worlds/adventure/Rules.py | 4 ++--
worlds/adventure/__init__.py | 37 ++++++++++++++++++------------------
5 files changed, 47 insertions(+), 44 deletions(-)
diff --git a/AdventureClient.py b/AdventureClient.py
index 7bfbd5ef6b..206c55df9a 100644
--- a/AdventureClient.py
+++ b/AdventureClient.py
@@ -80,7 +80,7 @@ class AdventureContext(CommonContext):
self.local_item_locations = {}
self.dragon_speed_info = {}
- options = Utils.get_options()
+ options = Utils.get_settings()
self.display_msgs = options["adventure_options"]["display_msgs"]
async def server_auth(self, password_requested: bool = False):
@@ -102,7 +102,7 @@ class AdventureContext(CommonContext):
def on_package(self, cmd: str, args: dict):
if cmd == 'Connected':
self.locations_array = None
- if Utils.get_options()["adventure_options"].get("death_link", False):
+ if Utils.get_settings()["adventure_options"].get("death_link", False):
self.set_deathlink = True
async_start(self.get_freeincarnates_used())
elif cmd == "RoomInfo":
@@ -415,8 +415,8 @@ async def atari_sync_task(ctx: AdventureContext):
async def run_game(romfile):
- auto_start = Utils.get_options()["adventure_options"].get("rom_start", True)
- rom_args = Utils.get_options()["adventure_options"].get("rom_args")
+ auto_start = Utils.get_settings()["adventure_options"].get("rom_start", True)
+ rom_args = Utils.get_settings()["adventure_options"].get("rom_args")
if auto_start is True:
import webbrowser
webbrowser.open(romfile)
diff --git a/worlds/adventure/Options.py b/worlds/adventure/Options.py
index 9e0cc9d686..e6a8e4c202 100644
--- a/worlds/adventure/Options.py
+++ b/worlds/adventure/Options.py
@@ -2,7 +2,8 @@ from __future__ import annotations
from typing import Dict
-from Options import Choice, Option, DefaultOnToggle, DeathLink, Range, Toggle
+from dataclasses import dataclass
+from Options import Choice, Option, DefaultOnToggle, DeathLink, Range, Toggle, PerGameCommonOptions
class FreeincarnateMax(Range):
@@ -223,22 +224,22 @@ class StartCastle(Choice):
option_white = 2
default = option_yellow
+@dataclass
+class AdventureOptions(PerGameCommonOptions):
+ dragon_slay_check: DragonSlayCheck
+ death_link: DeathLink
+ bat_logic: BatLogic
+ freeincarnate_max: FreeincarnateMax
+ dragon_rando_type: DragonRandoType
+ connector_multi_slot: ConnectorMultiSlot
+ yorgle_speed: YorgleStartingSpeed
+ yorgle_min_speed: YorgleMinimumSpeed
+ grundle_speed: GrundleStartingSpeed
+ grundle_min_speed: GrundleMinimumSpeed
+ rhindle_speed: RhindleStartingSpeed
+ rhindle_min_speed: RhindleMinimumSpeed
+ difficulty_switch_a: DifficultySwitchA
+ difficulty_switch_b: DifficultySwitchB
+ start_castle: StartCastle
-adventure_option_definitions: Dict[str, type(Option)] = {
- "dragon_slay_check": DragonSlayCheck,
- "death_link": DeathLink,
- "bat_logic": BatLogic,
- "freeincarnate_max": FreeincarnateMax,
- "dragon_rando_type": DragonRandoType,
- "connector_multi_slot": ConnectorMultiSlot,
- "yorgle_speed": YorgleStartingSpeed,
- "yorgle_min_speed": YorgleMinimumSpeed,
- "grundle_speed": GrundleStartingSpeed,
- "grundle_min_speed": GrundleMinimumSpeed,
- "rhindle_speed": RhindleStartingSpeed,
- "rhindle_min_speed": RhindleMinimumSpeed,
- "difficulty_switch_a": DifficultySwitchA,
- "difficulty_switch_b": DifficultySwitchB,
- "start_castle": StartCastle,
-}
diff --git a/worlds/adventure/Regions.py b/worlds/adventure/Regions.py
index 00617b2f71..e72806ca45 100644
--- a/worlds/adventure/Regions.py
+++ b/worlds/adventure/Regions.py
@@ -1,4 +1,5 @@
from BaseClasses import MultiWorld, Region, Entrance, LocationProgressType
+from Options import PerGameCommonOptions
from .Locations import location_table, LocationData, AdventureLocation, dragon_room_to_region
@@ -24,7 +25,7 @@ def connect(world: MultiWorld, player: int, source: str, target: str, rule: call
connect(world, player, target, source, rule, True)
-def create_regions(multiworld: MultiWorld, player: int, dragon_rooms: []) -> None:
+def create_regions(options: PerGameCommonOptions, multiworld: MultiWorld, player: int, dragon_rooms: []) -> None:
menu = Region("Menu", player, multiworld)
@@ -74,7 +75,7 @@ def create_regions(multiworld: MultiWorld, player: int, dragon_rooms: []) -> Non
credits_room_far_side.exits.append(Entrance(player, "CreditsFromFarSide", credits_room_far_side))
multiworld.regions.append(credits_room_far_side)
- dragon_slay_check = multiworld.dragon_slay_check[player].value
+ dragon_slay_check = options.dragon_slay_check.value
priority_locations = determine_priority_locations(multiworld, dragon_slay_check)
for name, location_data in location_table.items():
diff --git a/worlds/adventure/Rules.py b/worlds/adventure/Rules.py
index 6f4b53faa1..9302953012 100644
--- a/worlds/adventure/Rules.py
+++ b/worlds/adventure/Rules.py
@@ -6,7 +6,7 @@ from BaseClasses import LocationProgressType
def set_rules(self) -> None:
world = self.multiworld
- use_bat_logic = world.bat_logic[self.player].value == BatLogic.option_use_logic
+ use_bat_logic = self.options.bat_logic.value == BatLogic.option_use_logic
set_rule(world.get_entrance("YellowCastlePort", self.player),
lambda state: state.has("Yellow Key", self.player))
@@ -28,7 +28,7 @@ def set_rules(self) -> None:
lambda state: state.has("Bridge", self.player) or
state.has("Magnet", self.player))
- dragon_slay_check = world.dragon_slay_check[self.player].value
+ dragon_slay_check = self.options.dragon_slay_check.value
if dragon_slay_check:
if self.difficulty_switch_b == DifficultySwitchB.option_hard_with_unlock_item:
set_rule(world.get_location("Slay Yorgle", self.player),
diff --git a/worlds/adventure/__init__.py b/worlds/adventure/__init__.py
index 1c2583b3ed..ed5ebbd3dc 100644
--- a/worlds/adventure/__init__.py
+++ b/worlds/adventure/__init__.py
@@ -15,7 +15,8 @@ from Options import AssembleOptions
from worlds.AutoWorld import WebWorld, World
from Fill import fill_restrictive
from worlds.generic.Rules import add_rule, set_rule
-from .Options import adventure_option_definitions, DragonRandoType, DifficultySwitchA, DifficultySwitchB
+from .Options import DragonRandoType, DifficultySwitchA, DifficultySwitchB, \
+ AdventureOptions
from .Rom import get_base_rom_bytes, get_base_rom_path, AdventureDeltaPatch, apply_basepatch, \
AdventureAutoCollectLocation
from .Items import item_table, ItemData, nothing_item_id, event_table, AdventureItem, standard_item_max
@@ -109,7 +110,7 @@ class AdventureWorld(World):
game: ClassVar[str] = "Adventure"
web: ClassVar[WebWorld] = AdventureWeb()
- option_definitions: ClassVar[Dict[str, AssembleOptions]] = adventure_option_definitions
+ options_dataclass = AdventureOptions
settings: ClassVar[AdventureSettings]
item_name_to_id: ClassVar[Dict[str, int]] = {name: data.id for name, data in item_table.items()}
location_name_to_id: ClassVar[Dict[str, int]] = {name: data.location_id for name, data in location_table.items()}
@@ -149,18 +150,18 @@ class AdventureWorld(World):
bytearray(f"ADVENTURE{__version__.replace('.', '')[:3]}_{self.player}_{self.multiworld.seed}", "utf8")[:21]
self.rom_name.extend([0] * (21 - len(self.rom_name)))
- self.dragon_rando_type = self.multiworld.dragon_rando_type[self.player].value
- self.dragon_slay_check = self.multiworld.dragon_slay_check[self.player].value
- self.connector_multi_slot = self.multiworld.connector_multi_slot[self.player].value
- self.yorgle_speed = self.multiworld.yorgle_speed[self.player].value
- self.yorgle_min_speed = self.multiworld.yorgle_min_speed[self.player].value
- self.grundle_speed = self.multiworld.grundle_speed[self.player].value
- self.grundle_min_speed = self.multiworld.grundle_min_speed[self.player].value
- self.rhindle_speed = self.multiworld.rhindle_speed[self.player].value
- self.rhindle_min_speed = self.multiworld.rhindle_min_speed[self.player].value
- self.difficulty_switch_a = self.multiworld.difficulty_switch_a[self.player].value
- self.difficulty_switch_b = self.multiworld.difficulty_switch_b[self.player].value
- self.start_castle = self.multiworld.start_castle[self.player].value
+ self.dragon_rando_type = self.options.dragon_rando_type.value
+ self.dragon_slay_check = self.options.dragon_slay_check.value
+ self.connector_multi_slot = self.options.connector_multi_slot.value
+ self.yorgle_speed = self.options.yorgle_speed.value
+ self.yorgle_min_speed = self.options.yorgle_min_speed.value
+ self.grundle_speed = self.options.grundle_speed.value
+ self.grundle_min_speed = self.options.grundle_min_speed.value
+ self.rhindle_speed = self.options.rhindle_speed.value
+ self.rhindle_min_speed = self.options.rhindle_min_speed.value
+ self.difficulty_switch_a = self.options.difficulty_switch_a.value
+ self.difficulty_switch_b = self.options.difficulty_switch_b.value
+ self.start_castle = self.options.start_castle.value
self.created_items = 0
if self.dragon_slay_check == 0:
@@ -227,7 +228,7 @@ class AdventureWorld(World):
extra_filler_count = num_locations - self.created_items
# traps would probably go here, if enabled
- freeincarnate_max = self.multiworld.freeincarnate_max[self.player].value
+ freeincarnate_max = self.options.freeincarnate_max.value
actual_freeincarnates = min(extra_filler_count, freeincarnate_max)
self.multiworld.itempool += [self.create_item("Freeincarnate") for _ in range(actual_freeincarnates)]
self.created_items += actual_freeincarnates
@@ -247,7 +248,7 @@ class AdventureWorld(World):
self.created_items += 1
def create_regions(self) -> None:
- create_regions(self.multiworld, self.player, self.dragon_rooms)
+ create_regions(self.options, self.multiworld, self.player, self.dragon_rooms)
set_rules = set_rules
@@ -354,7 +355,7 @@ class AdventureWorld(World):
auto_collect_locations: [AdventureAutoCollectLocation] = []
local_item_to_location: {int, int} = {}
bat_no_touch_locs: [LocationData] = []
- bat_logic: int = self.multiworld.bat_logic[self.player].value
+ bat_logic: int = self.options.bat_logic.value
try:
rom_deltas: { int, int } = {}
self.place_dragons(rom_deltas)
@@ -421,7 +422,7 @@ class AdventureWorld(World):
item_position_data_start = get_item_position_data_start(unplaced_item.table_index)
rom_deltas[item_position_data_start] = 0xff
- if self.multiworld.connector_multi_slot[self.player].value:
+ if self.options.connector_multi_slot.value:
rom_deltas[connector_port_offset] = (self.player & 0xff)
else:
rom_deltas[connector_port_offset] = 0
From 75bef3ddb15334d22ca6d0bbe905f93c4752a5ea Mon Sep 17 00:00:00 2001
From: Fabian Dill
Date: Tue, 11 Jun 2024 00:42:57 +0200
Subject: [PATCH 15/31] Various: fix absolute imports in worlds (#3489)
---
worlds/messenger/options.py | 2 +-
worlds/shorthike/Rules.py | 3 ++-
worlds/yugioh06/client_bh.py | 2 +-
worlds/yugioh06/opponents.py | 4 ++--
4 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/worlds/messenger/options.py b/worlds/messenger/options.py
index 73adf4ebdf..1f76dba489 100644
--- a/worlds/messenger/options.py
+++ b/worlds/messenger/options.py
@@ -5,7 +5,7 @@ from schema import And, Optional, Or, Schema
from Options import Accessibility, Choice, DeathLinkMixin, DefaultOnToggle, OptionDict, PerGameCommonOptions, \
PlandoConnections, Range, StartInventoryPool, Toggle, Visibility
-from worlds.messenger.portals import CHECKPOINTS, PORTALS, SHOP_POINTS
+from .portals import CHECKPOINTS, PORTALS, SHOP_POINTS
class MessengerAccessibility(Accessibility):
diff --git a/worlds/shorthike/Rules.py b/worlds/shorthike/Rules.py
index 4a71ebd3c8..33741c6d80 100644
--- a/worlds/shorthike/Rules.py
+++ b/worlds/shorthike/Rules.py
@@ -1,5 +1,6 @@
from worlds.generic.Rules import forbid_items_for_player, add_rule
-from worlds.shorthike.Options import Goal, GoldenFeatherProgression, MinShopCheckLogic, ShopCheckLogic
+from .Options import Goal, GoldenFeatherProgression, MinShopCheckLogic, ShopCheckLogic
+
def create_rules(self, location_table):
multiworld = self.multiworld
diff --git a/worlds/yugioh06/client_bh.py b/worlds/yugioh06/client_bh.py
index 910eba7c6a..ecbe48110a 100644
--- a/worlds/yugioh06/client_bh.py
+++ b/worlds/yugioh06/client_bh.py
@@ -5,7 +5,7 @@ from NetUtils import ClientStatus, NetworkItem
import worlds._bizhawk as bizhawk
from worlds._bizhawk.client import BizHawkClient
-from worlds.yugioh06 import item_to_index
+from . import item_to_index
if TYPE_CHECKING:
from worlds._bizhawk.context import BizHawkClientContext
diff --git a/worlds/yugioh06/opponents.py b/worlds/yugioh06/opponents.py
index 1746b56529..68d7c2880f 100644
--- a/worlds/yugioh06/opponents.py
+++ b/worlds/yugioh06/opponents.py
@@ -3,8 +3,8 @@ from typing import Dict, List, NamedTuple, Optional, Union
from BaseClasses import MultiWorld
from worlds.generic.Rules import CollectionRule
-from worlds.yugioh06 import item_to_index, tier_1_opponents, yugioh06_difficulty
-from worlds.yugioh06.locations import special
+from . import item_to_index, tier_1_opponents, yugioh06_difficulty
+from .locations import special
class OpponentData(NamedTuple):
From ccfffa11478981cdaa9317da88526bf75d27afdd Mon Sep 17 00:00:00 2001
From: Zach Parks
Date: Mon, 10 Jun 2024 18:55:02 -0500
Subject: [PATCH 16/31] CODEOWNERS: Replace @ThePhar with @qwint as Hollow
Knight maintainer. (#3508)
---
docs/CODEOWNERS | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS
index 10b962d499..9fbd483796 100644
--- a/docs/CODEOWNERS
+++ b/docs/CODEOWNERS
@@ -70,7 +70,7 @@
/worlds/heretic/ @Daivuk
# Hollow Knight
-/worlds/hk/ @BadMagic100 @ThePhar
+/worlds/hk/ @BadMagic100 @qwint
# Hylics 2
/worlds/hylics2/ @TRPG0
From 54531c6ebae4f3dab1ebb08a6513d51e96517e31 Mon Sep 17 00:00:00 2001
From: Justus Lind
Date: Tue, 11 Jun 2024 11:11:19 +1000
Subject: [PATCH 17/31] Muse Dash: Remove regions for a decent speed gain in
generating worlds (#3435)
* Remove Muse Dash Regions.
* Update comments.
---
worlds/musedash/__init__.py | 20 ++++++++------------
1 file changed, 8 insertions(+), 12 deletions(-)
diff --git a/worlds/musedash/__init__.py b/worlds/musedash/__init__.py
index a9eacbbcf8..ab3a4819fc 100644
--- a/worlds/musedash/__init__.py
+++ b/worlds/musedash/__init__.py
@@ -249,9 +249,7 @@ class MuseDashWorld(World):
def create_regions(self) -> None:
menu_region = Region("Menu", self.player, self.multiworld)
- song_select_region = Region("Song Select", self.player, self.multiworld)
- self.multiworld.regions += [menu_region, song_select_region]
- menu_region.connect(song_select_region)
+ self.multiworld.regions += [menu_region]
# Make a collection of all songs available for this rando.
# 1. All starting songs
@@ -265,18 +263,16 @@ class MuseDashWorld(World):
self.random.shuffle(included_song_copy)
all_selected_locations.extend(included_song_copy)
- # Make a region per song/album, then adds 1-2 item locations to them
+ # Adds 2 item locations per song/album to the menu region.
for i in range(0, len(all_selected_locations)):
name = all_selected_locations[i]
- region = Region(name, self.player, self.multiworld)
- self.multiworld.regions.append(region)
- song_select_region.connect(region, name, lambda state, place=name: state.has(place, self.player))
+ loc1 = MuseDashLocation(self.player, name + "-0", self.md_collection.song_locations[name + "-0"], menu_region)
+ loc1.access_rule = lambda state, place=name: state.has(place, self.player)
+ menu_region.locations.append(loc1)
- # Muse Dash requires 2 locations per song to be *interesting*. Balanced out by filler.
- region.add_locations({
- name + "-0": self.md_collection.song_locations[name + "-0"],
- name + "-1": self.md_collection.song_locations[name + "-1"]
- }, MuseDashLocation)
+ loc2 = MuseDashLocation(self.player, name + "-1", self.md_collection.song_locations[name + "-1"], menu_region)
+ loc2.access_rule = lambda state, place=name: state.has(place, self.player)
+ menu_region.locations.append(loc2)
def set_rules(self) -> None:
self.multiworld.completion_condition[self.player] = lambda state: \
From 87d24eb38a00b1eee26b61a27d3cd9f1fd264e6a Mon Sep 17 00:00:00 2001
From: Louis M
Date: Tue, 11 Jun 2024 18:59:46 -0400
Subject: [PATCH 18/31] Aquaria: Add entrance rule and fix
start_inventory_from_pool (#3473)
---
worlds/aquaria/Locations.py | 6 +++---
worlds/aquaria/Regions.py | 7 ++++---
worlds/aquaria/__init__.py | 12 ++++--------
worlds/aquaria/test/__init__.py | 6 +++---
worlds/aquaria/test/test_beast_form_access.py | 4 ++--
.../test_no_progression_hard_hidden_locations.py | 2 +-
.../test/test_progression_hard_hidden_locations.py | 2 +-
worlds/aquaria/test/test_sun_form_access.py | 3 +++
8 files changed, 21 insertions(+), 21 deletions(-)
diff --git a/worlds/aquaria/Locations.py b/worlds/aquaria/Locations.py
index 7360efde06..33d165db41 100644
--- a/worlds/aquaria/Locations.py
+++ b/worlds/aquaria/Locations.py
@@ -185,7 +185,7 @@ class AquariaLocations:
"Mithalas City, second bulb at the end of the top path": 698040,
"Mithalas City, bulb in the top path": 698036,
"Mithalas City, Mithalas Pot": 698174,
- "Mithalas City, urn in the Cathedral flower tube entrance": 698128,
+ "Mithalas City, urn in the Castle flower tube entrance": 698128,
}
locations_mithalas_city_fishpass = {
@@ -246,7 +246,7 @@ class AquariaLocations:
"Kelp Forest top left area, bulb in the bottom left clearing": 698044,
"Kelp Forest top left area, bulb in the path down from the top left clearing": 698045,
"Kelp Forest top left area, bulb in the top left clearing": 698046,
- "Kelp Forest top left, Jelly Egg": 698185,
+ "Kelp Forest top left area, Jelly Egg": 698185,
}
locations_forest_tl_fp = {
@@ -332,7 +332,7 @@ class AquariaLocations:
}
locations_veil_tr_l = {
- "The Veil top right area, bulb in the top of the waterfall": 698080,
+ "The Veil top right area, bulb at the top of the waterfall": 698080,
"The Veil top right area, Transturtle": 698210,
}
diff --git a/worlds/aquaria/Regions.py b/worlds/aquaria/Regions.py
index f2f85749f3..2812025925 100755
--- a/worlds/aquaria/Regions.py
+++ b/worlds/aquaria/Regions.py
@@ -771,6 +771,7 @@ class AquariaRegions:
self.__connect_regions("Sunken City left area", "Sunken City boss area",
self.sunken_city_l, self.sunken_city_boss,
lambda state: _has_beast_form(state, self.player) and
+ _has_sun_form(state, self.player) and
_has_energy_form(state, self.player) and
_has_bind_song(state, self.player))
@@ -983,7 +984,7 @@ class AquariaRegions:
lambda state: _has_damaging_item(state, self.player))
add_rule(self.multiworld.get_location("Mithalas City, third urn in the city reserve", self.player),
lambda state: _has_damaging_item(state, self.player))
- add_rule(self.multiworld.get_location("Mithalas City, urn in the Cathedral flower tube entrance", self.player),
+ add_rule(self.multiworld.get_location("Mithalas City, urn in the Castle flower tube entrance", self.player),
lambda state: _has_damaging_item(state, self.player))
add_rule(self.multiworld.get_location("Mithalas City Castle, urn in the bedroom", self.player),
lambda state: _has_damaging_item(state, self.player))
@@ -1023,7 +1024,7 @@ class AquariaRegions:
lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player))
add_rule(self.multiworld.get_location("Sun Worm path, second cliff bulb", self.player),
lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player))
- add_rule(self.multiworld.get_location("The Veil top right area, bulb in the top of the waterfall", self.player),
+ add_rule(self.multiworld.get_location("The Veil top right area, bulb at the top of the waterfall", self.player),
lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player))
def __adjusting_under_rock_location(self) -> None:
@@ -1175,7 +1176,7 @@ class AquariaRegions:
self.multiworld.get_location("Sun Worm path, second cliff bulb",
self.player).item_rule =\
lambda item: item.classification != ItemClassification.progression
- self.multiworld.get_location("The Veil top right area, bulb in the top of the waterfall",
+ self.multiworld.get_location("The Veil top right area, bulb at the top of the waterfall",
self.player).item_rule =\
lambda item: item.classification != ItemClassification.progression
self.multiworld.get_location("Bubble Cave, bulb in the left cave wall",
diff --git a/worlds/aquaria/__init__.py b/worlds/aquaria/__init__.py
index 3c0cc3bded..ce46aeea75 100644
--- a/worlds/aquaria/__init__.py
+++ b/worlds/aquaria/__init__.py
@@ -167,14 +167,10 @@ class AquariaWorld(World):
self.__pre_fill_item("Transturtle Simon Says", "Arnassi Ruins, Transturtle", precollected)
self.__pre_fill_item("Transturtle Arnassi Ruins", "Simon Says area, Transturtle", precollected)
for name, data in item_table.items():
- if name in precollected:
- precollected.remove(name)
- self.multiworld.itempool.append(self.create_item(self.get_filler_item_name()))
- else:
- if name not in self.exclude:
- for i in range(data.count):
- item = self.create_item(name)
- self.multiworld.itempool.append(item)
+ if name not in self.exclude:
+ for i in range(data.count):
+ item = self.create_item(name)
+ self.multiworld.itempool.append(item)
def set_rules(self) -> None:
"""
diff --git a/worlds/aquaria/test/__init__.py b/worlds/aquaria/test/__init__.py
index 198ccb0f62..5c63c9bb29 100644
--- a/worlds/aquaria/test/__init__.py
+++ b/worlds/aquaria/test/__init__.py
@@ -56,7 +56,7 @@ after_home_water_locations = [
"Mithalas City, second bulb at the end of the top path",
"Mithalas City, bulb in the top path",
"Mithalas City, Mithalas Pot",
- "Mithalas City, urn in the Cathedral flower tube entrance",
+ "Mithalas City, urn in the Castle flower tube entrance",
"Mithalas City, Doll",
"Mithalas City, urn inside a home fish pass",
"Mithalas City Castle, bulb in the flesh hole",
@@ -93,7 +93,7 @@ after_home_water_locations = [
"Kelp Forest top left area, bulb in the bottom left clearing",
"Kelp Forest top left area, bulb in the path down from the top left clearing",
"Kelp Forest top left area, bulb in the top left clearing",
- "Kelp Forest top left, Jelly Egg",
+ "Kelp Forest top left area, Jelly Egg",
"Kelp Forest top left area, bulb close to the Verse Egg",
"Kelp Forest top left area, Verse Egg",
"Kelp Forest top right area, bulb under the rock in the right path",
@@ -125,7 +125,7 @@ after_home_water_locations = [
"Turtle cave, Urchin Costume",
"The Veil top right area, bulb in the middle of the wall jump cliff",
"The Veil top right area, Golden Starfish",
- "The Veil top right area, bulb in the top of the waterfall",
+ "The Veil top right area, bulb at the top of the waterfall",
"The Veil top right area, Transturtle",
"The Veil bottom area, bulb in the left path",
"The Veil bottom area, bulb in the spirit path",
diff --git a/worlds/aquaria/test/test_beast_form_access.py b/worlds/aquaria/test/test_beast_form_access.py
index c25070d470..4bb4d5656c 100644
--- a/worlds/aquaria/test/test_beast_form_access.py
+++ b/worlds/aquaria/test/test_beast_form_access.py
@@ -20,14 +20,14 @@ class BeastFormAccessTest(AquariaTestBase):
"Mithalas City, second bulb at the end of the top path",
"Mithalas City, bulb in the top path",
"Mithalas City, Mithalas Pot",
- "Mithalas City, urn in the Cathedral flower tube entrance",
+ "Mithalas City, urn in the Castle flower tube entrance",
"Mermog cave, Piranha Egg",
"Mithalas Cathedral, Mithalan Dress",
"Turtle cave, bulb in Bubble Cliff",
"Turtle cave, Urchin Costume",
"Sun Worm path, first cliff bulb",
"Sun Worm path, second cliff bulb",
- "The Veil top right area, bulb in the top of the waterfall",
+ "The Veil top right area, bulb at the top of the waterfall",
"Bubble Cave, bulb in the left cave wall",
"Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
"Bubble Cave, Verse Egg",
diff --git a/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py b/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py
index 817b9547a8..b0d2b0d880 100644
--- a/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py
+++ b/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py
@@ -30,7 +30,7 @@ class UNoProgressionHardHiddenTest(AquariaTestBase):
"Final Boss area, bulb in the boss third form room",
"Sun Worm path, first cliff bulb",
"Sun Worm path, second cliff bulb",
- "The Veil top right area, bulb in the top of the waterfall",
+ "The Veil top right area, bulb at the top of the waterfall",
"Bubble Cave, bulb in the left cave wall",
"Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
"Bubble Cave, Verse Egg",
diff --git a/worlds/aquaria/test/test_progression_hard_hidden_locations.py b/worlds/aquaria/test/test_progression_hard_hidden_locations.py
index 2b7c8ddac9..390fc40b29 100644
--- a/worlds/aquaria/test/test_progression_hard_hidden_locations.py
+++ b/worlds/aquaria/test/test_progression_hard_hidden_locations.py
@@ -30,7 +30,7 @@ class UNoProgressionHardHiddenTest(AquariaTestBase):
"Final Boss area, bulb in the boss third form room",
"Sun Worm path, first cliff bulb",
"Sun Worm path, second cliff bulb",
- "The Veil top right area, bulb in the top of the waterfall",
+ "The Veil top right area, bulb at the top of the waterfall",
"Bubble Cave, bulb in the left cave wall",
"Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
"Bubble Cave, Verse Egg",
diff --git a/worlds/aquaria/test/test_sun_form_access.py b/worlds/aquaria/test/test_sun_form_access.py
index dfd732ec91..cbe8c08a52 100644
--- a/worlds/aquaria/test/test_sun_form_access.py
+++ b/worlds/aquaria/test/test_sun_form_access.py
@@ -18,6 +18,9 @@ class SunFormAccessTest(AquariaTestBase):
"Abyss right area, bulb behind the rock in the whale room",
"Octopus Cave, Dumbo Egg",
"Beating Octopus Prime",
+ "Sunken City, bulb on top of the boss area",
+ "Beating the Golem",
+ "Sunken City cleared",
"Final Boss area, bulb in the boss third form room",
"Objective complete"
]
From e755f1a0b5b588f51b26e8ac9eddd69fba044567 Mon Sep 17 00:00:00 2001
From: Fabian Dill
Date: Wed, 12 Jun 2024 02:14:30 +0200
Subject: [PATCH 19/31] SC2: don't close all SC2 instances when one quits
(#3507)
---
worlds/_sc2common/bot/sc2process.py | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/worlds/_sc2common/bot/sc2process.py b/worlds/_sc2common/bot/sc2process.py
index e366321659..f74ed9c18f 100644
--- a/worlds/_sc2common/bot/sc2process.py
+++ b/worlds/_sc2common/bot/sc2process.py
@@ -28,6 +28,11 @@ class kill_switch:
logger.debug("kill_switch: Add switch")
cls._to_kill.append(value)
+ @classmethod
+ def kill(cls, value):
+ logger.info(f"kill_switch: Process cleanup for 1 process")
+ value._clean(verbose=False)
+
@classmethod
def kill_all(cls):
logger.info(f"kill_switch: Process cleanup for {len(cls._to_kill)} processes")
@@ -116,7 +121,7 @@ class SC2Process:
async def __aexit__(self, *args):
logger.exception("async exit")
await self._close_connection()
- kill_switch.kill_all()
+ kill_switch.kill(self)
signal.signal(signal.SIGINT, signal.SIG_DFL)
@property
From 7299891bdf86d5134692ae55fac92ad32ba17059 Mon Sep 17 00:00:00 2001
From: Natalie Weizenbaum
Date: Tue, 11 Jun 2024 18:22:14 -0700
Subject: [PATCH 20/31] Allow worlds to add options to prebuilt groups (#3509)
Previously, this crashed because `typing.NamedTuple` fields such as
`group.name` aren't assignable. Now it will only fail for group names
that are actually incorrectly cased, and will fail with a better error
message.
---
worlds/AutoWorld.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py
index 6e17f023f6..bed375cf08 100644
--- a/worlds/AutoWorld.py
+++ b/worlds/AutoWorld.py
@@ -123,8 +123,8 @@ class WebWorldRegister(type):
assert group.options, "A custom defined Option Group must contain at least one Option."
# catch incorrectly titled versions of the prebuilt groups so they don't create extra groups
title_name = group.name.title()
- if title_name in prebuilt_options:
- group.name = title_name
+ assert title_name not in prebuilt_options or title_name == group.name, \
+ f"Prebuilt group name \"{group.name}\" must be \"{title_name}\""
if group.name == "Item & Location Options":
assert not any(option in item_and_loc_options for option in group.options), \
From b9e454ab4ec7903a66e71324805f93b0332bc286 Mon Sep 17 00:00:00 2001
From: Silvris <58583688+Silvris@users.noreply.github.com>
Date: Tue, 11 Jun 2024 20:23:46 -0500
Subject: [PATCH 21/31] TS: add indirect connections (#3490)
---
worlds/timespinner/Regions.py | 18 +++++++++++++-----
1 file changed, 13 insertions(+), 5 deletions(-)
diff --git a/worlds/timespinner/Regions.py b/worlds/timespinner/Regions.py
index 4f53f75eff..757a41c388 100644
--- a/worlds/timespinner/Regions.py
+++ b/worlds/timespinner/Regions.py
@@ -70,7 +70,7 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w
logic = TimespinnerLogic(world, player, precalculated_weights)
connect(world, player, 'Lake desolation', 'Lower lake desolation', lambda state: flooded.flood_lake_desolation or logic.has_timestop(state) or state.has('Talaria Attachment', player))
- connect(world, player, 'Lake desolation', 'Upper lake desolation', lambda state: logic.has_fire(state) and state.can_reach('Upper Lake Serene', 'Region', player))
+ connect(world, player, 'Lake desolation', 'Upper lake desolation', lambda state: logic.has_fire(state) and state.can_reach('Upper Lake Serene', 'Region', player), "Upper Lake Serene")
connect(world, player, 'Lake desolation', 'Skeleton Shaft', lambda state: flooded.flood_lake_desolation or logic.has_doublejump(state))
connect(world, player, 'Lake desolation', 'Space time continuum', logic.has_teleport)
connect(world, player, 'Upper lake desolation', 'Lake desolation')
@@ -80,7 +80,7 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w
connect(world, player, 'Eastern lake desolation', 'Space time continuum', logic.has_teleport)
connect(world, player, 'Eastern lake desolation', 'Library')
connect(world, player, 'Eastern lake desolation', 'Lower lake desolation')
- connect(world, player, 'Eastern lake desolation', 'Upper lake desolation', lambda state: logic.has_fire(state) and state.can_reach('Upper Lake Serene', 'Region', player))
+ connect(world, player, 'Eastern lake desolation', 'Upper lake desolation', lambda state: logic.has_fire(state) and state.can_reach('Upper Lake Serene', 'Region', player), "Upper Lake Serene")
connect(world, player, 'Library', 'Eastern lake desolation')
connect(world, player, 'Library', 'Library top', lambda state: logic.has_doublejump(state) or state.has('Talaria Attachment', player))
connect(world, player, 'Library', 'Varndagroth tower left', logic.has_keycard_D)
@@ -185,7 +185,7 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w
if is_option_enabled(world, player, "GyreArchives"):
connect(world, player, 'The lab (upper)', 'Ravenlord\'s Lair', lambda state: state.has('Merchant Crow', player))
connect(world, player, 'Ravenlord\'s Lair', 'The lab (upper)')
- connect(world, player, 'Library top', 'Ifrit\'s Lair', lambda state: state.has('Kobo', player) and state.can_reach('Refugee Camp', 'Region', player))
+ connect(world, player, 'Library top', 'Ifrit\'s Lair', lambda state: state.has('Kobo', player) and state.can_reach('Refugee Camp', 'Region', player), "Refugee Camp")
connect(world, player, 'Ifrit\'s Lair', 'Library top')
@@ -242,11 +242,19 @@ def connectStartingRegion(world: MultiWorld, player: int):
def connect(world: MultiWorld, player: int, source: str, target: str,
- rule: Optional[Callable[[CollectionState], bool]] = None):
+ rule: Optional[Callable[[CollectionState], bool]] = None,
+ indirect: str = ""):
sourceRegion = world.get_region(source, player)
targetRegion = world.get_region(target, player)
- sourceRegion.connect(targetRegion, rule=rule)
+ entrance = sourceRegion.connect(targetRegion, rule=rule)
+
+ if indirect:
+ indirectRegion = world.get_region(indirect, player)
+ if indirectRegion in world.indirect_connections:
+ world.indirect_connections[indirectRegion].add(entrance)
+ else:
+ world.indirect_connections[indirectRegion] = {entrance}
def split_location_datas_per_region(locations: List[LocationData]) -> Dict[str, List[LocationData]]:
From 3b9b9353b7e7588cb59c496fd295397dcbcdf9b6 Mon Sep 17 00:00:00 2001
From: Fabian Dill
Date: Wed, 12 Jun 2024 15:34:46 +0200
Subject: [PATCH 22/31] WebHost: delete old docs files (#3503)
---
WebHost.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/WebHost.py b/WebHost.py
index afacd6288e..08ef3c4307 100644
--- a/WebHost.py
+++ b/WebHost.py
@@ -58,6 +58,7 @@ def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]]
worlds[game] = world
base_target_path = Utils.local_path("WebHostLib", "static", "generated", "docs")
+ shutil.rmtree(base_target_path, ignore_errors=True)
for game, world in worlds.items():
# copy files from world's docs folder to the generated folder
target_path = os.path.join(base_target_path, game)
From 2daccded365258633bd8ac268c5a58ae9ee31a43 Mon Sep 17 00:00:00 2001
From: Fabian Dill
Date: Wed, 12 Jun 2024 15:35:51 +0200
Subject: [PATCH 23/31] Core: don't lock progression (#3501)
---
Fill.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/Fill.py b/Fill.py
index d8147b2eac..4967ff0736 100644
--- a/Fill.py
+++ b/Fill.py
@@ -483,15 +483,15 @@ def distribute_items_restrictive(multiworld: MultiWorld,
if panic_method == "swap":
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool,
swap=True,
- on_place=mark_for_locking, name="Progression", single_player_placement=multiworld.players == 1)
+ name="Progression", single_player_placement=multiworld.players == 1)
elif panic_method == "raise":
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool,
swap=False,
- on_place=mark_for_locking, name="Progression", single_player_placement=multiworld.players == 1)
+ name="Progression", single_player_placement=multiworld.players == 1)
elif panic_method == "start_inventory":
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool,
swap=False, allow_partial=True,
- on_place=mark_for_locking, name="Progression", single_player_placement=multiworld.players == 1)
+ name="Progression", single_player_placement=multiworld.players == 1)
if progitempool:
for item in progitempool:
logging.debug(f"Moved {item} to start_inventory to prevent fill failure.")
From acf85eb9abb3de4863fe9350a6cdddee71c9be44 Mon Sep 17 00:00:00 2001
From: black-sliver <59490463+black-sliver@users.noreply.github.com>
Date: Wed, 12 Jun 2024 18:54:59 +0200
Subject: [PATCH 24/31] Speedups: remove dependency on c++ (#2796)
* Speedups: remove dependency on c++
* Speedups: intset: handle malloc failing
* Speedups: intset: fix corner case for int64 on 32bit systems
original idea was to only use bucket->val if int(-1) # this is all 0xff... adding 1 results in 0, but it's not negative
+# configure INTSET for player
+cdef extern from *:
+ """
+ #define INTSET_NAME ap_player_set
+ #define INTSET_TYPE uint32_t // has to match ap_player_t
+ """
+
+# create INTSET for player
+cdef extern from "intset.h":
+ """
+ #undef INTSET_NAME
+ #undef INTSET_TYPE
+ """
+ ctypedef struct ap_player_set:
+ pass
+
+ ap_player_set* ap_player_set_new(size_t bucket_count) nogil
+ void ap_player_set_free(ap_player_set* set) nogil
+ bint ap_player_set_add(ap_player_set* set, ap_player_t val) nogil
+ bint ap_player_set_contains(ap_player_set* set, ap_player_t val) nogil
+
cdef struct LocationEntry:
# layout is so that
@@ -185,7 +206,7 @@ cdef class LocationStore:
def find_item(self, slots: Set[int], seeked_item_id: int) -> Generator[Tuple[int, int, int, int, int], None, None]:
cdef ap_id_t item = seeked_item_id
cdef ap_player_t receiver
- cdef std_set[ap_player_t] receivers
+ cdef ap_player_set* receivers
cdef size_t slot_count = len(slots)
if slot_count == 1:
# specialized implementation for single slot
@@ -197,13 +218,20 @@ cdef class LocationStore:
yield entry.sender, entry.location, entry.item, entry.receiver, entry.flags
elif slot_count:
# generic implementation with lookup in set
- for receiver in slots:
- receivers.insert(receiver)
- with nogil:
- for entry in self.entries[:self.entry_count]:
- if entry.item == item and receivers.count(entry.receiver):
- with gil:
- yield entry.sender, entry.location, entry.item, entry.receiver, entry.flags
+ receivers = ap_player_set_new(min(1023, slot_count)) # limit top level struct to 16KB
+ if not receivers:
+ raise MemoryError()
+ try:
+ for receiver in slots:
+ if not ap_player_set_add(receivers, receiver):
+ raise MemoryError()
+ with nogil:
+ for entry in self.entries[:self.entry_count]:
+ if entry.item == item and ap_player_set_contains(receivers, entry.receiver):
+ with gil:
+ yield entry.sender, entry.location, entry.item, entry.receiver, entry.flags
+ finally:
+ ap_player_set_free(receivers)
def get_for_player(self, slot: int) -> Dict[int, Set[int]]:
cdef ap_player_t receiver = slot
diff --git a/_speedups.pyxbld b/_speedups.pyxbld
index e1fe19b2ef..974eaed03b 100644
--- a/_speedups.pyxbld
+++ b/_speedups.pyxbld
@@ -1,8 +1,10 @@
-# This file is required to get pyximport to work with C++.
-# Switching from std::set to a pure C implementation is still on the table to simplify everything.
+# This file is used when doing pyximport
+import os
def make_ext(modname, pyxfilename):
from distutils.extension import Extension
return Extension(name=modname,
sources=[pyxfilename],
- language='c++')
+ depends=["intset.h"],
+ include_dirs=[os.getcwd()],
+ language="c")
diff --git a/intset.h b/intset.h
new file mode 100644
index 0000000000..fac84fb6f8
--- /dev/null
+++ b/intset.h
@@ -0,0 +1,135 @@
+/* A specialized unordered_set implementation for literals, where bucket_count
+ * is defined at initialization rather than increased automatically.
+ */
+#include
+#include
+#include
+#include
+
+#ifndef INTSET_NAME
+#error "Please #define INTSET_NAME ... before including intset.h"
+#endif
+
+#ifndef INTSET_TYPE
+#error "Please #define INTSET_TYPE ... before including intset.h"
+#endif
+
+/* macros to generate unique names from INTSET_NAME */
+#ifndef INTSET_CONCAT
+#define INTSET_CONCAT_(a, b) a ## b
+#define INTSET_CONCAT(a, b) INTSET_CONCAT_(a, b)
+#define INTSET_FUNC_(a, b) INTSET_CONCAT(a, _ ## b)
+#endif
+
+#define INTSET_FUNC(name) INTSET_FUNC_(INTSET_NAME, name)
+#define INTSET_BUCKET INTSET_CONCAT(INTSET_NAME, Bucket)
+#define INTSET_UNION INTSET_CONCAT(INTSET_NAME, Union)
+
+#if defined(_MSC_VER)
+#pragma warning(push)
+#pragma warning(disable : 4200)
+#endif
+
+
+typedef struct {
+ size_t count;
+ union INTSET_UNION {
+ INTSET_TYPE val;
+ INTSET_TYPE *data;
+ } v;
+} INTSET_BUCKET;
+
+typedef struct {
+ size_t bucket_count;
+ INTSET_BUCKET buckets[];
+} INTSET_NAME;
+
+static INTSET_NAME *INTSET_FUNC(new)(size_t buckets)
+{
+ size_t i, size;
+ INTSET_NAME *set;
+
+ if (buckets < 1)
+ buckets = 1;
+ if ((SIZE_MAX - sizeof(INTSET_NAME)) / sizeof(INTSET_BUCKET) < buckets)
+ return NULL;
+ size = sizeof(INTSET_NAME) + buckets * sizeof(INTSET_BUCKET);
+ set = (INTSET_NAME*)malloc(size);
+ if (!set)
+ return NULL;
+ memset(set, 0, size); /* gcc -fanalyzer does not understand this sets all buckets' count to 0 */
+ for (i = 0; i < buckets; i++) {
+ set->buckets[i].count = 0;
+ }
+ set->bucket_count = buckets;
+ return set;
+}
+
+static void INTSET_FUNC(free)(INTSET_NAME *set)
+{
+ size_t i;
+ if (!set)
+ return;
+ for (i = 0; i < set->bucket_count; i++) {
+ if (set->buckets[i].count > 1)
+ free(set->buckets[i].v.data);
+ }
+ free(set);
+}
+
+static bool INTSET_FUNC(contains)(INTSET_NAME *set, INTSET_TYPE val)
+{
+ size_t i;
+ INTSET_BUCKET* bucket = &set->buckets[(size_t)val % set->bucket_count];
+ if (bucket->count == 1)
+ return bucket->v.val == val;
+ for (i = 0; i < bucket->count; ++i) {
+ if (bucket->v.data[i] == val)
+ return true;
+ }
+ return false;
+}
+
+static bool INTSET_FUNC(add)(INTSET_NAME *set, INTSET_TYPE val)
+{
+ INTSET_BUCKET* bucket;
+
+ if (INTSET_FUNC(contains)(set, val))
+ return true; /* ok */
+
+ bucket = &set->buckets[(size_t)val % set->bucket_count];
+ if (bucket->count == 0) {
+ bucket->v.val = val;
+ bucket->count = 1;
+ } else if (bucket->count == 1) {
+ INTSET_TYPE old = bucket->v.val;
+ bucket->v.data = (INTSET_TYPE*)malloc(2 * sizeof(INTSET_TYPE));
+ if (!bucket->v.data) {
+ bucket->v.val = old;
+ return false; /* error */
+ }
+ bucket->v.data[0] = old;
+ bucket->v.data[1] = val;
+ bucket->count = 2;
+ } else {
+ size_t new_bucket_size;
+ INTSET_TYPE* new_bucket_data;
+
+ new_bucket_size = (bucket->count + 1) * sizeof(INTSET_TYPE);
+ new_bucket_data = (INTSET_TYPE*)realloc(bucket->v.data, new_bucket_size);
+ if (!new_bucket_data)
+ return false; /* error */
+ bucket->v.data = new_bucket_data;
+ bucket->v.data[bucket->count++] = val;
+ }
+ return true; /* success */
+}
+
+
+#if defined(_MSC_VER)
+#pragma warning(pop)
+#endif
+
+#undef INTSET_FUNC
+#undef INTSET_BUCKET
+#undef INTSET_UNION
diff --git a/test/cpp/CMakeLists.txt b/test/cpp/CMakeLists.txt
new file mode 100644
index 0000000000..927b7494da
--- /dev/null
+++ b/test/cpp/CMakeLists.txt
@@ -0,0 +1,49 @@
+cmake_minimum_required(VERSION 3.5)
+project(ap-cpp-tests)
+
+enable_testing()
+
+find_package(GTest REQUIRED)
+
+if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
+ add_definitions("/source-charset:utf-8")
+ set(CMAKE_CXX_FLAGS_DEBUG "/MTd")
+ set(CMAKE_CXX_FLAGS_RELEASE "/MT")
+elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
+ # enable static analysis for gcc
+ add_compile_options(-fanalyzer -Werror)
+ # disable stuff that gets triggered by googletest
+ add_compile_options(-Wno-analyzer-malloc-leak)
+ # enable asan for gcc
+ add_compile_options(-fsanitize=address)
+ add_link_options(-fsanitize=address)
+endif ()
+
+add_executable(test_default)
+
+target_include_directories(test_default
+ PRIVATE
+ ${GTEST_INCLUDE_DIRS}
+)
+
+target_link_libraries(test_default
+ ${GTEST_BOTH_LIBRARIES}
+)
+
+add_test(
+ NAME test_default
+ COMMAND test_default
+)
+
+set_property(
+ TEST test_default
+ PROPERTY ENVIRONMENT "ASAN_OPTIONS=allocator_may_return_null=1"
+)
+
+file(GLOB ITEMS *)
+foreach(item ${ITEMS})
+ if(IS_DIRECTORY ${item} AND EXISTS ${item}/CMakeLists.txt)
+ message(${item})
+ add_subdirectory(${item})
+ endif()
+endforeach()
diff --git a/test/cpp/README.md b/test/cpp/README.md
new file mode 100644
index 0000000000..792b9be77e
--- /dev/null
+++ b/test/cpp/README.md
@@ -0,0 +1,32 @@
+# C++ tests
+
+Test framework for C and C++ code in AP.
+
+## Adding a Test
+
+### GoogleTest
+
+Adding GoogleTests is as simple as creating a directory with
+* one or more `test_*.cpp` files that define tests using
+ [GoogleTest API](https://google.github.io/googletest/)
+* a `CMakeLists.txt` that adds the .cpp files to `test_default` target using
+ [target_sources](https://cmake.org/cmake/help/latest/command/target_sources.html)
+
+### CTest
+
+If either GoogleTest is not suitable for the test or the build flags / sources / libraries are incompatible,
+you can add another CTest to the project using add_target and add_test, similar to how it's done for `test_default`.
+
+## Running Tests
+
+* Install [CMake](https://cmake.org/).
+* Build and/or install GoogleTest and make sure
+ [CMake can find it](https://cmake.org/cmake/help/latest/module/FindGTest.html), or
+ [create a parent `CMakeLists.txt` that fetches GoogleTest](https://google.github.io/googletest/quickstart-cmake.html).
+* Enter the directory with the top-most `CMakeLists.txt` and run
+ ```sh
+ mkdir build
+ cmake -S . -B build/ -DCMAKE_BUILD_TYPE=Release
+ cmake --build build/ --config Release && \
+ ctest --test-dir build/ -C Release --output-on-failure
+ ```
diff --git a/test/cpp/intset/CMakeLists.txt b/test/cpp/intset/CMakeLists.txt
new file mode 100644
index 0000000000..175e0bd0b9
--- /dev/null
+++ b/test/cpp/intset/CMakeLists.txt
@@ -0,0 +1,4 @@
+target_sources(test_default
+ PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}/test_intset.cpp
+)
diff --git a/test/cpp/intset/test_intset.cpp b/test/cpp/intset/test_intset.cpp
new file mode 100644
index 0000000000..2f85bea960
--- /dev/null
+++ b/test/cpp/intset/test_intset.cpp
@@ -0,0 +1,105 @@
+#include
+#include
+#include
+
+// uint32Set
+#define INTSET_NAME uint32Set
+#define INTSET_TYPE uint32_t
+#include "../../../intset.h"
+#undef INTSET_NAME
+#undef INTSET_TYPE
+
+// int64Set
+#define INTSET_NAME int64Set
+#define INTSET_TYPE int64_t
+#include "../../../intset.h"
+
+
+TEST(IntsetTest, ZeroBuckets)
+{
+ // trying to allocate with zero buckets has to either fail or be functioning
+ uint32Set *set = uint32Set_new(0);
+ if (!set)
+ return; // failed -> OK
+
+ EXPECT_FALSE(uint32Set_contains(set, 1));
+ EXPECT_TRUE(uint32Set_add(set, 1));
+ EXPECT_TRUE(uint32Set_contains(set, 1));
+ uint32Set_free(set);
+}
+
+TEST(IntsetTest, Duplicate)
+{
+ // adding the same number again can't fail
+ uint32Set *set = uint32Set_new(2);
+ ASSERT_TRUE(set);
+ EXPECT_TRUE(uint32Set_add(set, 0));
+ EXPECT_TRUE(uint32Set_add(set, 0));
+ EXPECT_TRUE(uint32Set_contains(set, 0));
+ uint32Set_free(set);
+}
+
+TEST(IntsetTest, SetAllocFailure)
+{
+ // try to allocate 100TB of RAM, should fail and return NULL
+ if (sizeof(size_t) < 8)
+ GTEST_SKIP() << "Alloc error not testable on 32bit";
+ int64Set *set = int64Set_new(6250000000000ULL);
+ EXPECT_FALSE(set);
+ int64Set_free(set);
+}
+
+TEST(IntsetTest, SetAllocOverflow)
+{
+ // try to overflow argument passed to malloc
+ int64Set *set = int64Set_new(std::numeric_limits::max());
+ EXPECT_FALSE(set);
+ int64Set_free(set);
+}
+
+TEST(IntsetTest, NullFree)
+{
+ // free(NULL) should not try to free buckets
+ uint32Set_free(NULL);
+ int64Set_free(NULL);
+}
+
+TEST(IntsetTest, BucketRealloc)
+{
+ // add a couple of values to the same bucket to test growing the bucket
+ uint32Set* set = uint32Set_new(1);
+ ASSERT_TRUE(set);
+ EXPECT_FALSE(uint32Set_contains(set, 0));
+ EXPECT_TRUE(uint32Set_add(set, 0));
+ EXPECT_TRUE(uint32Set_contains(set, 0));
+ for (uint32_t i = 1; i < 32; ++i) {
+ EXPECT_TRUE(uint32Set_add(set, i));
+ EXPECT_TRUE(uint32Set_contains(set, i - 1));
+ EXPECT_TRUE(uint32Set_contains(set, i));
+ EXPECT_FALSE(uint32Set_contains(set, i + 1));
+ }
+ uint32Set_free(set);
+}
+
+TEST(IntSet, Max)
+{
+ constexpr auto n = std::numeric_limits::max();
+ uint32Set *set = uint32Set_new(1);
+ ASSERT_TRUE(set);
+ EXPECT_FALSE(uint32Set_contains(set, n));
+ EXPECT_TRUE(uint32Set_add(set, n));
+ EXPECT_TRUE(uint32Set_contains(set, n));
+ uint32Set_free(set);
+}
+
+TEST(InsetTest, Negative)
+{
+ constexpr auto n = std::numeric_limits::min();
+ static_assert(n < 0, "n not negative");
+ int64Set *set = int64Set_new(3);
+ ASSERT_TRUE(set);
+ EXPECT_FALSE(int64Set_contains(set, n));
+ EXPECT_TRUE(int64Set_add(set, n));
+ EXPECT_TRUE(int64Set_contains(set, n));
+ int64Set_free(set);
+}
diff --git a/test/netutils/test_location_store.py b/test/netutils/test_location_store.py
index a7f117255f..f3e83989be 100644
--- a/test/netutils/test_location_store.py
+++ b/test/netutils/test_location_store.py
@@ -1,4 +1,5 @@
# Tests for _speedups.LocationStore and NetUtils._LocationStore
+import os
import typing
import unittest
import warnings
@@ -7,6 +8,8 @@ from NetUtils import LocationStore, _LocationStore
State = typing.Dict[typing.Tuple[int, int], typing.Set[int]]
RawLocations = typing.Dict[int, typing.Dict[int, typing.Tuple[int, int, int]]]
+ci = bool(os.environ.get("CI")) # always set in GitHub actions
+
sample_data: RawLocations = {
1: {
11: (21, 2, 7),
@@ -24,6 +27,9 @@ sample_data: RawLocations = {
3: {
9: (99, 4, 0),
},
+ 5: {
+ 9: (99, 5, 0),
+ }
}
empty_state: State = {
@@ -45,14 +51,14 @@ class Base:
store: typing.Union[LocationStore, _LocationStore]
def test_len(self) -> None:
- self.assertEqual(len(self.store), 4)
+ self.assertEqual(len(self.store), 5)
self.assertEqual(len(self.store[1]), 3)
def test_key_error(self) -> None:
with self.assertRaises(KeyError):
_ = self.store[0]
with self.assertRaises(KeyError):
- _ = self.store[5]
+ _ = self.store[6]
locations = self.store[1] # no Exception
with self.assertRaises(KeyError):
_ = locations[7]
@@ -71,7 +77,7 @@ class Base:
self.assertEqual(self.store[1].get(10, (None, None, None)), (None, None, None))
def test_iter(self) -> None:
- self.assertEqual(sorted(self.store), [1, 2, 3, 4])
+ self.assertEqual(sorted(self.store), [1, 2, 3, 4, 5])
self.assertEqual(len(self.store), len(sample_data))
self.assertEqual(list(self.store[1]), [11, 12, 13])
self.assertEqual(len(self.store[1]), len(sample_data[1]))
@@ -85,13 +91,26 @@ class Base:
self.assertEqual(sorted(self.store[1].items())[0][1], self.store[1][11])
def test_find_item(self) -> None:
+ # empty player set
self.assertEqual(sorted(self.store.find_item(set(), 99)), [])
+ # no such player, single
+ self.assertEqual(sorted(self.store.find_item({6}, 99)), [])
+ # no such player, set
+ self.assertEqual(sorted(self.store.find_item({7, 8, 9}, 99)), [])
+ # no such item
self.assertEqual(sorted(self.store.find_item({3}, 1)), [])
- self.assertEqual(sorted(self.store.find_item({5}, 99)), [])
+ # valid matches
self.assertEqual(sorted(self.store.find_item({3}, 99)),
[(4, 9, 99, 3, 0)])
self.assertEqual(sorted(self.store.find_item({3, 4}, 99)),
[(3, 9, 99, 4, 0), (4, 9, 99, 3, 0)])
+ self.assertEqual(sorted(self.store.find_item({2, 3, 4}, 99)),
+ [(3, 9, 99, 4, 0), (4, 9, 99, 3, 0)])
+ # test hash collision in set
+ self.assertEqual(sorted(self.store.find_item({3, 5}, 99)),
+ [(4, 9, 99, 3, 0), (5, 9, 99, 5, 0)])
+ self.assertEqual(sorted(self.store.find_item(set(range(2048)), 13)),
+ [(1, 13, 13, 1, 0)])
def test_get_for_player(self) -> None:
self.assertEqual(self.store.get_for_player(3), {4: {9}})
@@ -196,18 +215,20 @@ class TestPurePythonLocationStoreConstructor(Base.TestLocationStoreConstructor):
super().setUp()
-@unittest.skipIf(LocationStore is _LocationStore, "_speedups not available")
+@unittest.skipIf(LocationStore is _LocationStore and not ci, "_speedups not available")
class TestSpeedupsLocationStore(Base.TestLocationStore):
"""Run base method tests for cython implementation."""
def setUp(self) -> None:
+ self.assertFalse(LocationStore is _LocationStore, "Failed to load _speedups")
self.store = LocationStore(sample_data)
super().setUp()
-@unittest.skipIf(LocationStore is _LocationStore, "_speedups not available")
+@unittest.skipIf(LocationStore is _LocationStore and not ci, "_speedups not available")
class TestSpeedupsLocationStoreConstructor(Base.TestLocationStoreConstructor):
"""Run base constructor tests and tests the additional constraints for cython implementation."""
def setUp(self) -> None:
+ self.assertFalse(LocationStore is _LocationStore, "Failed to load _speedups")
self.type = LocationStore
super().setUp()
From c108845d1ff26979ddb4a5168faec82b1206292e Mon Sep 17 00:00:00 2001
From: black-sliver <59490463+black-sliver@users.noreply.github.com>
Date: Wed, 12 Jun 2024 18:55:48 +0200
Subject: [PATCH 25/31] CI: more checks in build and rework compression (#3336)
* CI: build: fail fast if setup.py fails on windows
* CI: build: fail for missing uploads, rework compression
Upload-artifact allows setting compression level now.
The change speeds up both upload and extraction.
* CI: match build gz in release
* CI: build: verify worlds all load
* CI: build: generate a game
* Generate: move worlds loaded exception to allow settings to init from worlds
* CI: build: build setup before running tests
---
.github/workflows/build.yml | 60 +++++++++++++++++++++++++++++++----
.github/workflows/release.yml | 2 +-
Generate.py | 8 +++--
3 files changed, 59 insertions(+), 11 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 80aaf70c21..dd88d8d7d7 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -40,6 +40,10 @@ jobs:
run: |
python -m pip install --upgrade pip
python setup.py build_exe --yes
+ if ( $? -eq $false ) {
+ Write-Error "setup.py failed!"
+ exit 1
+ }
$NAME="$(ls build | Select-String -Pattern 'exe')".Split('.',2)[1]
$ZIP_NAME="Archipelago_$NAME.7z"
echo "$NAME -> $ZIP_NAME"
@@ -49,12 +53,6 @@ jobs:
Rename-Item "exe.$NAME" Archipelago
7z a -mx=9 -mhe=on -ms "../dist/$ZIP_NAME" Archipelago
Rename-Item Archipelago "exe.$NAME" # inno_setup.iss expects the original name
- - name: Store 7z
- uses: actions/upload-artifact@v4
- with:
- name: ${{ env.ZIP_NAME }}
- path: dist/${{ env.ZIP_NAME }}
- retention-days: 7 # keep for 7 days, should be enough
- name: Build Setup
run: |
& "${env:ProgramFiles(x86)}\Inno Setup 6\iscc.exe" inno_setup.iss /DNO_SIGNTOOL
@@ -65,11 +63,38 @@ jobs:
$contents = Get-ChildItem -Path setups/*.exe -Force -Recurse
$SETUP_NAME=$contents[0].Name
echo "SETUP_NAME=$SETUP_NAME" >> $Env:GITHUB_ENV
+ - name: Check build loads expected worlds
+ shell: bash
+ run: |
+ cd build/exe*
+ mv Players/Templates/meta.yaml .
+ ls -1 Players/Templates | sort > setup-player-templates.txt
+ rm -R Players/Templates
+ timeout 30 ./ArchipelagoLauncher "Generate Template Options" || true
+ ls -1 Players/Templates | sort > generated-player-templates.txt
+ cmp setup-player-templates.txt generated-player-templates.txt \
+ || diff setup-player-templates.txt generated-player-templates.txt
+ mv meta.yaml Players/Templates/
+ - name: Test Generate
+ shell: bash
+ run: |
+ cd build/exe*
+ cp Players/Templates/Clique.yaml Players/
+ timeout 30 ./ArchipelagoGenerate
+ - name: Store 7z
+ uses: actions/upload-artifact@v4
+ with:
+ name: ${{ env.ZIP_NAME }}
+ path: dist/${{ env.ZIP_NAME }}
+ compression-level: 0 # .7z is incompressible by zip
+ if-no-files-found: error
+ retention-days: 7 # keep for 7 days, should be enough
- name: Store Setup
uses: actions/upload-artifact@v4
with:
name: ${{ env.SETUP_NAME }}
path: setups/${{ env.SETUP_NAME }}
+ if-no-files-found: error
retention-days: 7 # keep for 7 days, should be enough
build-ubuntu2004:
@@ -110,7 +135,7 @@ jobs:
echo -e "setup.py dist output:\n `ls dist`"
cd dist && export APPIMAGE_NAME="`ls *.AppImage`" && cd ..
export TAR_NAME="${APPIMAGE_NAME%.AppImage}.tar.gz"
- (cd build && DIR_NAME="`ls | grep exe`" && mv "$DIR_NAME" Archipelago && tar -czvf ../dist/$TAR_NAME Archipelago && mv Archipelago "$DIR_NAME")
+ (cd build && DIR_NAME="`ls | grep exe`" && mv "$DIR_NAME" Archipelago && tar -cv Archipelago | gzip -8 > ../dist/$TAR_NAME && mv Archipelago "$DIR_NAME")
echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV
echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV
# - copy code above to release.yml -
@@ -118,15 +143,36 @@ jobs:
run: |
source venv/bin/activate
python setup.py build_exe --yes
+ - name: Check build loads expected worlds
+ shell: bash
+ run: |
+ cd build/exe*
+ mv Players/Templates/meta.yaml .
+ ls -1 Players/Templates | sort > setup-player-templates.txt
+ rm -R Players/Templates
+ timeout 30 ./ArchipelagoLauncher "Generate Template Options" || true
+ ls -1 Players/Templates | sort > generated-player-templates.txt
+ cmp setup-player-templates.txt generated-player-templates.txt \
+ || diff setup-player-templates.txt generated-player-templates.txt
+ mv meta.yaml Players/Templates/
+ - name: Test Generate
+ shell: bash
+ run: |
+ cd build/exe*
+ cp Players/Templates/Clique.yaml Players/
+ timeout 30 ./ArchipelagoGenerate
- name: Store AppImage
uses: actions/upload-artifact@v4
with:
name: ${{ env.APPIMAGE_NAME }}
path: dist/${{ env.APPIMAGE_NAME }}
+ if-no-files-found: error
retention-days: 7
- name: Store .tar.gz
uses: actions/upload-artifact@v4
with:
name: ${{ env.TAR_NAME }}
path: dist/${{ env.TAR_NAME }}
+ compression-level: 0 # .gz is incompressible by zip
+ if-no-files-found: error
retention-days: 7
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 2d7f1253b7..3f8651d408 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -69,7 +69,7 @@ jobs:
echo -e "setup.py dist output:\n `ls dist`"
cd dist && export APPIMAGE_NAME="`ls *.AppImage`" && cd ..
export TAR_NAME="${APPIMAGE_NAME%.AppImage}.tar.gz"
- (cd build && DIR_NAME="`ls | grep exe`" && mv "$DIR_NAME" Archipelago && tar -czvf ../dist/$TAR_NAME Archipelago && mv Archipelago "$DIR_NAME")
+ (cd build && DIR_NAME="`ls | grep exe`" && mv "$DIR_NAME" Archipelago && tar -cv Archipelago | gzip -8 > ../dist/$TAR_NAME && mv Archipelago "$DIR_NAME")
echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV
echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV
# - code above copied from build.yml -
diff --git a/Generate.py b/Generate.py
index 0cef081120..1fbb9e76a4 100644
--- a/Generate.py
+++ b/Generate.py
@@ -66,13 +66,15 @@ def get_seed_name(random_source) -> str:
def main(args=None):
+ # __name__ == "__main__" check so unittests that already imported worlds don't trip this.
+ if __name__ == "__main__" and "worlds" in sys.modules:
+ raise Exception("Worlds system should not be loaded before logging init.")
+
if not args:
args = mystery_argparse()
seed = get_seed(args.seed)
- # __name__ == "__main__" check so unittests that already imported worlds don't trip this.
- if __name__ == "__main__" and "worlds" in sys.modules:
- raise Exception("Worlds system should not be loaded before logging init.")
+
Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level)
random.seed(seed)
seed_name = get_seed_name(random)
From da34800f43a445470820778e3a7e0c5be5b7d4d3 Mon Sep 17 00:00:00 2001
From: JoshuaEagles
Date: Thu, 13 Jun 2024 00:53:01 -0400
Subject: [PATCH 26/31] Fix Incorrect Link Syntax in SA2B Linux Setup (#3524)
---
worlds/sa2b/docs/setup_en.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/worlds/sa2b/docs/setup_en.md b/worlds/sa2b/docs/setup_en.md
index 354ef4bbe9..f32001a678 100644
--- a/worlds/sa2b/docs/setup_en.md
+++ b/worlds/sa2b/docs/setup_en.md
@@ -48,7 +48,7 @@
7. Install protontricks, on the Steam Deck this can be done via the Discover store, on other distros instructions vary, [see its github page](https://github.com/Matoking/protontricks).
-8. Download the [.NET 7 Desktop Runtime for x64 Windows](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-7.0.17-windows-x64-installer}. If this link does not work, the download can be found on [this page](https://dotnet.microsoft.com/en-us/download/dotnet/7.0).
+8. Download the [.NET 7 Desktop Runtime for x64 Windows](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-7.0.17-windows-x64-installer). If this link does not work, the download can be found on [this page](https://dotnet.microsoft.com/en-us/download/dotnet/7.0).
9. Right click the .NET 7 Desktop Runtime exe, and assuming protontricks was installed correctly, the option to "Open with Protontricks Launcher" should be available. Click that, and in the popup window that opens, select SAModManager.exe. Follow the prompts after this to install the .NET 7 Desktop Runtime for SAModManager. Once it is done, you should be able to successfully launch SAModManager to steam.
From f6e3113af6805c38e1e5c322c5747b89465722f1 Mon Sep 17 00:00:00 2001
From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
Date: Thu, 13 Jun 2024 10:39:16 +0200
Subject: [PATCH 27/31] WebHost: Fix "Add" button for custom option values
causing a weird redirect (#3518)
* WebHost: Fix "Add" button for Progression Balancing causing a weird redirect
This "add" button is part of a form, which causes it to submit the form, because the default type for a button is "submit".
This PR changes the type of the button to "button", which causes it to not submit the form and just execute its normal effect.
(An alternative would be `event.preventDefault()` but that seems less clean to me, but also I'm not a HTML/JS dev)
* There's also multiple.
---
WebHostLib/templates/weightedOptions/macros.html | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/WebHostLib/templates/weightedOptions/macros.html b/WebHostLib/templates/weightedOptions/macros.html
index 55a56e3285..c7a5d4174b 100644
--- a/WebHostLib/templates/weightedOptions/macros.html
+++ b/WebHostLib/templates/weightedOptions/macros.html
@@ -47,7 +47,7 @@
{% endif %}
- Add
+ Add
@@ -72,7 +72,7 @@
This option allows custom values only. Please enter your desired values below.
- Add
+ Add
@@ -89,7 +89,7 @@
Custom values are also allowed for this option. To create one, enter it into the input box below.
- Add
+ Add
From 2ae51364d9052731ea3fae2b25aa61d5a7e5a8ae Mon Sep 17 00:00:00 2001
From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
Date: Thu, 13 Jun 2024 18:24:56 +0200
Subject: [PATCH 28/31] WebHost: Fix default values that are 2 or more words in
Weighted Options (#3519)
* WeightedOptions: Fix default values that are 2 or more words
* So much simpler
---
WebHostLib/templates/weightedOptions/macros.html | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/WebHostLib/templates/weightedOptions/macros.html b/WebHostLib/templates/weightedOptions/macros.html
index c7a5d4174b..4d9e7ca4d3 100644
--- a/WebHostLib/templates/weightedOptions/macros.html
+++ b/WebHostLib/templates/weightedOptions/macros.html
@@ -19,7 +19,7 @@
{% for id, name in option.name_lookup.items() %}
{% if name != 'random' %}
{% if option.default != 'random' %}
- {{ RangeRow(option_name, option, option.get_option_name(id), name, False, name if option.get_option_name(option.default)|lower == name else None) }}
+ {{ RangeRow(option_name, option, option.get_option_name(id), name, False, name if option.default == id else None) }}
{% else %}
{{ RangeRow(option_name, option, option.get_option_name(id), name) }}
{% endif %}
@@ -97,7 +97,7 @@
{% for id, name in option.name_lookup.items() %}
{% if name != 'random' %}
{% if option.default != 'random' %}
- {{ RangeRow(option_name, option, option.get_option_name(id), name, False, name if option.get_option_name(option.default)|lower == name else None) }}
+ {{ RangeRow(option_name, option, option.get_option_name(id), name, False, name if option.default == id else None) }}
{% else %}
{{ RangeRow(option_name, option, option.get_option_name(id), name) }}
{% endif %}
From 533395d336073081bcf8a7cba461f7529a49d8ff Mon Sep 17 00:00:00 2001
From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
Date: Thu, 13 Jun 2024 23:29:39 +0200
Subject: [PATCH 29/31] WebHost: Fix Named Range displays on Player Options
page (#3521)
* Player Options: Fix Named Range displays
* Also add validation to the NamedRange class itself
* Don't break Stardew
* Comment
* Do replace first so title works correctly
* Bring change to Weighted Options as well
---
Options.py | 6 ++++++
WebHostLib/templates/playerOptions/macros.html | 4 ++--
WebHostLib/templates/weightedOptions/macros.html | 2 +-
3 files changed, 9 insertions(+), 3 deletions(-)
diff --git a/Options.py b/Options.py
index 40a6996d32..2d3ef99d64 100644
--- a/Options.py
+++ b/Options.py
@@ -735,6 +735,12 @@ class NamedRange(Range):
elif value > self.range_end and value not in self.special_range_names.values():
raise Exception(f"{value} is higher than maximum {self.range_end} for option {self.__class__.__name__} " +
f"and is also not one of the supported named special values: {self.special_range_names}")
+
+ # See docstring
+ for key in self.special_range_names:
+ if key != key.lower():
+ raise Exception(f"{self.__class__.__name__} has an invalid special_range_names key: {key}. "
+ f"NamedRange keys must use only lowercase letters, and ideally should be snake_case.")
self.value = value
@classmethod
diff --git a/WebHostLib/templates/playerOptions/macros.html b/WebHostLib/templates/playerOptions/macros.html
index b34ac79a02..2187ffe913 100644
--- a/WebHostLib/templates/playerOptions/macros.html
+++ b/WebHostLib/templates/playerOptions/macros.html
@@ -57,9 +57,9 @@
{% for key, val in option.special_range_names.items() %}
{% if option.default == val %}
- {{ key }} ({{ val }})
+ {{ key|replace("_", " ")|title }} ({{ val }})
{% else %}
- {{ key }} ({{ val }})
+ {{ key|replace("_", " ")|title }} ({{ val }})
{% endif %}
{% endfor %}
Custom
diff --git a/WebHostLib/templates/weightedOptions/macros.html b/WebHostLib/templates/weightedOptions/macros.html
index 4d9e7ca4d3..a1d3196971 100644
--- a/WebHostLib/templates/weightedOptions/macros.html
+++ b/WebHostLib/templates/weightedOptions/macros.html
@@ -41,7 +41,7 @@
The following values have special meanings, and may fall outside the normal range.
{% for name, value in option.special_range_names.items() %}
- {{ value }}: {{ name }}
+ {{ value }}: {{ name|replace("_", " ")|title }}
{% endfor %}
{% endif %}
From e9ad7cb7975475f9395b1c091d13dab2c614cc93 Mon Sep 17 00:00:00 2001
From: Fabian Dill
Date: Thu, 13 Jun 2024 23:37:52 +0200
Subject: [PATCH 30/31] WebHost: fix option doc indent (#3513)
* WebHost: fix option doc indent
* Update macros.html
---
WebHostLib/templates/playerOptions/macros.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/WebHostLib/templates/playerOptions/macros.html b/WebHostLib/templates/playerOptions/macros.html
index 2187ffe913..76cf076994 100644
--- a/WebHostLib/templates/playerOptions/macros.html
+++ b/WebHostLib/templates/playerOptions/macros.html
@@ -196,7 +196,7 @@
{% macro OptionTitle(option_name, option) %}
{{ option.display_name|default(option_name) }}:
- (?)
+ (?)
{% endmacro %}
From 1fe3d842c8ec4df70dd0d7bd3b9d0fbae7a33e0f Mon Sep 17 00:00:00 2001
From: black-sliver <59490463+black-sliver@users.noreply.github.com>
Date: Fri, 14 Jun 2024 08:47:47 +0200
Subject: [PATCH 31/31] CI: Install specific inno version (#3526)
* CI: Install specific inno version
* great mobile dev experience
* maybe this
* really don't enjoy PS
* Anothet attempt
* maybe fix log
* slowly going mad
* fml
* allow downgrade
---
.github/workflows/build.yml | 1 +
1 file changed, 1 insertion(+)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index dd88d8d7d7..23c463fb94 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -36,6 +36,7 @@ jobs:
run: |
Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip
Expand-Archive -Path enemizer.zip -DestinationPath EnemizerCLI -Force
+ choco install innosetup --version=6.2.2 --allow-downgrade
- name: Build
run: |
python -m pip install --upgrade pip