Skip to content

Commit 3847db5

Browse files
committed
Merge branch 'main' into snes-reader
2 parents b87c992 + 4b03061 commit 3847db5

File tree

292 files changed

+12724
-6286
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

292 files changed

+12724
-6286
lines changed

.github/workflows/build.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,22 @@ on:
99
- 'setup.py'
1010
- 'requirements.txt'
1111
- '*.iss'
12+
- 'worlds/*/archipelago.json'
1213
pull_request:
1314
paths:
1415
- '.github/workflows/build.yml'
1516
- 'setup.py'
1617
- 'requirements.txt'
1718
- '*.iss'
19+
- 'worlds/*/archipelago.json'
1820
workflow_dispatch:
1921

2022
env:
2123
ENEMIZER_VERSION: 7.1
2224
# NOTE: since appimage/appimagetool and appimage/type2-runtime does not have tags anymore,
2325
# we check the sha256 and require manual intervention if it was updated.
2426
APPIMAGETOOL_VERSION: continuous
25-
APPIMAGETOOL_X86_64_HASH: '29348a20b80827cd261c28e95172ff828b69d43d4e4e18e3fd069e2c8693c94e'
27+
APPIMAGETOOL_X86_64_HASH: '9493a6b253a01f84acb9c624c38810ecfa11d99daa829b952b0bff43113080f9'
2628
APPIMAGE_RUNTIME_VERSION: continuous
2729
APPIMAGE_RUNTIME_X86_64_HASH: 'e70ffa9b69b211574d0917adc482dd66f25a0083427b5945783965d55b0b0a8b'
2830

.github/workflows/docker.yml

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
name: Build and Publish Docker Images
2+
3+
on:
4+
push:
5+
paths:
6+
- "**"
7+
- "!docs/**"
8+
- "!deploy/**"
9+
- "!setup.py"
10+
- "!.gitignore"
11+
- "!.github/workflows/**"
12+
- ".github/workflows/docker.yml"
13+
branches:
14+
- "*"
15+
tags:
16+
- "v?[0-9]+.[0-9]+.[0-9]*"
17+
workflow_dispatch:
18+
19+
env:
20+
REGISTRY: ghcr.io
21+
22+
jobs:
23+
prepare:
24+
runs-on: ubuntu-latest
25+
outputs:
26+
image-name: ${{ steps.image.outputs.name }}
27+
tags: ${{ steps.meta.outputs.tags }}
28+
labels: ${{ steps.meta.outputs.labels }}
29+
package-name: ${{ steps.package.outputs.name }}
30+
steps:
31+
- name: Checkout repository
32+
uses: actions/checkout@v4
33+
34+
- name: Set lowercase image name
35+
id: image
36+
run: |
37+
echo "name=${GITHUB_REPOSITORY,,}" >> $GITHUB_OUTPUT
38+
39+
- name: Set package name
40+
id: package
41+
run: |
42+
echo "name=$(basename ${GITHUB_REPOSITORY,,})" >> $GITHUB_OUTPUT
43+
44+
- name: Extract metadata
45+
id: meta
46+
uses: docker/metadata-action@v5
47+
with:
48+
images: ${{ env.REGISTRY }}/${{ steps.image.outputs.name }}
49+
tags: |
50+
type=ref,event=branch,enable={{is_not_default_branch}}
51+
type=semver,pattern={{version}}
52+
type=semver,pattern={{major}}.{{minor}}
53+
type=raw,value=nightly,enable={{is_default_branch}}
54+
55+
- name: Compute final tags
56+
id: final-tags
57+
run: |
58+
readarray -t tags <<< "${{ steps.meta.outputs.tags }}"
59+
60+
if [[ "${{ github.ref_type }}" == "tag" ]]; then
61+
tag="${{ github.ref_name }}"
62+
if [[ "$tag" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
63+
full_latest="${{ env.REGISTRY }}/${{ steps.image.outputs.name }}:latest"
64+
# Check if latest is already in tags to avoid duplicates
65+
if ! printf '%s\n' "${tags[@]}" | grep -q "^$full_latest$"; then
66+
tags+=("$full_latest")
67+
fi
68+
fi
69+
fi
70+
71+
# Set multiline output
72+
echo "tags<<EOF" >> $GITHUB_OUTPUT
73+
printf '%s\n' "${tags[@]}" >> $GITHUB_OUTPUT
74+
echo "EOF" >> $GITHUB_OUTPUT
75+
76+
build:
77+
needs: prepare
78+
runs-on: ${{ matrix.runner }}
79+
permissions:
80+
contents: read
81+
packages: write
82+
strategy:
83+
matrix:
84+
include:
85+
- platform: amd64
86+
runner: ubuntu-latest
87+
suffix: amd64
88+
cache-scope: amd64
89+
- platform: arm64
90+
runner: ubuntu-24.04-arm
91+
suffix: arm64
92+
cache-scope: arm64
93+
steps:
94+
- name: Checkout repository
95+
uses: actions/checkout@v4
96+
97+
- name: Set up Docker Buildx
98+
uses: docker/setup-buildx-action@v3
99+
100+
- name: Log in to GitHub Container Registry
101+
uses: docker/login-action@v3
102+
with:
103+
registry: ${{ env.REGISTRY }}
104+
username: ${{ github.actor }}
105+
password: ${{ secrets.GITHUB_TOKEN }}
106+
107+
- name: Compute suffixed tags
108+
id: tags
109+
run: |
110+
readarray -t tags <<< "${{ needs.prepare.outputs.tags }}"
111+
suffixed=()
112+
for t in "${tags[@]}"; do
113+
suffixed+=("$t-${{ matrix.suffix }}")
114+
done
115+
echo "tags=$(IFS=','; echo "${suffixed[*]}")" >> $GITHUB_OUTPUT
116+
117+
- name: Build and push Docker image
118+
uses: docker/build-push-action@v5
119+
with:
120+
context: .
121+
file: ./Dockerfile
122+
platforms: linux/${{ matrix.platform }}
123+
push: true
124+
tags: ${{ steps.tags.outputs.tags }}
125+
labels: ${{ needs.prepare.outputs.labels }}
126+
cache-from: type=gha,scope=${{ matrix.cache-scope }}
127+
cache-to: type=gha,mode=max,scope=${{ matrix.cache-scope }}
128+
provenance: false
129+
130+
manifest:
131+
needs: [prepare, build]
132+
runs-on: ubuntu-latest
133+
permissions:
134+
contents: read
135+
packages: write
136+
steps:
137+
- name: Log in to GitHub Container Registry
138+
uses: docker/login-action@v3
139+
with:
140+
registry: ${{ env.REGISTRY }}
141+
username: ${{ github.actor }}
142+
password: ${{ secrets.GITHUB_TOKEN }}
143+
144+
- name: Create and push multi-arch manifest
145+
run: |
146+
readarray -t tag_array <<< "${{ needs.prepare.outputs.tags }}"
147+
148+
for tag in "${tag_array[@]}"; do
149+
docker manifest create "$tag" \
150+
"$tag-amd64" \
151+
"$tag-arm64"
152+
153+
docker manifest push "$tag"
154+
done

.github/workflows/label-pull-requests.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ env:
1212
jobs:
1313
labeler:
1414
name: 'Apply content-based labels'
15-
if: github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'synchronize'
1615
runs-on: ubuntu-latest
1716
steps:
1817
- uses: actions/labeler@v5

.github/workflows/release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ name: Release
55
on:
66
push:
77
tags:
8-
- '*.*.*'
8+
- 'v?[0-9]+.[0-9]+.[0-9]*'
99

1010
env:
1111
ENEMIZER_VERSION: 7.1
1212
# NOTE: since appimage/appimagetool and appimage/type2-runtime does not have tags anymore,
1313
# we check the sha256 and require manual intervention if it was updated.
1414
APPIMAGETOOL_VERSION: continuous
15-
APPIMAGETOOL_X86_64_HASH: '29348a20b80827cd261c28e95172ff828b69d43d4e4e18e3fd069e2c8693c94e'
15+
APPIMAGETOOL_X86_64_HASH: '9493a6b253a01f84acb9c624c38810ecfa11d99daa829b952b0bff43113080f9'
1616
APPIMAGE_RUNTIME_VERSION: continuous
1717
APPIMAGE_RUNTIME_X86_64_HASH: 'e70ffa9b69b211574d0917adc482dd66f25a0083427b5945783965d55b0b0a8b'
1818

.run/Build APWorld.run.xml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<component name="ProjectRunConfigurationManager">
2+
<configuration default="false" name="Build APWorld" type="PythonConfigurationType" factoryName="Python">
3+
<module name="Archipelago" />
4+
<option name="ENV_FILES" value="" />
5+
<option name="INTERPRETER_OPTIONS" value="" />
6+
<option name="PARENT_ENVS" value="true" />
7+
<envs>
8+
<env name="PYTHONUNBUFFERED" value="1" />
9+
</envs>
10+
<option name="SDK_HOME" value="" />
11+
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/" />
12+
<option name="IS_MODULE_SDK" value="true" />
13+
<option name="ADD_CONTENT_ROOTS" value="true" />
14+
<option name="ADD_SOURCE_ROOTS" value="true" />
15+
<option name="SCRIPT_NAME" value="$ContentRoot$/Launcher.py" />
16+
<option name="PARAMETERS" value="\&quot;Build APWorlds\&quot;" />
17+
<option name="SHOW_COMMAND_LINE" value="false" />
18+
<option name="EMULATE_TERMINAL" value="false" />
19+
<option name="MODULE_MODE" value="false" />
20+
<option name="REDIRECT_INPUT" value="false" />
21+
<option name="INPUT_FILE" value="" />
22+
<method v="2" />
23+
</configuration>
24+
</component>

BaseClasses.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ def set_item_links(self):
261261
"local_items": set(item_link.get("local_items", [])),
262262
"non_local_items": set(item_link.get("non_local_items", [])),
263263
"link_replacement": replacement_prio.index(item_link["link_replacement"]),
264+
"skip_if_solo": item_link.get("skip_if_solo", False),
264265
}
265266

266267
for _name, item_link in item_links.items():
@@ -284,6 +285,8 @@ def set_item_links(self):
284285

285286
for group_name, item_link in item_links.items():
286287
game = item_link["game"]
288+
if item_link["skip_if_solo"] and len(item_link["players"]) == 1:
289+
continue
287290
group_id, group = self.add_group(group_name, game, set(item_link["players"]))
288291

289292
group["item_pool"] = item_link["item_pool"]
@@ -1343,8 +1346,7 @@ def get_connecting_entrance(self, is_main_entrance: Callable[[Entrance], bool])
13431346
for entrance in self.entrances: # BFS might be better here, trying DFS for now.
13441347
return entrance.parent_region.get_connecting_entrance(is_main_entrance)
13451348

1346-
def add_locations(self, locations: Dict[str, Optional[int]],
1347-
location_type: Optional[type[Location]] = None) -> None:
1349+
def add_locations(self, locations: Mapping[str, int | None], location_type: type[Location] | None = None) -> None:
13481350
"""
13491351
Adds locations to the Region object, where location_type is your Location class and locations is a dict of
13501352
location names to address.
@@ -1432,16 +1434,16 @@ def create_er_target(self, name: str) -> Entrance:
14321434
entrance.connect(self)
14331435
return entrance
14341436

1435-
def add_exits(self, exits: Union[Iterable[str], Dict[str, Optional[str]]],
1436-
rules: Dict[str, Callable[[CollectionState], bool]] = None) -> List[Entrance]:
1437+
def add_exits(self, exits: Iterable[str] | Mapping[str, str | None],
1438+
rules: Mapping[str, Callable[[CollectionState], bool]] | None = None) -> List[Entrance]:
14371439
"""
14381440
Connects current region to regions in exit dictionary. Passed region names must exist first.
14391441
14401442
:param exits: exits from the region. format is {"connecting_region": "exit_name"}. if a non dict is provided,
14411443
created entrances will be named "self.name -> connecting_region"
14421444
:param rules: rules for the exits from this region. format is {"connecting_region": rule}
14431445
"""
1444-
if not isinstance(exits, Dict):
1446+
if not isinstance(exits, Mapping):
14451447
exits = dict.fromkeys(exits)
14461448
return [
14471449
self.connect(
@@ -1855,6 +1857,9 @@ def write_option(option_key: str, option_obj: Options.AssembleOptions) -> None:
18551857
Utils.__version__, self.multiworld.seed))
18561858
outfile.write('Filling Algorithm: %s\n' % self.multiworld.algorithm)
18571859
outfile.write('Players: %d\n' % self.multiworld.players)
1860+
if self.multiworld.players > 1:
1861+
loc_count = len([loc for loc in self.multiworld.get_locations() if not loc.is_event])
1862+
outfile.write('Total Location Count: %d\n' % loc_count)
18581863
outfile.write(f'Plando Options: {self.multiworld.plando_options}\n')
18591864
AutoWorld.call_stage(self.multiworld, "write_spoiler_header", outfile)
18601865

@@ -1863,6 +1868,9 @@ def write_option(option_key: str, option_obj: Options.AssembleOptions) -> None:
18631868
outfile.write('\nPlayer %d: %s\n' % (player, self.multiworld.get_player_name(player)))
18641869
outfile.write('Game: %s\n' % self.multiworld.game[player])
18651870

1871+
loc_count = len([loc for loc in self.multiworld.get_locations(player) if not loc.is_event])
1872+
outfile.write('Location Count: %d\n' % loc_count)
1873+
18661874
for f_option, option in self.multiworld.worlds[player].options_dataclass.type_hints.items():
18671875
write_option(f_option, option)
18681876

CommonClient.py

Lines changed: 11 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -99,17 +99,6 @@ def _cmd_received(self) -> bool:
9999
self.ctx.on_print_json({"data": parts, "cmd": "PrintJSON"})
100100
return True
101101

102-
def get_current_datapackage(self) -> dict[str, typing.Any]:
103-
"""
104-
Return datapackage for current game if known.
105-
106-
:return: The datapackage for the currently registered game. If not found, an empty dictionary will be returned.
107-
"""
108-
if not self.ctx.game:
109-
return {}
110-
checksum = self.ctx.checksums[self.ctx.game]
111-
return Utils.load_data_package_for_checksum(self.ctx.game, checksum)
112-
113102
def _cmd_missing(self, filter_text = "") -> bool:
114103
"""List all missing location checks, from your local game state.
115104
Can be given text, which will be used as filter."""
@@ -119,8 +108,8 @@ def _cmd_missing(self, filter_text = "") -> bool:
119108
count = 0
120109
checked_count = 0
121110

122-
lookup = self.get_current_datapackage().get("location_name_to_id", {})
123-
for location, location_id in lookup.items():
111+
lookup = self.ctx.location_names[self.ctx.game]
112+
for location_id, location in lookup.items():
124113
if filter_text and filter_text not in location:
125114
continue
126115
if location_id < 0:
@@ -141,11 +130,10 @@ def _cmd_missing(self, filter_text = "") -> bool:
141130
self.output("No missing location checks found.")
142131
return True
143132

144-
def output_datapackage_part(self, key: str, name: str) -> bool:
133+
def output_datapackage_part(self, name: typing.Literal["Item Names", "Location Names"]) -> bool:
145134
"""
146135
Helper to digest a specific section of this game's datapackage.
147136
148-
:param key: The dictionary key in the datapackage.
149137
:param name: Printed to the user as context for the part.
150138
151139
:return: Whether the process was successful.
@@ -154,23 +142,20 @@ def output_datapackage_part(self, key: str, name: str) -> bool:
154142
self.output(f"No game set, cannot determine {name}.")
155143
return False
156144

157-
lookup = self.get_current_datapackage().get(key)
158-
if lookup is None:
159-
self.output("datapackage not yet loaded, try again")
160-
return False
161-
145+
lookup = self.ctx.item_names if name == "Item Names" else self.ctx.location_names
146+
lookup = lookup[self.ctx.game]
162147
self.output(f"{name} for {self.ctx.game}")
163-
for key in lookup:
164-
self.output(key)
148+
for name in lookup.values():
149+
self.output(name)
165150
return True
166151

167152
def _cmd_items(self) -> bool:
168153
"""List all item names for the currently running game."""
169-
return self.output_datapackage_part("item_name_to_id", "Item Names")
154+
return self.output_datapackage_part("Item Names")
170155

171156
def _cmd_locations(self) -> bool:
172157
"""List all location names for the currently running game."""
173-
return self.output_datapackage_part("location_name_to_id", "Location Names")
158+
return self.output_datapackage_part("Location Names")
174159

175160
def output_group_part(self, group_key: typing.Literal["item_name_groups", "location_name_groups"],
176161
filter_key: str,
@@ -871,9 +856,9 @@ async def server_loop(ctx: CommonContext, address: typing.Optional[str] = None)
871856

872857
server_url = urllib.parse.urlparse(address)
873858
if server_url.username:
874-
ctx.username = server_url.username
859+
ctx.username = urllib.parse.unquote(server_url.username)
875860
if server_url.password:
876-
ctx.password = server_url.password
861+
ctx.password = urllib.parse.unquote(server_url.password)
877862

878863
def reconnect_hint() -> str:
879864
return ", type /connect to reconnect" if ctx.server_address else ""

Fill.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,10 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
129129
for i, location in enumerate(placements))
130130
for (i, location, unsafe) in swap_attempts:
131131
placed_item = location.item
132+
if item_to_place == placed_item:
133+
# The number of allowed swaps is limited, so do not allow a swap of an item with a copy of
134+
# itself.
135+
continue
132136
# Unplaceable items can sometimes be swapped infinitely. Limit the
133137
# number of times we will swap an individual item to prevent this
134138
swap_count = swapped_items[placed_item.player, placed_item.name, unsafe]

0 commit comments

Comments
 (0)