Compare commits

...

73 Commits
0.3.7 ... 0.3.8

Author SHA1 Message Date
Fabian Dill
ea2175cb8a MultiServer: load old forfeit_mode if release_mode not present 2023-01-30 00:54:57 +01:00
Fabian Dill
11873e059a Setup: don't use dependency before it's installed 2023-01-29 22:36:04 +01:00
Fabian Dill
6c1023a88c Subnautica: fix swim_rule considers items property use (#1419) 2023-01-29 22:12:39 +01:00
The T
0be0732a2b WebHost: FAQ: change "seeds" to "a world" where world is the right term.
Berserker has frequently corrected, that each player's game is a single world, inside a larger seed; not that each player's game is a seed.
2023-01-29 22:11:53 +01:00
lordlou
c9aa283711 SMZ3: chest game fix (#1417)
Fixed DW Chest Game always sending checks for the 2 chests. The checks sent were the proper "Chest Game" location for the first time the player would open the second chest but all other times, it would send either the last check that was done or default to sending location 0x00 which is SM "Power Bomb (Crateria surface)".
2023-01-28 01:51:19 +01:00
Fabian Dill
cf2204a861 Factorio: add option "Tech Cost Distribution" (#1404)
* Factorio: add option "Tech Cost Distribution"

* TextClient: None out game on disconnect

* TextClient: disconnect is async
2023-01-27 15:38:12 -08:00
Fabian Dill
dfdcad28e5 TextcVient: game reset (#1416)
* TextClient: None out game on disconnect
2023-01-28 00:32:48 +01:00
Fabian Dill
ab4324c901 Factorio: add option "ramping tech cost" (#1403)
* Factorio: add option "ramping tech cost"

* Factorio: fix missing s

* Factorio: add display_name to ranmping tech costs
2023-01-27 15:30:05 -08:00
Fabian Dill
1e251dcdc0 Setup: new cx-Freeze just dropped 2023-01-27 20:06:56 +01:00
espeon65536
9c1f7bfea9 oot: remove special NL exceptions in entrance randomization
turns out they were causing lots of issues
2023-01-26 21:24:27 +01:00
KonoTyran
5393563700 MultiServer: Data Storage Additions #1411
adds 3 new operations to datastorage that allows adding and removing of elements from list and dicts.
2023-01-25 06:14:46 +01:00
toasterparty
28576f2b0d OC2: decrease default difficulty (#1413) 2023-01-25 01:04:13 +01:00
Fabian Dill
ba519fecd0 Setup: update some stuff to 6.14.0 cx-Freeze (#1412)
* Setup: update some stuff to 6.14.0 cx-Freeze

* Fix BuildCommand and replace include_files by cutom step

* setup.py: bit more cleanup for extra_libs

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-01-25 00:20:26 +01:00
espeon65536
86fb450ecc Core: recache all locations before locality rules
Some worlds would not trigger a recache, causing locations to be missed when setting locality rules.
2023-01-24 06:14:31 +01:00
SonicRPika
920240cb6f Pokemon Red and Blue: Fix to having all traps disabled (#1408) 2023-01-24 04:34:45 +01:00
Fabian Dill
53dd0d5a7d SC2: verify downloaded data is a zipfile 2023-01-24 03:54:23 +01:00
Fabian Dill
807f544b26 SC2: use warning log level for potentially broken map files 2023-01-24 03:45:43 +01:00
SoldierofOrder
1d1693df62 SC2Client: Changes to /download_data and feedback (#1347)
* SC2Client: Added feedback for users who have map files, but no version #.

* SC2Client: Fixed a missing space.

* SC2Client: /download_data now always forces a download.
2023-01-24 03:44:12 +01:00
Fabian Dill
51574959ec Setup: add moduleupdater prompt to setup.py 2023-01-24 03:42:55 +01:00
Fabian Dill
04f726aef2 kvui: always display all tab headers (#1399) 2023-01-24 03:42:34 +01:00
CaitSith2
8a4298e504 ALttP: Fix hint tile hints being potentially useless with item links. (#1400)
* ALttP: Fix hint tile hints being potentially useless with item links.

* use the set returned from world.get_player_groups(player)

* Move the group resolving to BaseClasses. Fix silver arrow hints as well.
2023-01-24 03:42:13 +01:00
Fabian Dill
e7f8f40464 Factorio: fix automation-level tech costs before automation (#1402)
* Factorio: fix automation-level tech costs before automation

* Factorio: remove double-rolling of science cost
2023-01-24 03:36:50 +01:00
Fabian Dill
847582ff5f Server: fix release_mode (#1407)
* Server: fix release_mode

* Core: actually rename forfeit to release across the program
2023-01-24 03:36:27 +01:00
recklesscoder
1a44f5cf1c CommonClient: Fix address pre-selection (#1406) 2023-01-23 04:59:51 +01:00
Fabian Dill
032bc75070 Core: demote logfile deletion loglevel to debug 2023-01-23 02:23:16 +01:00
Alchav
fb47483212 Pokémon R/B: Fix trainersanity location name 2023-01-23 00:38:38 +01:00
Alchav
d185df3972 Pokémon R/B: Use local random object when randomizing trainer parties in generate_output 2023-01-23 00:38:38 +01:00
lordlou
941dcb60e5 SM: fixed flawed and limited comeback check (#1398)
The issue at hand is fixing impossible seeds generated by a lack of properly checking if the player can come back to its previous region after reaching for a new location, as reported here: https://discord.com/channels/731205301247803413/1050529825212874763/1050529825212874763

The previous attempt at checking for comeback was only done against "Landing Site" and the custom start region which is a partial solution at best. For exemple, generating a single player plando seed with a custom starting location at "red_brinstar_elevator" with a forced red door at "RedTowerElevatorBottomLeft" and 2 Missiles set at "Morphing Ball" and "Energy Tank, Brinstar Ceiling" would generate an impossible seed where the player is expected to go through the green door "LandingSiteRight" with no Supers to go to the only possible next location "Power Bomb (red Brinstar spike room)". This is because the comeback check would pass because it would consider coming back to "Landing Site" enough.

The proposed solution is keeping a record of the last accessed region when collecting items. It would then be used as the source of the comeback check with the destination being the new location. This check had to be moved from can_fill() to can_reach() because the maximum_exploration_state of the AP filler only use can_reach().

Its still not perfect because collect() can be called in batch for many items at a time so the last accessed region will be set as the last collected item and will be used for the next comeback checks.

This was tested a bit with the given exemple above (its now failing generation) and by generating some 8 SM players seed with many door color rando, area rando and boss rando enabled.
2023-01-23 00:36:18 +01:00
Fabian Dill
25756831b7 Core: mark version as 0.3.8 2023-01-21 17:30:30 +01:00
Fabian Dill
9add1495d5 SSL support (#1340) 2023-01-21 17:29:27 +01:00
SonicRPika
34dba007dc Pokemon Red and Blue: Updates to trap weights and tracker support (#1395)
* Added cerulean_cave_condition to fill_slot_data

Added `cerulean_cave_condition` to the `fill_slot_data` function, for a poptracker feature being worked on as it was missing

* Added the potential for any traps to be disabled

Adding the ability to disable any kind of trap, for example if you want any status trap except being Poisoned. Will add a contingency to not try and roll a trap if they are all set to disabled.

* Added contingency to if all traps are disabled

Added a contingency to creating items such that it doesn't try to create a trap if all the traps are disabled

* Updated variable name

Edited name of variable to follow PEP 8 variable naming conventions
2023-01-20 18:49:12 +01:00
Fabian Dill
02d3eef565 Core: convert mixture of Plando Options and Settings into just Options 2023-01-19 17:20:23 +01:00
Fabian Dill
c839a76fe7 LttP: allow hinting and tracking "Take Any" type shops (#1392)
* LttP: allow hinting and tracking "Take Any" type shops
fix broken behaviour since bow/cave split


Co-authored-by: CaitSith2 <d_good@caitsith2.com>
2023-01-19 16:17:43 +01:00
alwaysintreble
29e1c3dcf4 LTTP: fix open pyramid for real this time (#1393) 2023-01-19 16:17:16 +01:00
recklesscoder
f6616da5a9 Docs/Subnautica: Updated console instructions, misc clarifications (#1394) 2023-01-19 16:09:08 +01:00
Fabian Dill
8678e02d54 Subnautica: correct doc string placement for early seaglide 2023-01-19 00:03:26 +01:00
Fabian Dill
2f37bedc92 Tests: ensure item name groups do not collide with item names (#1074) 2023-01-18 15:45:48 +01:00
Alchav
91fdfe3e17 Pokémon R/B: Add inheritance to "Completely Random" option as well 2023-01-18 04:26:40 +01:00
Alchav
a41b0051a6 Pokémon R/B: Fix TM/HM compatibility bug 2023-01-18 04:26:40 +01:00
alwaysintreble
b8abe9f980 Tests: add a test to check for dupe locations (#1378) 2023-01-15 20:18:32 +01:00
alwaysintreble
dd3ae5ecbd core: write the plando settings to the spoiler log (#1248)
Co-authored-by: Zach Parks <zach@alliware.com>
2023-01-15 18:10:26 +01:00
PoryGone
e96602d31b SA2B: Fix Gate region connections (#1384) 2023-01-15 17:55:36 +01:00
el-u
81d953daa3 alttp: add item rules for prize locations (#1380) 2023-01-14 14:29:54 +01:00
Alchav
bd774a454e Pokémon R/B: Fix Safari Zone Gate bug (#1381) 2023-01-14 04:59:09 +01:00
espeon65536
ca724c92ad oot: force itempool to higher settings if required by heart logic 2023-01-13 23:53:13 +01:00
espeon65536
11eebbbd32 Ocarina of Time: 0.3.7 hotfixes round 2 (#1351)
* oot: repair closed forest + dungeon ER

* oot: finally skip triforce pieces in balancing

* oot: fix mq_dungeons_mode set to mq or count

* oot: force 0.3.7 client
hopefully this makes people update

* oot: temp fix for skip-child-zelda crash
eventually I want to decide on a better fix for this though

* oot: remove skip-child-zelda item inside if tree

* oot: fix classification of some thieves hideout locations in tracker

* oot: fix regional shuffle for hideout keys and ganon boss key

* oot: properly attach hints to dungeon locations

* Fix entrance shuffle flag not being set correctly due to new dungeon shuffle option format
2023-01-12 20:20:49 +01:00
Doug Hoskisson
608794cded ZillionClient: fix manual disconnect (#1266) 2023-01-07 10:27:43 +01:00
eudaimonistic
816de5ff02 Docs: code_of_conduct.md (#1350)
Update to point of contact.
2023-01-07 10:24:41 +01:00
Fabian Dill
0b941e2268 LttP: attempt at preventing ghost location checks (#1355) 2023-01-07 10:22:15 +01:00
beauxq
57713cda50 Zillion: minor terrain logic update
standing on a moving walkway requires 2 columns of standing space
2023-01-07 10:12:45 +01:00
Jarno
f56cdd6ec3 Sudoku: Hints will no-longer duplicate (#1371) 2023-01-07 10:10:20 +01:00
t3hf1gm3nt
773c517757 update LTTP player template to add all universal AP options (#1372) 2023-01-07 10:09:33 +01:00
JoshuaEagles
2509b7fa3f SA2B: Add Linux section to setup guide (#1374) 2023-01-07 10:00:19 +01:00
toasterparty
10652d23e0 [OC2] Logic: fixes fails when horde levels/items are excluded from location pool (#1369) 2023-01-05 15:49:50 +01:00
JoshuaEagles
f0bc3d33ac Subnautica: add Linux note to setup guide (#1365) 2023-01-04 15:26:32 +01:00
toasterparty
92d1ed60c6 [OC2] Fix "Moon 1-5" never appearing in level pool (#1366) 2023-01-04 15:21:52 +01:00
Zach Parks
fe2b431821 MultiServer: Remove forced_auto_forfeit (#1363) 2023-01-02 19:26:34 -06:00
Zach Parks
0cc83698f9 Docs: Add special name keywords to docs. (#1353) 2023-01-02 14:42:47 -06:00
Alchav
428f643b07 Pokémon R/B: Fix Pokémon Tower 7F crash (#1362) 2023-01-02 13:29:44 -06:00
Fabian Dill
d4e2b75520 Clients: retry connection with ssl (#1341) 2023-01-02 20:24:54 +01:00
Fabian Dill
96cc7f79dc Subnautica: fix early seaglide 2023-01-02 20:24:14 +01:00
Fabian Dill
bdfbc7e14a Network: allow sending frozenset 2023-01-02 20:23:31 +01:00
Fabian Dill
94c6562f82 Tests: make sure DB overwrite actually takes 2023-01-02 20:23:00 +01:00
alwaysintreble
22fe31a141 Generate: fix default utils options (#1361) 2023-01-02 12:48:31 -06:00
alwaysintreble
72fa19ee1f MultiServer/WebHost: rename all references to forfeit and deprecate it (#1243)
* Webhost: rename all references to forfeit and deprecate it

* needed some renames in multiserver for all the commands to function

* remove forfeit commands

* support forfeit_mode for clients

* rename `forfeit_player` to `release_player`
2023-01-02 12:29:21 -06:00
Zach Parks
d899e918b4 Rogue Legacy: Fix early vendors and architect... again. (#1359) 2023-01-02 12:25:47 -06:00
Zach Parks
33d31c4f0f WebHost: Capitalize Special Range choices to keep consistency. (#1360) 2023-01-02 12:25:33 -06:00
Jarno
9c3c69702a WebHost: Fixed game order by title in Site Map (#1349) 2023-01-02 12:24:08 -06:00
Alchav
dae1a3e0f9 Pokemon R/B: Add Revive to better_shops (#1352) 2023-01-02 12:21:08 -06:00
Alchav
1f1ef10cfe [Pokémon R/B] Fix DeathLink softlock and increment data version (#1348) 2022-12-24 08:25:34 +01:00
Alchav
760af59308 [Pokemon R/B] Fix missing lift key logic 2022-12-23 09:39:21 +01:00
Alchav
3dd7e3e706 [Pokemon R/B] actually implement lose_money_on_blackout 2022-12-23 09:39:21 +01:00
espeon65536
189b129dca oot: repair closed forest + dungeon ER 2022-12-22 06:40:51 +01:00
88 changed files with 1309 additions and 747 deletions

View File

@@ -1,20 +1,20 @@
from __future__ import annotations
from argparse import Namespace
import copy
from enum import unique, IntEnum, IntFlag
import logging
import json
import functools
from collections import OrderedDict, Counter, deque
from typing import List, Dict, Optional, Set, Iterable, Union, Any, Tuple, TypedDict, Callable, NamedTuple
import typing # this can go away when Python 3.8 support is dropped
import secrets
import json
import logging
import random
import secrets
import typing # this can go away when Python 3.8 support is dropped
from argparse import Namespace
from collections import OrderedDict, Counter, deque
from enum import unique, IntEnum, IntFlag
from typing import List, Dict, Optional, Set, Iterable, Union, Any, Tuple, TypedDict, Callable, NamedTuple
import NetUtils
import Options
import Utils
import NetUtils
class Group(TypedDict, total=False):
@@ -48,6 +48,7 @@ class MultiWorld():
precollected_items: Dict[int, List[Item]]
state: CollectionState
plando_options: PlandoOptions
accessibility: Dict[int, Options.Accessibility]
early_items: Dict[int, Dict[str, int]]
local_early_items: Dict[int, Dict[str, int]]
@@ -160,6 +161,7 @@ class MultiWorld():
self.custom_data = {}
self.worlds = {}
self.slot_seeds = {}
self.plando_options = PlandoOptions.none
def get_all_ids(self) -> Tuple[int, ...]:
return self.player_ids + tuple(self.groups)
@@ -391,7 +393,12 @@ class MultiWorld():
def get_items(self) -> List[Item]:
return [loc.item for loc in self.get_filled_locations()] + self.itempool
def find_item_locations(self, item, player: int) -> List[Location]:
def find_item_locations(self, item, player: int, resolve_group_locations: bool = False) -> List[Location]:
if resolve_group_locations:
player_groups = self.get_player_groups(player)
return [location for location in self.get_locations() if
location.item and location.item.name == item and location.player not in player_groups and
(location.item.player == player or location.item.player in player_groups)]
return [location for location in self.get_locations() if
location.item and location.item.name == item and location.item.player == player]
@@ -399,7 +406,12 @@ class MultiWorld():
return next(location for location in self.get_locations() if
location.item and location.item.name == item and location.item.player == player)
def find_items_in_locations(self, items: Set[str], player: int) -> List[Location]:
def find_items_in_locations(self, items: Set[str], player: int, resolve_group_locations: bool = False) -> List[Location]:
if resolve_group_locations:
player_groups = self.get_player_groups(player)
return [location for location in self.get_locations() if
location.item and location.item.name in items and location.player not in player_groups and
(location.item.player == player or location.item.player in player_groups)]
return [location for location in self.get_locations() if
location.item and location.item.name in items and location.item.player == player]
@@ -1558,6 +1570,7 @@ class Spoiler():
Utils.__version__, self.multiworld.seed))
outfile.write('Filling Algorithm: %s\n' % self.multiworld.algorithm)
outfile.write('Players: %d\n' % self.multiworld.players)
outfile.write(f'Plando Options: {self.multiworld.plando_options}\n')
AutoWorld.call_stage(self.multiworld, "write_spoiler_header", outfile)
for player in range(1, self.multiworld.players + 1):
@@ -1674,6 +1687,45 @@ class Tutorial(NamedTuple):
authors: List[str]
class PlandoOptions(IntFlag):
none = 0b0000
items = 0b0001
connections = 0b0010
texts = 0b0100
bosses = 0b1000
@classmethod
def from_option_string(cls, option_string: str) -> PlandoOptions:
result = cls(0)
for part in option_string.split(","):
part = part.strip().lower()
if part:
result = cls._handle_part(part, result)
return result
@classmethod
def from_set(cls, option_set: Set[str]) -> PlandoOptions:
result = cls(0)
for part in option_set:
result = cls._handle_part(part, result)
return result
@classmethod
def _handle_part(cls, part: str, base: PlandoOptions) -> PlandoOptions:
try:
part = cls[part]
except Exception as e:
raise KeyError(f"{part} is not a recognized name for a plando module. "
f"Known options: {', '.join(flag.name for flag in cls)}") from e
else:
return base | part
def __str__(self) -> str:
if self.value:
return ", ".join(flag.name for flag in PlandoOptions if self.value & flag.value)
return "None"
seeddigits = 20

View File

@@ -193,7 +193,7 @@ class CommonContext:
self.hint_cost = None
self.slot_info = {}
self.permissions = {
"forfeit": "disabled",
"release": "disabled",
"collect": "disabled",
"remaining": "disabled",
}
@@ -260,7 +260,7 @@ class CommonContext:
self.server_task = None
self.hint_cost = None
self.permissions = {
"forfeit": "disabled",
"release": "disabled",
"collect": "disabled",
"remaining": "disabled",
}
@@ -494,7 +494,7 @@ class CommonContext:
self._messagebox.open()
return self._messagebox
def _handle_connection_loss(self, msg: str) -> None:
def handle_connection_loss(self, msg: str) -> None:
"""Helper for logging and displaying a loss of connection. Must be called from an except block."""
exc_info = sys.exc_info()
logger.exception(msg, exc_info=exc_info, extra={'compact_gui': True})
@@ -580,14 +580,22 @@ async def server_loop(ctx: CommonContext, address: typing.Optional[str] = None)
for msg in decode(data):
await process_server_cmd(ctx, msg)
logger.warning(f"Disconnected from multiworld server{reconnect_hint()}")
except websockets.InvalidMessage:
# probably encrypted
if address.startswith("ws://"):
await server_loop(ctx, "ws" + address[1:])
else:
ctx.handle_connection_loss(f"Lost connection to the multiworld server due to InvalidMessage"
f"{reconnect_hint()}")
except ConnectionRefusedError:
ctx._handle_connection_loss("Connection refused by the server. May not be running Archipelago on that address or port.")
ctx.handle_connection_loss("Connection refused by the server. "
"May not be running Archipelago on that address or port.")
except websockets.InvalidURI:
ctx._handle_connection_loss("Failed to connect to the multiworld server (invalid URI)")
ctx.handle_connection_loss("Failed to connect to the multiworld server (invalid URI)")
except OSError:
ctx._handle_connection_loss("Failed to connect to the multiworld server")
ctx.handle_connection_loss("Failed to connect to the multiworld server")
except Exception:
ctx._handle_connection_loss(f"Lost connection to the multiworld server{reconnect_hint()}")
ctx.handle_connection_loss(f"Lost connection to the multiworld server{reconnect_hint()}")
finally:
await ctx.connection_closed()
if ctx.server_address and ctx.username and not ctx.disconnected_intentionally:
@@ -813,6 +821,10 @@ if __name__ == '__main__':
def on_package(self, cmd: str, args: dict):
if cmd == "Connected":
self.game = self.slot_info[self.slot].game
async def disconnect(self, allow_autoreconnect: bool = False):
self.game = ""
await super().disconnect(allow_autoreconnect)
async def main(args):

View File

@@ -2,14 +2,13 @@ from __future__ import annotations
import argparse
import logging
import random
import urllib.request
import urllib.parse
from typing import Set, Dict, Tuple, Callable, Any, Union
import os
from collections import Counter, ChainMap
import random
import string
import enum
import urllib.parse
import urllib.request
from collections import Counter, ChainMap
from typing import Dict, Tuple, Callable, Any, Union
import ModuleUpdate
@@ -18,52 +17,17 @@ ModuleUpdate.update()
import Utils
from worlds.alttp import Options as LttPOptions
from worlds.generic import PlandoConnection
from Utils import parse_yamls, version_tuple, __version__, tuplize_version, get_options, local_path, user_path
from Utils import parse_yamls, version_tuple, __version__, tuplize_version, get_options, user_path
from worlds.alttp.EntranceRandomizer import parse_arguments
from Main import main as ERmain
from BaseClasses import seeddigits, get_seed
from BaseClasses import seeddigits, get_seed, PlandoOptions
import Options
from worlds.alttp.Text import TextTable
from worlds.AutoWorld import AutoWorldRegister
import copy
class PlandoSettings(enum.IntFlag):
items = 0b0001
connections = 0b0010
texts = 0b0100
bosses = 0b1000
@classmethod
def from_option_string(cls, option_string: str) -> PlandoSettings:
result = cls(0)
for part in option_string.split(","):
part = part.strip().lower()
if part:
result = cls._handle_part(part, result)
return result
@classmethod
def from_set(cls, option_set: Set[str]) -> PlandoSettings:
result = cls(0)
for part in option_set:
result = cls._handle_part(part, result)
return result
@classmethod
def _handle_part(cls, part: str, base: PlandoSettings) -> PlandoSettings:
try:
part = cls[part]
except Exception as e:
raise KeyError(f"{part} is not a recognized name for a plando module. "
f"Known options: {', '.join(flag.name for flag in cls)}") from e
else:
return base | part
def __str__(self) -> str:
if self.value:
return ", ".join(flag.name for flag in PlandoSettings if self.value & flag.value)
return "Off"
def mystery_argparse():
@@ -97,7 +61,7 @@ def mystery_argparse():
args.weights_file_path = os.path.join(args.player_files_path, args.weights_file_path)
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: PlandoSettings = PlandoSettings.from_option_string(args.plando)
args.plando: PlandoOptions = PlandoOptions.from_option_string(args.plando)
return args, options
@@ -170,6 +134,7 @@ def main(args=None, callback=ERmain):
f"A mix is also permitted.")
erargs = parse_arguments(['--multi', str(args.multi)])
erargs.seed = seed
erargs.plando_options = args.plando
erargs.glitch_triforce = options["generator"]["glitch_triforce_room"]
erargs.spoiler = args.spoiler
erargs.race = args.race
@@ -226,7 +191,7 @@ def main(args=None, callback=ERmain):
elif not erargs.name[player]: # if name was not specified, generate it from filename
erargs.name[player] = os.path.splitext(os.path.split(path)[-1])[0]
erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
player += 1
except Exception as e:
raise ValueError(f"File {path} is destroyed. Please fix your yaml.") from e
@@ -443,7 +408,7 @@ def roll_triggers(weights: dict, triggers: list) -> dict:
return weights
def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, option: type(Options.Option), plando_options: PlandoSettings):
def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, option: type(Options.Option), plando_options: PlandoOptions):
if option_key in game_weights:
try:
if not option.supports_weighting:
@@ -459,7 +424,7 @@ def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str,
setattr(ret, option_key, option.from_any(option.default)) # call the from_any here to support default "random"
def roll_settings(weights: dict, plando_options: PlandoSettings = PlandoSettings.bosses):
def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.bosses):
if "linked_options" in weights:
weights = roll_linked_options(weights)
@@ -472,7 +437,7 @@ def roll_settings(weights: dict, plando_options: PlandoSettings = PlandoSettings
if tuplize_version(version) > version_tuple:
raise Exception(f"Settings reports required version of generator is at least {version}, "
f"however generator is of version {__version__}")
required_plando_options = PlandoSettings.from_option_string(requirements.get("plando", ""))
required_plando_options = PlandoOptions.from_option_string(requirements.get("plando", ""))
if required_plando_options not in plando_options:
if required_plando_options:
raise Exception(f"Settings reports required plando module {str(required_plando_options)}, "
@@ -506,12 +471,12 @@ def roll_settings(weights: dict, plando_options: PlandoSettings = PlandoSettings
if option_key not in world_type.option_definitions and \
(option_key not in Options.common_options or option_key in game_weights):
handle_option(ret, game_weights, option_key, option, plando_options)
if PlandoSettings.items in plando_options:
if PlandoOptions.items in plando_options:
ret.plando_items = game_weights.get("plando_items", [])
if ret.game == "Minecraft" or ret.game == "Ocarina of Time":
# bad hardcoded behavior to make this work for now
ret.plando_connections = []
if PlandoSettings.connections in plando_options:
if PlandoOptions.connections in plando_options:
options = game_weights.get("plando_connections", [])
for placement in options:
if roll_percentage(get_choice("percentage", placement, 100)):
@@ -626,7 +591,7 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
raise Exception(f"unknown Medallion {medallion} for {'misery mire' if index == 0 else 'turtle rock'}")
ret.plando_texts = {}
if PlandoSettings.texts in plando_options:
if PlandoOptions.texts in plando_options:
tt = TextTable()
tt.removeUnwantedText()
options = weights.get("plando_texts", [])
@@ -638,7 +603,7 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
ret.plando_texts[at] = str(get_choice_legacy("text", placement))
ret.plando_connections = []
if PlandoSettings.connections in plando_options:
if PlandoOptions.connections in plando_options:
options = weights.get("plando_connections", [])
for placement in options:
if roll_percentage(get_choice_legacy("percentage", placement, 100)):

26
Main.py
View File

@@ -38,6 +38,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
logger = logging.getLogger()
world.set_seed(seed, args.race, str(args.outputname if args.outputname else world.seed))
world.plando_options = args.plando_options
world.shuffle = args.shuffle.copy()
world.logic = args.logic.copy()
@@ -121,6 +122,10 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
logger.info('Creating Items.')
AutoWorld.call_all(world, "create_items")
# All worlds should have finished creating all regions, locations, and entrances.
# Recache to ensure that they are all visible for locality rules.
world._recache()
logger.info('Calculating Access Rules.')
for player in world.player_ids:
@@ -291,27 +296,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
checks_in_area[location.player]["Dark World"].append(location.address)
checks_in_area[location.player]["Total"] += 1
oldmancaves = []
takeanyregions = ["Old Man Sword Cave", "Take-Any #1", "Take-Any #2", "Take-Any #3", "Take-Any #4"]
for index, take_any in enumerate(takeanyregions):
for region in [world.get_region(take_any, player) for player in
world.get_game_players("A Link to the Past") if world.retro_caves[player]]:
item = world.create_item(
region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'],
region.player)
player = region.player
location_id = SHOP_ID_START + total_shop_slots + index
main_entrance = region.get_connecting_entrance(is_main_entrance)
if main_entrance.parent_region.type == RegionType.LightWorld:
checks_in_area[player]["Light World"].append(location_id)
else:
checks_in_area[player]["Dark World"].append(location_id)
checks_in_area[player]["Total"] += 1
er_hint_data[player][location_id] = main_entrance.name
oldmancaves.append(((location_id, player), (item.code, player)))
FillDisabledShopSlots(world)
def write_multidata():

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
import argparse
import asyncio
import copy
import functools
import logging
import zlib
@@ -22,6 +23,9 @@ import ModuleUpdate
ModuleUpdate.update()
if typing.TYPE_CHECKING:
import ssl
import websockets
import colorama
try:
@@ -40,6 +44,28 @@ min_client_version = Version(0, 1, 6)
print_command_compatability_threshold = Version(0, 3, 5) # Remove backwards compatibility around 0.3.7
colorama.init()
def remove_from_list(container, value):
try:
container.remove(value)
except ValueError:
pass
return container
def pop_from_container(container, value):
try:
container.pop(value)
except ValueError:
pass
return container
def update_dict(dictionary, entries):
dictionary.update(entries)
return dictionary
# functions callable on storable data on the server by clients
modify_functions = {
"add": operator.add, # add together two objects, using python's "+" operator (works on strings and lists as append)
@@ -56,6 +82,10 @@ modify_functions = {
"and": operator.and_,
"left_shift": operator.lshift,
"right_shift": operator.rshift,
# lists/dicts
"remove": remove_from_list,
"pop": pop_from_container,
"update": update_dict,
}
@@ -116,7 +146,7 @@ class Context:
"location_check_points": int,
"server_password": str,
"password": str,
"forfeit_mode": str,
"release_mode": str,
"remaining_mode": str,
"collect_mode": str,
"item_cheat": bool,
@@ -134,11 +164,10 @@ class Context:
item_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]]
location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})')
all_item_and_group_names: typing.Dict[str, typing.Set[str]]
forced_auto_forfeits: typing.Dict[str, bool]
non_hintable_names: typing.Dict[str, typing.Set[str]]
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
hint_cost: int, item_cheat: bool, forfeit_mode: str = "disabled", collect_mode="disabled",
hint_cost: int, item_cheat: bool, release_mode: str = "disabled", collect_mode="disabled",
remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2,
log_network: bool = False):
super(Context, self).__init__()
@@ -154,7 +183,7 @@ class Context:
self.player_names: typing.Dict[team_slot, str] = {}
self.player_name_lookup: typing.Dict[str, team_slot] = {}
self.connect_names = {} # names of slots clients can connect to
self.allow_forfeits = {}
self.allow_releases = {}
# player location_id item_id target_player_id
self.locations = {}
self.host = host
@@ -171,7 +200,7 @@ class Context:
self.location_check_points = location_check_points
self.hints_used = collections.defaultdict(int)
self.hints: typing.Dict[team_slot, typing.Set[NetUtils.Hint]] = collections.defaultdict(set)
self.forfeit_mode: str = forfeit_mode
self.release_mode: str = release_mode
self.remaining_mode: str = remaining_mode
self.collect_mode: str = collect_mode
self.item_cheat = item_cheat
@@ -204,7 +233,6 @@ class Context:
self.gamespackage = {}
self.item_name_groups = {}
self.all_item_and_group_names = {}
self.forced_auto_forfeits = collections.defaultdict(lambda: False)
self.non_hintable_names = collections.defaultdict(frozenset)
self._load_game_data()
@@ -217,7 +245,6 @@ class Context:
self.item_name_groups = {world_name: world.item_name_groups for world_name, world in
worlds.AutoWorldRegister.world_types.items()}
for world_name, world in worlds.AutoWorldRegister.world_types.items():
self.forced_auto_forfeits[world_name] = world.forced_auto_forfeit
self.non_hintable_names[world_name] = world.hint_blacklist
def _init_game_data(self):
@@ -317,7 +344,7 @@ class Context:
if not client.auth:
return
if client.version >= print_command_compatability_threshold:
async_start(self.send_msgs(client,
async_start(self.send_msgs(client,
[{"cmd": "PrintJSON", "data": [{ "text": text }]} for text in texts]))
else:
async_start(self.send_msgs(client, [{"cmd": "Print", "text": text} for text in texts]))
@@ -513,9 +540,10 @@ class Context:
"group_collected": dict(self.group_collected),
"stored_data": self.stored_data,
"game_options": {"hint_cost": self.hint_cost, "location_check_points": self.location_check_points,
"server_password": self.server_password, "password": self.password, "forfeit_mode":
self.forfeit_mode, "remaining_mode": self.remaining_mode, "collect_mode":
self.collect_mode, "item_cheat": self.item_cheat, "compatibility": self.compatibility}
"server_password": self.server_password, "password": self.password,
"forfeit_mode": self.release_mode, "release_mode": self.release_mode, # TODO remove forfeit_mode around 0.4
"remaining_mode": self.remaining_mode, "collect_mode": self.collect_mode,
"item_cheat": self.item_cheat, "compatibility": self.compatibility}
}
@@ -546,7 +574,7 @@ class Context:
self.location_check_points = savedata["game_options"]["location_check_points"]
self.server_password = savedata["game_options"]["server_password"]
self.password = savedata["game_options"]["password"]
self.forfeit_mode = savedata["game_options"]["forfeit_mode"]
self.release_mode = savedata["game_options"].get("release_mode", savedata["game_options"].get("forfeit_mode", "goal"))
self.remaining_mode = savedata["game_options"]["remaining_mode"]
self.collect_mode = savedata["game_options"]["collect_mode"]
self.item_cheat = savedata["game_options"]["item_cheat"]
@@ -591,6 +619,8 @@ class Context:
def _set_options(self, server_options: dict):
for key, value in server_options.items():
if key == "forfeit_mode":
key = "release_mode"
data_type = self.simple_options.get(key, None)
if data_type is not None:
if value not in {False, True, None}: # some can be boolean OR text, such as password
@@ -658,10 +688,8 @@ class Context:
self.notify_all(finished_msg)
if "auto" in self.collect_mode:
collect_player(self, client.team, client.slot)
if "auto" in self.forfeit_mode:
forfeit_player(self, client.team, client.slot)
elif self.forced_auto_forfeits[self.games[client.slot]]:
forfeit_player(self, client.team, client.slot)
if "auto" in self.release_mode:
release_player(self, client.team, client.slot)
self.save() # save goal completion flag
def on_new_hint(self, team: int, slot: int):
@@ -734,7 +762,8 @@ async def on_client_connected(ctx: Context, client: Client):
def get_permissions(ctx) -> typing.Dict[str, Permission]:
return {
"forfeit": Permission.from_text(ctx.forfeit_mode),
"forfeit": Permission.from_text(ctx.release_mode), # TODO remove around 0.4
"release": Permission.from_text(ctx.release_mode),
"remaining": Permission.from_text(ctx.remaining_mode),
"collect": Permission.from_text(ctx.collect_mode)
}
@@ -862,7 +891,7 @@ def update_checked_locations(ctx: Context, team: int, slot: int):
[{"cmd": "RoomUpdate", "checked_locations": get_checked_checks(ctx, team, slot)}])
def forfeit_player(ctx: Context, team: int, slot: int):
def release_player(ctx: Context, team: int, slot: int):
"""register any locations that are in the multidata"""
all_locations = set(ctx.locations[slot])
ctx.notify_all("%s (Team #%d) has released all remaining items from their world." % (ctx.player_names[(team, slot)], team + 1))
@@ -1228,23 +1257,19 @@ class ClientMessageProcessor(CommonCommandProcessor):
def _cmd_release(self) -> bool:
"""Sends remaining items in your world to their recipients."""
return self._cmd_forfeit()
def _cmd_forfeit(self) -> bool:
"""Surrender and send your remaining items out to their recipients. Use release in the future."""
if self.ctx.allow_forfeits.get((self.client.team, self.client.slot), False):
forfeit_player(self.ctx, self.client.team, self.client.slot)
if self.ctx.allow_releases.get((self.client.team, self.client.slot), False):
release_player(self.ctx, self.client.team, self.client.slot)
return True
if "enabled" in self.ctx.forfeit_mode:
forfeit_player(self.ctx, self.client.team, self.client.slot)
if "enabled" in self.ctx.release_mode:
release_player(self.ctx, self.client.team, self.client.slot)
return True
elif "disabled" in self.ctx.forfeit_mode:
elif "disabled" in self.ctx.release_mode:
self.output("Sorry, client item releasing has been disabled on this server. "
"You can ask the server admin for a /release")
return False
else: # is auto or goal
if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL:
forfeit_player(self.ctx, self.client.team, self.client.slot)
release_player(self.ctx, self.client.team, self.client.slot)
return True
else:
self.output(
@@ -1741,7 +1766,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
return
args["cmd"] = "SetReply"
value = ctx.stored_data.get(args["key"], args.get("default", 0))
args["original_value"] = value
args["original_value"] = copy.copy(value)
for operation in args["operations"]:
func = modify_functions[operation["operation"]]
value = func(value, operation["value"])
@@ -1872,28 +1897,23 @@ class ServerCommandProcessor(CommonCommandProcessor):
@mark_raw
def _cmd_release(self, player_name: str) -> bool:
"""Send out the remaining items from a player to their intended recipients."""
return self._cmd_forfeit(player_name)
@mark_raw
def _cmd_forfeit(self, player_name: str) -> bool:
"""Send out the remaining items from a player to their intended recipients."""
player = self.resolve_player(player_name)
if player:
team, slot, _ = player
forfeit_player(self.ctx, team, slot)
release_player(self.ctx, team, slot)
return True
self.output(f"Could not find player {player_name} to release")
return False
@mark_raw
def _cmd_allow_forfeit(self, player_name: str) -> bool:
def _cmd_allow_release(self, player_name: str) -> bool:
"""Allow the specified player to use the !release command."""
player = self.resolve_player(player_name)
if player:
team, slot, name = player
self.ctx.allow_forfeits[(team, slot)] = True
self.ctx.allow_releases[(team, slot)] = True
self.output(f"Player {name} is now allowed to use the !release command at any time.")
return True
@@ -1901,12 +1921,12 @@ class ServerCommandProcessor(CommonCommandProcessor):
return False
@mark_raw
def _cmd_forbid_forfeit(self, player_name: str) -> bool:
def _cmd_forbid_release(self, player_name: str) -> bool:
""""Disallow the specified player from using the !release command."""
player = self.resolve_player(player_name)
if player:
team, slot, name = player
self.ctx.allow_forfeits[(team, slot)] = False
self.ctx.allow_releases[(team, slot)] = False
self.output(f"Player {name} has to follow the server restrictions on use of the !release command.")
return True
@@ -2061,7 +2081,7 @@ class ServerCommandProcessor(CommonCommandProcessor):
return input_text
setattr(self.ctx, option_name, attrtype(option))
self.output(f"Set option {option_name} to {getattr(self.ctx, option_name)}")
if option_name in {"forfeit_mode", "remaining_mode", "collect_mode"}:
if option_name in {"release_mode", "remaining_mode", "collect_mode"}:
self.ctx.broadcast_all([{"cmd": "RoomUpdate", 'permissions': get_permissions(self.ctx)}])
elif option_name in {"hint_cost", "location_check_points"}:
self.ctx.broadcast_all([{"cmd": "RoomUpdate", option_name: getattr(self.ctx, option_name)}])
@@ -2101,19 +2121,21 @@ def parse_args() -> argparse.Namespace:
parser.add_argument('--password', default=defaults["password"])
parser.add_argument('--savefile', default=defaults["savefile"])
parser.add_argument('--disable_save', default=defaults["disable_save"], action='store_true')
parser.add_argument('--cert', help="Path to a SSL Certificate for encryption.")
parser.add_argument('--cert_key', help="Path to SSL Certificate Key file")
parser.add_argument('--loglevel', default=defaults["loglevel"],
choices=['debug', 'info', 'warning', 'error', 'critical'])
parser.add_argument('--location_check_points', default=defaults["location_check_points"], type=int)
parser.add_argument('--hint_cost', default=defaults["hint_cost"], type=int)
parser.add_argument('--disable_item_cheat', default=defaults["disable_item_cheat"], action='store_true')
parser.add_argument('--forfeit_mode', default=defaults["forfeit_mode"], nargs='?',
parser.add_argument('--release_mode', default=defaults["release_mode"], nargs='?',
choices=['auto', 'enabled', 'disabled', "goal", "auto-enabled"], help='''\
Select !forfeit Accessibility. (default: %(default)s)
auto: Automatic "forfeit" on goal completion
enabled: !forfeit is always available
disabled: !forfeit is never available
goal: !forfeit can be used after goal completion
auto-enabled: !forfeit is available and automatically triggered on goal completion
Select !release Accessibility. (default: %(default)s)
auto: Automatic "release" on goal completion
enabled: !release is always available
disabled: !release is never available
goal: !release can be used after goal completion
auto-enabled: !release is available and automatically triggered on goal completion
''')
parser.add_argument('--collect_mode', default=defaults["collect_mode"], nargs='?',
choices=['auto', 'enabled', 'disabled', "goal", "auto-enabled"], help='''\
@@ -2135,7 +2157,7 @@ def parse_args() -> argparse.Namespace:
help="automatically shut down the server after this many minutes without new location checks. "
"0 to keep running. Not yet implemented.")
parser.add_argument('--use_embedded_options', action="store_true",
help='retrieve forfeit, remaining and hint options from the multidata file,'
help='retrieve release, remaining and hint options from the multidata file,'
' instead of host.yaml')
parser.add_argument('--compatibility', default=defaults["compatibility"], type=int,
help="""
@@ -2173,11 +2195,19 @@ async def auto_shutdown(ctx, to_cancel=None):
await asyncio.sleep(seconds)
def load_server_cert(path: str, cert_key: typing.Optional[str]) -> "ssl.SSLContext":
import ssl
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ssl_context.load_default_certs()
ssl_context.load_cert_chain(path, cert_key if cert_key else path)
return ssl_context
async def main(args: argparse.Namespace):
Utils.init_logging("Server", loglevel=args.loglevel.lower())
ctx = Context(args.host, args.port, args.server_password, args.password, args.location_check_points,
args.hint_cost, not args.disable_item_cheat, args.forfeit_mode, args.collect_mode,
args.hint_cost, not args.disable_item_cheat, args.release_mode, args.collect_mode,
args.remaining_mode,
args.auto_shutdown, args.compatibility, args.log_network)
data_filename = args.multidata
@@ -2208,8 +2238,10 @@ async def main(args: argparse.Namespace):
ctx.init_save(not args.disable_save)
ssl_context = load_server_cert(args.cert, args.cert_key) if args.cert else None
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), host=ctx.host, port=ctx.port, ping_timeout=None,
ping_interval=None)
ping_interval=None, ssl=ssl_context)
ip = args.host if args.host else Utils.get_public_ipv4()
logging.info('Hosting game at %s:%d (%s)' % (ip, ctx.port,
'No password' if not ctx.password else 'Password: %s' % ctx.password))

View File

@@ -43,7 +43,7 @@ class Permission(enum.IntFlag):
disabled = 0b000 # 0, completely disables access
enabled = 0b001 # 1, allows manual use
goal = 0b010 # 2, allows manual use after goal completion
auto = 0b110 # 6, forces use after goal completion, only works for forfeit
auto = 0b110 # 6, forces use after goal completion, only works for release
auto_enabled = 0b111 # 7, forces use after goal completion, allows manual use any time
@staticmethod
@@ -86,7 +86,7 @@ def _scan_for_TypedTuples(obj: typing.Any) -> typing.Any:
data = obj._asdict()
data["class"] = obj.__class__.__name__
return data
if isinstance(obj, (tuple, list, set)):
if isinstance(obj, (tuple, list, set, frozenset)):
return tuple(_scan_for_TypedTuples(o) for o in obj)
if isinstance(obj, dict):
return {key: _scan_for_TypedTuples(value) for key, value in obj.items()}
@@ -109,7 +109,7 @@ def get_any_version(data: dict) -> Version:
return Version(int(data["major"]), int(data["minor"]), int(data["build"]))
whitelist = {
allowlist = {
"NetworkPlayer": NetworkPlayer,
"NetworkItem": NetworkItem,
"NetworkSlot": NetworkSlot
@@ -125,7 +125,7 @@ def _object_hook(o: typing.Any) -> typing.Any:
hook = custom_hooks.get(o.get("class", None), None)
if hook:
return hook(o)
cls = whitelist.get(o.get("class", None), None)
cls = allowlist.get(o.get("class", None), None)
if cls:
for key in tuple(o):
if key not in cls._fields:

View File

@@ -133,10 +133,10 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
raise NotImplementedError
if typing.TYPE_CHECKING:
from Generate import PlandoSettings
from Generate import PlandoOptions
from worlds.AutoWorld import World
def verify(self, world: World, player_name: str, plando_options: PlandoSettings) -> None:
def verify(self, world: World, player_name: str, plando_options: PlandoOptions) -> None:
pass
else:
def verify(self, *args, **kwargs) -> None:
@@ -578,8 +578,8 @@ class PlandoBosses(TextChoice, metaclass=BossMeta):
def verify(self, world, player_name: str, plando_options) -> None:
if isinstance(self.value, int):
return
from Generate import PlandoSettings
if not(PlandoSettings.bosses & plando_options):
from Generate import PlandoOptions
if not(PlandoOptions.bosses & plando_options):
import logging
# plando is disabled but plando options were given so pull the option and change it to an int
option = self.value.split(";")[-1]

View File

@@ -10,6 +10,8 @@ import re
import sys
import typing
import queue
import zipfile
import io
from pathlib import Path
# CommonClient import first to trigger ModuleUpdater
@@ -120,9 +122,9 @@ class StarcraftClientProcessor(ClientCommandProcessor):
sc2_logger.warning("When using set_path, you must type the path to your SC2 install directory.")
return False
def _cmd_download_data(self, force: bool = False) -> bool:
def _cmd_download_data(self) -> bool:
"""Download the most recent release of the necessary files for playing SC2 with
Archipelago. force should be True or False. force=True will overwrite your files."""
Archipelago. Will overwrite existing files."""
if "SC2PATH" not in os.environ:
check_game_install_path()
@@ -132,11 +134,11 @@ class StarcraftClientProcessor(ClientCommandProcessor):
else:
current_ver = None
tempzip, version = download_latest_release_zip('TheCondor07', 'Starcraft2ArchipelagoData', current_version=current_ver, force_download=force)
tempzip, version = download_latest_release_zip('TheCondor07', 'Starcraft2ArchipelagoData',
current_version=current_ver, force_download=True)
if tempzip != '':
try:
import zipfile
zipfile.ZipFile(tempzip).extractall(path=os.environ["SC2PATH"])
sc2_logger.info(f"Download complete. Version {version} installed.")
with open(os.environ["SC2PATH"]+"ArchipelagoSC2Version.txt", "w") as f:
@@ -195,12 +197,16 @@ class SC2Context(CommonContext):
self.build_location_to_mission_mapping()
# Looks for the required maps and mods for SC2. Runs check_game_install_path.
is_mod_installed_correctly()
maps_present = is_mod_installed_correctly()
if os.path.exists(os.environ["SC2PATH"] + "ArchipelagoSC2Version.txt"):
with open(os.environ["SC2PATH"] + "ArchipelagoSC2Version.txt", "r") as f:
current_ver = f.read()
if is_mod_update_available("TheCondor07", "Starcraft2ArchipelagoData", current_ver):
sc2_logger.info("NOTICE: Update for required files found. Run /download_data to install.")
elif maps_present:
sc2_logger.warning("NOTICE: Your map files may be outdated (version number not found). "
"Run /download_data to update them.")
def on_print_json(self, args: dict):
# goes to this world
@@ -1003,7 +1009,7 @@ def download_latest_release_zip(owner: str, repo: str, current_version: str = No
download_url = r1.json()["assets"][0]["browser_download_url"]
r2 = requests.get(download_url, headers=headers)
if r2.status_code == 200:
if r2.status_code == 200 and zipfile.is_zipfile(io.BytesIO(r2.content)):
with open(f"{repo}.zip", "wb") as fh:
fh.write(r2.content)
sc2_logger.info(f"Successfully downloaded {repo}.zip.")

View File

@@ -38,7 +38,7 @@ class Version(typing.NamedTuple):
build: int
__version__ = "0.3.7"
__version__ = "0.3.8"
version_tuple = tuplize_version(__version__)
is_linux = sys.platform.startswith("linux")
@@ -260,7 +260,7 @@ def get_default_options() -> OptionsType:
"disable_item_cheat": False,
"location_check_points": 1,
"hint_cost": 10,
"forfeit_mode": "goal",
"release_mode": "goal",
"collect_mode": "disabled",
"remaining_mode": "goal",
"auto_shutdown": 0,
@@ -505,7 +505,7 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, wri
except Exception as e:
logging.exception(e)
else:
logging.info(f"Deleted old logfile {file.path}")
logging.debug(f"Deleted old logfile {file.path}")
import threading
threading.Thread(target=_cleanup, name="LogCleaner").start()
import platform

View File

@@ -29,7 +29,7 @@ if not os.path.exists(configpath): # fall back to config.yaml in home
def get_app():
register()
app = raw_app
if os.path.exists(configpath):
if os.path.exists(configpath) and not app.config["TESTING"]:
import yaml
app.config.from_file(configpath, yaml.safe_load)
logging.info(f"Updated config from {configpath}")

View File

@@ -24,6 +24,8 @@ app.jinja_env.filters['all'] = all
app.config["SELFHOST"] = True # application process is in charge of running the websites
app.config["GENERATORS"] = 8 # maximum concurrent world gens
app.config["SELFLAUNCH"] = True # application process is in charge of launching Rooms.
app.config["SELFLAUNCHCERT"] = None # can point to a SSL Certificate to encrypt Room websocket connections
app.config["SELFLAUNCHKEY"] = None # can point to a SSL Certificate Key to encrypt Room websocket connections
app.config["SELFGEN"] = True # application process is in charge of scheduling Generations.
app.config["DEBUG"] = False
app.config["PORT"] = 80

View File

@@ -177,6 +177,8 @@ class MultiworldInstance():
with guardian_lock:
multiworlds[self.room_id] = self
self.ponyconfig = config["PONY"]
self.cert = config["SELFLAUNCHCERT"]
self.key = config["SELFLAUNCHKEY"]
def start(self):
if self.process and self.process.is_alive():
@@ -184,7 +186,8 @@ class MultiworldInstance():
logging.info(f"Spinning up {self.room_id}")
process = multiprocessing.Process(group=None, target=run_server_process,
args=(self.room_id, self.ponyconfig, get_static_server_data()),
args=(self.room_id, self.ponyconfig, get_static_server_data(),
self.cert, self.key),
name="MultiHost")
process.start()
# bind after start to prevent thread sync issues with guardian.

View File

@@ -12,7 +12,7 @@ def allowed_file(filename):
return filename.endswith(('.txt', ".yaml", ".zip"))
from Generate import roll_settings, PlandoSettings
from Generate import roll_settings, PlandoOptions
from Utils import parse_yamls
@@ -69,7 +69,7 @@ def get_yaml_data(file) -> Union[Dict[str, str], str, Markup]:
def roll_options(options: Dict[str, Union[dict, str]],
plando_options: Set[str] = frozenset({"bosses", "items", "connections", "texts"})) -> \
Tuple[Dict[str, Union[str, bool]], Dict[str, dict]]:
plando_options = PlandoSettings.from_set(set(plando_options))
plando_options = PlandoOptions.from_set(set(plando_options))
results = {}
rolled_results = {}
for filename, text in options.items():

View File

@@ -10,12 +10,14 @@ import random
import socket
import threading
import time
import typing
import websockets
from pony.orm import db_session, commit, select
from pony.orm import commit, db_session, select
import Utils
from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor
from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor, load_server_cert
from Utils import get_public_ipv4, get_public_ipv6, restricted_loads, cache_argsless
from .models import Room, Command, db
@@ -66,7 +68,6 @@ class WebHostContext(Context):
def _load_game_data(self):
for key, value in self.static_server_data.items():
setattr(self, key, value)
self.forced_auto_forfeits = collections.defaultdict(lambda: False, self.forced_auto_forfeits)
self.non_hintable_names = collections.defaultdict(frozenset, self.non_hintable_names)
def listen_to_db_commands(self):
@@ -126,7 +127,6 @@ def get_random_port():
def get_static_server_data() -> dict:
import worlds
data = {
"forced_auto_forfeits": {},
"non_hintable_names": {},
"gamespackage": worlds.network_data_package["games"],
"item_name_groups": {world_name: world.item_name_groups for world_name, world in
@@ -134,13 +134,13 @@ def get_static_server_data() -> dict:
}
for world_name, world in worlds.AutoWorldRegister.world_types.items():
data["forced_auto_forfeits"][world_name] = world.forced_auto_forfeit
data["non_hintable_names"][world_name] = world.hint_blacklist
return data
def run_server_process(room_id, ponyconfig: dict, static_server_data: dict):
def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
cert_file: typing.Optional[str], cert_key_file: typing.Optional[str]):
# establish DB connection for multidata and multisave
db.bind(**ponyconfig)
db.generate_mapping(check_tables=False)
@@ -150,15 +150,15 @@ def run_server_process(room_id, ponyconfig: dict, static_server_data: dict):
ctx = WebHostContext(static_server_data)
ctx.load(room_id)
ctx.init_save()
ssl_context = load_server_cert(cert_file, cert_key_file) if cert_file else None
try:
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ping_timeout=None,
ping_interval=None)
ping_interval=None, ssl=ssl_context)
await ctx.server
except Exception: # likely port in use - in windows this is OSError, but I didn't check the others
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, 0, ping_timeout=None,
ping_interval=None)
ping_interval=None, ssl=ssl_context)
await ctx.server
port = 0

View File

@@ -12,7 +12,7 @@ from flask import request, flash, redirect, url_for, session, render_template
from pony.orm import commit, db_session
from BaseClasses import seeddigits, get_seed
from Generate import handle_name, PlandoSettings
from Generate import handle_name, PlandoOptions
from Main import main as ERmain
from Utils import __version__
from WebHostLib import app
@@ -33,7 +33,7 @@ def get_meta(options_source: dict) -> dict:
server_options = {
"hint_cost": int(options_source.get("hint_cost", 10)),
"forfeit_mode": options_source.get("forfeit_mode", "goal"),
"release_mode": options_source.get("release_mode", "goal"),
"remaining_mode": options_source.get("remaining_mode", "disabled"),
"collect_mode": options_source.get("collect_mode", "disabled"),
"item_cheat": bool(int(options_source.get("item_cheat", 1))),
@@ -119,7 +119,7 @@ def gen_game(gen_options: dict, meta: Optional[Dict[str, Any]] = None, owner=Non
erargs.outputname = seedname
erargs.outputpath = target.name
erargs.teams = 1
erargs.plando_options = PlandoSettings.from_set(meta.setdefault("plando_options",
erargs.plando_options = PlandoOptions.from_set(meta.setdefault("plando_options",
{"bosses", "items", "connections", "texts"}))
name_counter = Counter()

View File

@@ -69,10 +69,6 @@ def tutorial(game, file, lang):
@app.route('/tutorial/')
def tutorial_landing():
worlds = {}
for game, world in AutoWorldRegister.world_types.items():
if not world.hidden:
worlds[game] = world
return render_template("tutorialLanding.html")

View File

@@ -20,7 +20,7 @@ comfortable exploiting certain glitches in the game.
## What is a multi-world?
While a randomizer shuffles a game, a multi-world randomizer shuffles that game for multiple players. For example, in a
two player multi-world, players A and B each get their own randomized version of a game, called seeds. In each player's
two player multi-world, players A and B each get their own randomized version of a game, called a world. In each player's
game, they may find items which belong to the other player. If player A finds an item which belongs to player B, the
item will be sent to player B's world over the internet.
@@ -29,7 +29,7 @@ their game.
## What happens if a person has to leave early?
If a player must leave early, they can use Archipelago's forfeit system. When a player forfeits their game, all the
If a player must leave early, they can use Archipelago's release system. When a player releases their game, all the
items in that game which belong to other players are sent out automatically, so other players can continue to play.
## What does multi-game mean?

View File

@@ -205,6 +205,11 @@ const buildOptionsTable = (settings, romOpts = false) => {
let presetOption = document.createElement('option');
presetOption.innerText = presetName;
presetOption.value = settings[setting].value_names[presetName];
const words = presetOption.innerText.split("_");
for (let i = 0; i < words.length; i++) {
words[i] = words[i][0].toUpperCase() + words[i].substring(1);
}
presetOption.innerText = words.join(" ");
specialRangeSelect.appendChild(presetOption);
});
let customOption = document.createElement('option');

View File

@@ -40,20 +40,20 @@
<tbody>
<tr>
<td>
<label for="forfeit_mode">Forfeit Permission:
<span class="interactive" data-tooltip="A forfeit releases all remaining items from the locations in your world.">
<label for="release_mode">Release Permission:
<span class="interactive" data-tooltip="Permissions on when players are able to release all remaining items from their world.">
(?)
</span>
</label>
</td>
<td>
<select name="forfeit_mode" id="forfeit_mode">
<select name="release_mode" id="release_mode">
<option value="auto">Automatic on goal completion</option>
<option value="goal">Allow !forfeit after goal completion</option>
<option value="goal">Allow !release after goal completion</option>
<option value="auto-enabled">
Automatic on goal completion and manual !forfeit
Automatic on goal completion and manual !release
</option>
<option value="enabled">Manual !forfeit</option>
<option value="enabled">Manual !release</option>
<option value="disabled">Disabled</option>
</select>
</td>
@@ -62,7 +62,7 @@
<tr>
<td>
<label for="collect_mode">Collect Permission:
<span class="interactive" data-tooltip="A collect releases all of your remaining items to you from across the multiworld.">
<span class="interactive" data-tooltip="Permissions on when players are able to collect all their remaining items from across the multiworld.">
(?)
</span>
</label>

View File

@@ -31,14 +31,14 @@
<h2>Game Info Pages</h2>
<ul>
{% for game in games %}
{% for game in games | title_sorted %}
<li><a href="{{ url_for('game_info', game=game, lang='en') }}">{{ game }}</a></li>
{% endfor %}
</ul>
<h2>Game Settings Pages</h2>
<ul>
{% for game in games %}
{% for game in games | title_sorted %}
<li><a href="{{ url_for('player_settings', game=game) }}">{{ game }}</a></li>
{% endfor %}
</ul>

View File

@@ -650,7 +650,7 @@ def __renderOoTTracker(multisave: Dict[str, Any], room: Room, locations: Dict[in
# Gather dungeon locations
area_id_ranges = {
"Overworld": ((67000, 67258), (67264, 67280), (67747, 68024), (68054, 68062)),
"Overworld": ((67000, 67263), (67269, 67280), (67747, 68024), (68054, 68062)),
"Deku Tree": ((67281, 67303), (68063, 68077)),
"Dodongo's Cavern": ((67304, 67334), (68078, 68160)),
"Jabu Jabu's Belly": ((67335, 67359), (68161, 68188)),
@@ -662,7 +662,7 @@ def __renderOoTTracker(multisave: Dict[str, Any], room: Room, locations: Dict[in
"Spirit Temple": ((67533, 67582), (68566, 68625)),
"Ice Cavern": ((67583, 67596), (68626, 68649)),
"Gerudo Training Ground": ((67597, 67635), (68650, 68656)),
"Thieves' Hideout": ((67259, 67263), (68025, 68053)),
"Thieves' Hideout": ((67264, 67268), (68025, 68053)),
"Ganon's Castle": ((67636, 67673), (68657, 68705)),
}

View File

@@ -48,6 +48,9 @@ class ZillionContext(CommonContext):
command_processor: Type[ClientCommandProcessor] = ZillionCommandProcessor
items_handling = 1 # receive items from other players
known_name: Optional[str]
""" This is almost the same as `auth` except `auth` is reset to `None` when server disconnects, and this isn't. """
from_game: "asyncio.Queue[events.EventFromGame]"
to_game: "asyncio.Queue[events.EventToGame]"
ap_local_count: int
@@ -82,6 +85,7 @@ class ZillionContext(CommonContext):
server_address: str,
password: str) -> None:
super().__init__(server_address, password)
self.known_name = None
self.from_game = asyncio.Queue()
self.to_game = asyncio.Queue()
self.got_room_info = asyncio.Event()
@@ -396,7 +400,8 @@ async def zillion_sync_task(ctx: ZillionContext) -> None:
game_id = memory.get_rom_to_ram_data(ram)
name, seed_end = name_seed_from_ram(game_id)
if len(name):
if name == ctx.auth:
if name == ctx.known_name:
ctx.auth = name
# this is the name we know
if ctx.server and ctx.server.socket: # type: ignore
if ctx.got_room_info.is_set():
@@ -439,6 +444,7 @@ async def zillion_sync_task(ctx: ZillionContext) -> None:
memory.reset_game_state()
ctx.auth = name
ctx.known_name = name
async_start(ctx.connect())
await asyncio.wait((
ctx.got_room_info.wait(),

View File

@@ -1,5 +1,5 @@
<TabbedPanel>
tab_width: 200
tab_width: root.width / app.tab_count
<SelectableLabel>:
canvas.before:
Color:

View File

@@ -148,7 +148,7 @@ The next step is to know what you need to make the game do now that you can modi
- Listen for messages from the Archipelago server
- Modify the game to display messages from the Archipelago server
- Add interface for connecting to the Archipelago server with passwords and sessions
- Add commands for manually rewarding, re-syncing, forfeiting, and other actions
- Add commands for manually rewarding, re-syncing, releasing, and other actions
To elaborate, you need to be able to inform the server whenever you check locations, print out messages that you receive
from the server in-game so players can read them, award items when the server tells you to, sync and re-sync when necessary,

View File

@@ -8,4 +8,4 @@ We conduct ourselves openly and inclusively here. Please do not contribute to an
These guidelines apply to all channels of communication within this GitHub repository. Please be respectful in both public channels, such as issues, and private ones, such as private messaging or emails.
Any incidents of abuse may be reported directly to ijwu at hmfarran@gmail.com.
Any incidents of abuse may be reported directly to eudaimonistic at eudaimonistic42@gmail.com

View File

@@ -70,7 +70,7 @@ Sent to clients when they connect to an Archipelago server.
| version | [NetworkVersion](#NetworkVersion) | Object denoting the version of Archipelago which the server is running. |
| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. Example: `WebHost` |
| password | bool | Denoted whether a password is required to join this room.|
| permissions | dict\[str, [Permission](#Permission)\[int\]\] | Mapping of permission name to [Permission](#Permission), keys are: "forfeit", "collect" and "remaining". |
| permissions | dict\[str, [Permission](#Permission)\[int\]\] | Mapping of permission name to [Permission](#Permission), keys are: "release", "collect" and "remaining". |
| hint_cost | int | The amount of points it costs to receive a hint from the server. |
| location_check_points | int | The amount of hint points you receive per item/location check completed. ||
| games | list\[str\] | List of games present in this multiworld. |
@@ -78,14 +78,14 @@ Sent to clients when they connect to an Archipelago server.
| seed_name | str | uniquely identifying name of this generation |
| time | float | Unix time stamp of "now". Send for time synchronization if wanted for things like the DeathLink Bounce. |
#### forfeit
Dictates what is allowed when it comes to a player forfeiting their run. A forfeit is an action which distributes the rest of the items in a player's run to those other players awaiting them.
#### release
Dictates what is allowed when it comes to a player releasing their run. A release is an action which distributes the rest of the items in a player's run to those other players awaiting them.
* `auto`: Distributes a player's items to other players when they complete their goal.
* `enabled`: Denotes that players may forfeit at any time in the game.
* `enabled`: Denotes that players may release at any time in the game.
* `auto-enabled`: Both of the above options together.
* `disabled`: All forfeit modes disabled.
* `goal`: Allows for manual use of forfeit command once a player completes their goal. (Disabled until goal completion)
* `disabled`: All release modes disabled.
* `goal`: Allows for manual use of release command once a player completes their goal. (Disabled until goal completion)
#### collect
Dictates what is allowed when it comes to a player collecting their run. A collect is an action which sends the rest of the items in a player's run.
@@ -411,6 +411,9 @@ The following operations can be applied to a datastorage key
| xor | Applies a bitwise Exclusive OR to the current value of the key with `value`. |
| left_shift | Applies a bitwise left-shift to the current value of the key by `value`. |
| right_shift | Applies a bitwise right-shift to the current value of the key by `value`. |
| remove | List only: removes the first instance of `value` found in the list. |
| pop | List or Dict: for lists it will remove the index of the `value` given. for dicts it removes the element with the specified key of `value`. |
| update | Dict only: Updates the dictionary with the specified elements given in `value` creating new keys, or updating old ones if they previously existed. |
### SetNotify
Used to register your current session for receiving all [SetReply](#SetReply) packages of certain keys to allow your client to keep track of changes.
@@ -596,7 +599,7 @@ class Permission(enum.IntEnum):
disabled = 0b000 # 0, completely disables access
enabled = 0b001 # 1, allows manual use
goal = 0b010 # 2, allows manual use after goal completion
auto = 0b110 # 6, forces use after goal completion, only works for forfeit and collect
auto = 0b110 # 6, forces use after goal completion, only works for release and collect
auto_enabled = 0b111 # 7, forces use after goal completion, allows manual use any time
```

View File

@@ -22,14 +22,14 @@ server_options:
# Relative point cost to receive a hint via !hint for players
# so for example hint_cost: 20 would mean that for every 20% of available checks, you get the ability to hint, for a total of 5
hint_cost: 10 # Set to 0 if you want free hints
# Forfeit modes
# A Forfeit sends out the remaining items *from* a world that forfeits
# "disabled" -> clients can't forfeit,
# "enabled" -> clients can always forfeit
# "auto" -> automatic forfeit on goal completion
# "auto-enabled" -> automatic forfeit on goal completion and manual forfeit is also enabled
# "goal" -> forfeit is allowed after goal completion
forfeit_mode: "goal"
# Release modes
# A Release sends out the remaining items *from* a world that releases
# "disabled" -> clients can't release,
# "enabled" -> clients can always release
# "auto" -> automatic release on goal completion
# "auto-enabled" -> automatic release on goal completion and manual release is also enabled
# "goal" -> release is allowed after goal completion
release_mode: "goal"
# Collect modes
# A Collect sends the remaining items *to* a world that collects
# "disabled" -> clients can't collect,

16
kvui.py
View File

@@ -330,6 +330,12 @@ class GameManager(App):
super(GameManager, self).__init__()
@property
def tab_count(self):
if hasattr(self, "tabs"):
return max(1, len(self.tabs.tab_list))
return 1
def build(self) -> Layout:
self.container = ContainerLayout()
@@ -392,11 +398,13 @@ class GameManager(App):
Clock.schedule_interval(self.update_texts, 1 / 30)
self.container.add_widget(self.grid)
# If the address contains a port, select it; otherwise, select the host.
s = self.server_connect_bar.text
host_start = s.find("@") + 1
ipv6_end = s.find("]", host_start) + 1
port_start = s.find(":", ipv6_end if ipv6_end > 0 else host_start) + 1
self.server_connect_bar.focus = True
self.server_connect_bar.select_text(
self.server_connect_bar.text.find(":") + 1,
len(self.server_connect_bar.text)
)
self.server_connect_bar.select_text(port_start if port_start > 0 else host_start, len(s))
return self.container

View File

@@ -27,17 +27,62 @@ game: # Pick a game to play
A Link to the Past: 1
requires:
version: 0.3.3 # Version of Archipelago required for this yaml to work as expected.
# Shared Options supported by all games:
accessibility:
items: 0 # Guarantees you will be able to acquire all items, but you may not be able to access all locations
locations: 50 # Guarantees you will be able to access all locations, and therefore all items
none: 0 # Guarantees only that the game is beatable. You may not be able to access all locations or acquire all items
progression_balancing: # A system to reduce BK, as in times during which you can't do anything, by moving your items into an earlier access sphere
0: 0 # Choose a lower number if you don't mind a longer multiworld, or can glitch/sequence break around missing items.
25: 0
50: 50 # Make it likely you have stuff to do.
99: 0 # Get important items early, and stay at the front of the progression.
A Link to the Past:
progression_balancing:
# A system that can move progression earlier, to try and prevent the player from getting stuck and bored early.
# A lower setting means more getting stuck. A higher setting means less getting stuck.
#
# You can define additional values between the minimum and maximum values.
# Minimum value is 0
# Maximum value is 99
random: 0
random-low: 0
random-high: 0
disabled: 0 # equivalent to 0
normal: 50 # equivalent to 50
extreme: 0 # equivalent to 99
accessibility:
# Set rules for reachability of your items/locations.
# Locations: ensure everything can be reached and acquired.
# Items: ensure all logically relevant items can be acquired.
# Minimal: ensure what is needed to reach your goal can be acquired.
locations: 0
items: 50
minimal: 0
local_items:
# Forces these items to be in their native world.
[ ]
non_local_items:
# Forces these items to be outside their native world.
[ ]
start_inventory:
# Start with these items.
{ }
start_hints:
# Start with these item's locations prefilled into the !hint command.
[ ]
start_location_hints:
# Start with these locations and their item prefilled into the !hint command
[ ]
exclude_locations:
# Prevent these locations from having an important item
[ ]
priority_locations:
# Prevent these locations from having an unimportant item
[ ]
item_links:
# Share part of your item pool with other players.
[ ]
### Logic Section ###
glitches_required: # Determine the logic required to complete the seed
none: 50 # No glitches required

View File

@@ -11,7 +11,10 @@ from collections.abc import Iterable
from hashlib import sha3_512
from pathlib import Path
import setuptools
if __name__ == "__main__":
import ModuleUpdate
ModuleUpdate.update()
from Launcher import components, icon_paths
from Utils import version_tuple, is_windows, is_linux
@@ -26,7 +29,7 @@ apworlds: set = {
# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it
import subprocess
import pkg_resources
requirement = 'cx-Freeze>=6.13.1'
requirement = 'cx-Freeze>=6.14.1'
try:
pkg_resources.require(requirement)
import cx_Freeze
@@ -36,12 +39,15 @@ except pkg_resources.ResolutionError:
subprocess.call([sys.executable, '-m', 'pip', 'install', requirement, '--upgrade'])
import cx_Freeze
# .build only exists if cx-Freeze is the right version, so we have to update/install that first before this line
import setuptools.command.build
if os.path.exists("X:/pw.txt"):
print("Using signtool")
with open("X:/pw.txt", encoding="utf-8-sig") as f:
pw = f.read()
signtool = r'signtool sign /f X:/_SITS_Zertifikat_.pfx /p "' + pw + r'" /fd sha256 /tr http://timestamp.digicert.com/ '
signtool = r'signtool sign /f X:/_SITS_Zertifikat_.pfx /p "' + pw + \
r'" /fd sha256 /tr http://timestamp.digicert.com/ '
else:
signtool = None
@@ -64,6 +70,7 @@ exes = [
]
extra_data = ["LICENSE", "data", "EnemizerCLI", "host.yaml", "SNI"]
extra_libs = ["libssl.so", "libcrypto.so"] if is_linux else []
def remove_sprites_from_folder(folder):
@@ -79,7 +86,7 @@ def _threaded_hash(filepath):
# cx_Freeze's build command runs other commands. Override to accept --yes and store that.
class BuildCommand(cx_Freeze.command.build.Build):
class BuildCommand(setuptools.command.build.build):
user_options = [
('yes', 'y', 'Answer "yes" to all questions.'),
]
@@ -103,6 +110,7 @@ class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE):
]
yes: bool
extra_data: Iterable # [any] not available in 3.8
extra_libs: Iterable # work around broken include_files
buildfolder: Path
libfolder: Path
@@ -113,6 +121,7 @@ class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE):
super().initialize_options()
self.yes = BuildCommand.last_yes
self.extra_data = []
self.extra_libs = []
def finalize_options(self):
super().finalize_options()
@@ -169,17 +178,22 @@ class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE):
self.buildtime = datetime.datetime.utcnow()
super().run()
# include_files seems to be broken with this setup. implement here
# include_files seems to not be done automatically. implement here
for src, dst in self.include_files:
print('copying', src, '->', self.buildfolder / dst)
print(f"copying {src} -> {self.buildfolder / dst}")
shutil.copyfile(src, self.buildfolder / dst, follow_symlinks=False)
# now that include_files is completely broken, run find_libs here
for src, dst in find_libs(*self.extra_libs):
print(f"copying {src} -> {self.buildfolder / dst}")
shutil.copyfile(src, self.buildfolder / dst, follow_symlinks=False)
# post build steps
if sys.platform == "win32": # kivy_deps is win32 only, linux picks them up automatically
if is_windows: # kivy_deps is win32 only, linux picks them up automatically
from kivy_deps import sdl2, glew
for folder in sdl2.dep_bins + glew.dep_bins:
shutil.copytree(folder, self.libfolder, dirs_exist_ok=True)
print('copying', folder, '->', self.libfolder)
print(f"copying {folder} -> {self.libfolder}")
for data in self.extra_data:
self.installfile(Path(data))
@@ -380,6 +394,9 @@ $APPDIR/$exe "$@"
def find_libs(*args: str) -> typing.Sequence[typing.Tuple[str, str]]:
"""Try to find system libraries to be included."""
if not args:
return []
arch = build_arch.replace('_', '-')
libc = 'libc6' # we currently don't support musl
@@ -446,12 +463,13 @@ cx_Freeze.setup(
"pandas"],
"zip_include_packages": ["*"],
"zip_exclude_packages": ["worlds", "sc2"],
"include_files": find_libs("libssl.so", "libcrypto.so") if is_linux else [],
"include_files": [], # broken in cx 6.14.0, we use more special sauce now
"include_msvcr": False,
"replace_paths": [("*", "")],
"replace_paths": ["*."],
"optimize": 1,
"build_exe": buildfolder,
"extra_data": extra_data,
"extra_libs": extra_libs,
"bin_includes": ["libffi.so", "libcrypt.so"] if is_linux else []
},
"bdist_appimage": {

View File

@@ -169,6 +169,9 @@ class WorldTestBase(unittest.TestCase):
def can_reach_location(self, location: str) -> bool:
return self.multiworld.state.can_reach(location, "Location", 1)
def can_reach_entrance(self, entrance: str) -> bool:
return self.multiworld.state.can_reach(entrance, "Entrance", 1)
def count(self, item_name: str) -> int:
return self.multiworld.state.count(item_name, 1)

View File

@@ -33,6 +33,14 @@ class TestBase(unittest.TestCase):
for item in items:
self.assertIn(item, world_type.item_name_to_id)
def testItemNameGroupConflict(self):
"""Test that all item name groups aren't also item names."""
for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game_name, game_name=game_name):
for group_name in world_type.item_name_groups:
with self.subTest(group_name, group_name=group_name):
self.assertNotIn(group_name, world_type.item_name_to_id)
def testItemCountGreaterEqualLocations(self):
for game_name, world_type in AutoWorldRegister.world_types.items():

View File

@@ -0,0 +1,16 @@
import unittest
from collections import Counter
from worlds.AutoWorld import AutoWorldRegister
from . import setup_default_world
class TestBase(unittest.TestCase):
def testCreateDuplicateLocations(self):
for game_name, world_type in AutoWorldRegister.world_types.items():
if game_name in {"Final Fantasy"}:
continue
multiworld = setup_default_world(world_type)
locations = Counter(multiworld.get_locations())
if locations:
self.assertLessEqual(locations.most_common(1)[0][1], 1,
f"{world_type.game} has duplicate of location {locations.most_common(1)}")

View File

@@ -11,10 +11,11 @@ class TestDocs(unittest.TestCase):
"filename": ":memory:",
"create_db": True,
}
app = get_app()
app.config.update({
raw_app.config.update({
"TESTING": True,
})
app = get_app()
cls.client = app.test_client()
def testCorrectErrorEmptyRequest(self):

View File

@@ -109,10 +109,10 @@ def call_stage(multiworld: "MultiWorld", method_name: str, *args: Any) -> None:
class WebWorld:
"""Webhost integration"""
settings_page: Union[bool, str] = True
"""display a settings page. Can be a link to a specific page or external tool."""
game_info_languages: List[str] = ['en']
"""docs folder will be scanned for game info pages using this list in the format '{language}_{game_name}.md'"""
@@ -160,10 +160,6 @@ class World(metaclass=AutoWorldRegister):
hint_blacklist: ClassVar[FrozenSet[str]] = frozenset() # any names that should not be hintable
# For games where after a victory it is impossible to go back in and get additional/remaining Locations checked.
# this forces forfeit: auto for those games.
forced_auto_forfeit: bool = False
# Hide World Type from various views. Does not remove functionality.
hidden: ClassVar[bool] = False

View File

@@ -322,7 +322,7 @@ location_table_misc = {'Bottle Merchant': (0x3c9, 0x2),
location_table_misc_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_misc.items()}
async def track_locations(ctx, roomid, roomdata):
async def track_locations(ctx, roomid, roomdata) -> bool:
from SNIClient import snes_read, snes_buffered_write, snes_flush_writes
new_locations = []
@@ -451,10 +451,126 @@ async def track_locations(ctx, roomid, roomdata):
if misc_data_changed:
snes_buffered_write(ctx, SAVEDATA_START + 0x3c6, bytes(misc_data))
if new_locations:
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": new_locations}])
# verify rom is still the same:
rom_name = await snes_read(ctx, ROMNAME_START, ROMNAME_SIZE)
if rom_name is None or all(byte == b"\x00" for byte in rom_name) or rom_name[:2] != b"AP" or \
rom_name != ctx.rom:
snes_logger.info(f"Discarding recent {len(new_locations)} checks as ROM Status has changed.")
return False
else:
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": new_locations}])
await snes_flush_writes(ctx)
return True
class ALTTPSNIClient(SNIClient):
game = "A Link to the Past"
async def deathlink_kill_player(self, ctx):
from SNIClient import DeathState, snes_read, snes_buffered_write, snes_flush_writes
invincible = await snes_read(ctx, WRAM_START + 0x037B, 1)
last_health = await snes_read(ctx, WRAM_START + 0xF36D, 1)
await asyncio.sleep(0.25)
health = await snes_read(ctx, WRAM_START + 0xF36D, 1)
if not invincible or not last_health or not health:
ctx.death_state = DeathState.dead
ctx.last_death_link = time.time()
return
if not invincible[0] and last_health[0] == health[0]:
snes_buffered_write(ctx, WRAM_START + 0xF36D, bytes([0])) # set current health to 0
snes_buffered_write(ctx, WRAM_START + 0x0373,
bytes([8])) # deal 1 full heart of damage at next opportunity
await snes_flush_writes(ctx)
await asyncio.sleep(1)
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
if not gamemode or gamemode[0] in DEATH_MODES:
ctx.death_state = DeathState.dead
async def validate_rom(self, ctx) -> bool:
from SNIClient import snes_read
rom_name = await snes_read(ctx, ROMNAME_START, ROMNAME_SIZE)
if rom_name is None or all(byte == b"\x00" for byte in rom_name) or rom_name[:2] != b"AP":
return False
ctx.game = self.game
ctx.items_handling = 0b001 # full local
ctx.rom = rom_name
death_link = await snes_read(ctx, DEATH_LINK_ACTIVE_ADDR, 1)
if death_link:
ctx.allow_collect = bool(death_link[0] & 0b100)
ctx.death_link_allow_survive = bool(death_link[0] & 0b10)
await ctx.update_death_link(bool(death_link[0] & 0b1))
return True
async def game_watcher(self, ctx):
from SNIClient import snes_read, snes_buffered_write, snes_flush_writes
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
if "DeathLink" in ctx.tags and gamemode and ctx.last_death_link + 1 < time.time():
currently_dead = gamemode[0] in DEATH_MODES
await ctx.handle_deathlink_state(currently_dead)
gameend = await snes_read(ctx, SAVEDATA_START + 0x443, 1)
game_timer = await snes_read(ctx, SAVEDATA_START + 0x42E, 4)
if gamemode is None or gameend is None or game_timer is None or \
(gamemode[0] not in INGAME_MODES and gamemode[0] not in ENDGAME_MODES):
return
if gameend[0]:
if not ctx.finished_game:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
if gamemode in ENDGAME_MODES: # triforce room and credits
return
data = await snes_read(ctx, RECV_PROGRESS_ADDR, 8)
if data is None:
return
recv_index = data[0] | (data[1] << 8)
recv_item = data[2]
roomid = data[4] | (data[5] << 8)
roomdata = data[6]
scout_location = data[7]
if recv_index < len(ctx.items_received) and recv_item == 0:
item = ctx.items_received[recv_index]
recv_index += 1
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
color(ctx.item_names[item.item], 'red', 'bold'),
color(ctx.player_names[item.player], 'yellow'),
ctx.location_names[item.location], recv_index, len(ctx.items_received)))
snes_buffered_write(ctx, RECV_PROGRESS_ADDR,
bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
snes_buffered_write(ctx, RECV_ITEM_ADDR,
bytes([item.item]))
snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR,
bytes([min(ROM_PLAYER_LIMIT, item.player) if item.player != ctx.slot else 0]))
if scout_location > 0 and scout_location in ctx.locations_info:
snes_buffered_write(ctx, SCOUTREPLY_LOCATION_ADDR,
bytes([scout_location]))
snes_buffered_write(ctx, SCOUTREPLY_ITEM_ADDR,
bytes([ctx.locations_info[scout_location].item]))
snes_buffered_write(ctx, SCOUTREPLY_PLAYER_ADDR,
bytes([min(ROM_PLAYER_LIMIT, ctx.locations_info[scout_location].player)]))
await snes_flush_writes(ctx)
if scout_location > 0 and scout_location not in ctx.locations_scouted:
ctx.locations_scouted.add(scout_location)
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": [scout_location]}])
same_rom = await track_locations(ctx, roomid, roomdata)
if not same_rom:
return
def get_alttp_settings(romfile: str):
@@ -582,112 +698,3 @@ def get_alttp_settings(romfile: str):
else:
adjusted = False
return adjustedromfile, adjusted
class ALTTPSNIClient(SNIClient):
game = "A Link to the Past"
async def deathlink_kill_player(self, ctx):
from SNIClient import DeathState, snes_read, snes_buffered_write, snes_flush_writes
invincible = await snes_read(ctx, WRAM_START + 0x037B, 1)
last_health = await snes_read(ctx, WRAM_START + 0xF36D, 1)
await asyncio.sleep(0.25)
health = await snes_read(ctx, WRAM_START + 0xF36D, 1)
if not invincible or not last_health or not health:
ctx.death_state = DeathState.dead
ctx.last_death_link = time.time()
return
if not invincible[0] and last_health[0] == health[0]:
snes_buffered_write(ctx, WRAM_START + 0xF36D, bytes([0])) # set current health to 0
snes_buffered_write(ctx, WRAM_START + 0x0373,
bytes([8])) # deal 1 full heart of damage at next opportunity
await snes_flush_writes(ctx)
await asyncio.sleep(1)
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
if not gamemode or gamemode[0] in DEATH_MODES:
ctx.death_state = DeathState.dead
async def validate_rom(self, ctx):
from SNIClient import snes_read, snes_buffered_write, snes_flush_writes
rom_name = await snes_read(ctx, ROMNAME_START, ROMNAME_SIZE)
if rom_name is None or rom_name == bytes([0] * ROMNAME_SIZE) or rom_name[:2] != b"AP":
return False
ctx.game = self.game
ctx.items_handling = 0b001 # full local
ctx.rom = rom_name
death_link = await snes_read(ctx, DEATH_LINK_ACTIVE_ADDR, 1)
if death_link:
ctx.allow_collect = bool(death_link[0] & 0b100)
ctx.death_link_allow_survive = bool(death_link[0] & 0b10)
await ctx.update_death_link(bool(death_link[0] & 0b1))
return True
async def game_watcher(self, ctx):
from SNIClient import snes_read, snes_buffered_write, snes_flush_writes
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
if "DeathLink" in ctx.tags and gamemode and ctx.last_death_link + 1 < time.time():
currently_dead = gamemode[0] in DEATH_MODES
await ctx.handle_deathlink_state(currently_dead)
gameend = await snes_read(ctx, SAVEDATA_START + 0x443, 1)
game_timer = await snes_read(ctx, SAVEDATA_START + 0x42E, 4)
if gamemode is None or gameend is None or game_timer is None or \
(gamemode[0] not in INGAME_MODES and gamemode[0] not in ENDGAME_MODES):
return
if gameend[0]:
if not ctx.finished_game:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
if gamemode in ENDGAME_MODES: # triforce room and credits
return
data = await snes_read(ctx, RECV_PROGRESS_ADDR, 8)
if data is None:
return
recv_index = data[0] | (data[1] << 8)
recv_item = data[2]
roomid = data[4] | (data[5] << 8)
roomdata = data[6]
scout_location = data[7]
if recv_index < len(ctx.items_received) and recv_item == 0:
item = ctx.items_received[recv_index]
recv_index += 1
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
color(ctx.item_names[item.item], 'red', 'bold'),
color(ctx.player_names[item.player], 'yellow'),
ctx.location_names[item.location], recv_index, len(ctx.items_received)))
snes_buffered_write(ctx, RECV_PROGRESS_ADDR,
bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
snes_buffered_write(ctx, RECV_ITEM_ADDR,
bytes([item.item]))
snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR,
bytes([min(ROM_PLAYER_LIMIT, item.player) if item.player != ctx.slot else 0]))
if scout_location > 0 and scout_location in ctx.locations_info:
snes_buffered_write(ctx, SCOUTREPLY_LOCATION_ADDR,
bytes([scout_location]))
snes_buffered_write(ctx, SCOUTREPLY_ITEM_ADDR,
bytes([ctx.locations_info[scout_location].item]))
snes_buffered_write(ctx, SCOUTREPLY_PLAYER_ADDR,
bytes([min(ROM_PLAYER_LIMIT, ctx.locations_info[scout_location].player)]))
await snes_flush_writes(ctx)
if scout_location > 0 and scout_location not in ctx.locations_scouted:
ctx.locations_scouted.add(scout_location)
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": [scout_location]}])
await track_locations(ctx, roomid, roomdata)

View File

@@ -3,7 +3,7 @@ import logging
from BaseClasses import Region, RegionType, ItemClassification
from worlds.alttp.SubClasses import ALttPLocation
from worlds.alttp.Shops import TakeAny, total_shop_slots, set_up_shops, shuffle_shops
from worlds.alttp.Shops import TakeAny, total_shop_slots, set_up_shops, shuffle_shops, create_dynamic_shop_locations
from worlds.alttp.Bosses import place_bosses
from worlds.alttp.Dungeons import get_dungeon_item_pool_player
from worlds.alttp.EntranceShuffle import connect_entrance
@@ -436,12 +436,13 @@ def generate_itempool(world):
if world.shop_shuffle[player]:
shuffle_shops(world, nonprogressionitems, player)
create_dynamic_shop_locations(world, player)
world.itempool += progressionitems + nonprogressionitems
if world.retro_caves[player]:
set_up_take_anys(world, player) # depends on world.itempool to be set
# set_up_take_anys needs to run first
create_dynamic_shop_locations(world, player)
take_any_locations = {
@@ -487,7 +488,7 @@ def set_up_take_anys(world, player):
world.itempool.append(ItemFactory('Rupees (20)', player))
old_man_take_any.shop.add_inventory(0, sword.name, 0, 0, create_location=True)
else:
old_man_take_any.shop.add_inventory(0, 'Rupees (300)', 0, 0)
old_man_take_any.shop.add_inventory(0, 'Rupees (300)', 0, 0, create_location=True)
for num in range(4):
take_any = Region("Take-Any #{}".format(num+1), RegionType.Cave, 'a cave of choice', player)
@@ -501,29 +502,11 @@ def set_up_take_anys(world, player):
take_any.shop = TakeAny(take_any, room_id, 0xE3, True, True, total_shop_slots + num + 1)
world.shops.append(take_any.shop)
take_any.shop.add_inventory(0, 'Blue Potion', 0, 0)
take_any.shop.add_inventory(1, 'Boss Heart Container', 0, 0)
take_any.shop.add_inventory(1, 'Boss Heart Container', 0, 0, create_location=True)
world.initialize_regions()
def create_dynamic_shop_locations(world, player):
for shop in world.shops:
if shop.region.player == player:
for i, item in enumerate(shop.inventory):
if item is None:
continue
if item['create_location']:
loc = ALttPLocation(player, f"{shop.region.name} {shop.slot_names[i]}", parent=shop.region)
shop.region.locations.append(loc)
world.clear_location_cache()
world.push_item(loc, ItemFactory(item['item'], player), False)
loc.shop_slot = i
loc.event = True
loc.locked = True
def get_pool_core(world, player: int):
shuffle = world.shuffle[player]
difficulty = world.difficulty[player]

View File

@@ -20,7 +20,7 @@ def GetBeemizerItem(world, player: int, item):
# should be replaced with direct world.create_item(item) call in the future
def ItemFactory(items, player: int):
def ItemFactory(items: typing.Union[str, typing.Iterable[str]], player: int):
from worlds.alttp import ALTTPWorld
world = ALTTPWorld(None, player)
ret = []

View File

@@ -686,142 +686,296 @@ lookup_name_to_id = {name: data[0] for name, data in location_table.items() if t
lookup_name_to_id = {**lookup_name_to_id, **{name: data[1] for name, data in key_drop_data.items()}}
lookup_name_to_id.update(shop_table_by_location)
lookup_vanilla_location_to_entrance = {1572883: 'Kings Grave Inner Rocks', 191256: 'Kings Grave Inner Rocks',
1573194: 'Kings Grave Inner Rocks', 1573189: 'Kings Grave Inner Rocks',
212328: 'Kings Grave Inner Rocks', 60175: 'Blinds Hideout',
60178: 'Blinds Hideout', 60181: 'Blinds Hideout', 60184: 'Blinds Hideout',
60187: 'Blinds Hideout', 188229: 'Hyrule Castle Secret Entrance Drop',
59761: 'Hyrule Castle Secret Entrance Drop', 975299: 'Zoras River',
1573193: 'Zoras River', 59824: 'Waterfall of Wishing',
59857: 'Waterfall of Wishing', 59770: 'Kings Grave', 59788: 'Dam',
59836: 'Links House', 59854: 'Tavern North', 59881: 'Chicken House',
59890: 'Aginahs Cave', 60034: 'Sahasrahlas Hut', 60037: 'Sahasrahlas Hut',
60040: 'Sahasrahlas Hut', 193020: 'Sahasrahlas Hut',
60046: 'Kakariko Well Drop', 60049: 'Kakariko Well Drop',
60052: 'Kakariko Well Drop', 60055: 'Kakariko Well Drop',
60058: 'Kakariko Well Drop', 1572906: 'Blacksmiths Hut',
1572885: 'Bat Cave Drop', 211407: 'Sick Kids House',
212605: 'Hobo Bridge', 1572864: 'Lost Woods Hideout Drop',
1572865: 'Lumberjack Tree Tree', 1572867: 'Cave 45',
1572868: 'Graveyard Cave', 1572869: 'Checkerboard Cave',
60226: 'Mini Moldorm Cave', 60229: 'Mini Moldorm Cave',
60232: 'Mini Moldorm Cave', 60235: 'Mini Moldorm Cave',
1572880: 'Mini Moldorm Cave', 60238: 'Ice Rod Cave',
60223: 'Bonk Rock Cave', 1572882: 'Library', 1572884: 'Potion Shop',
1573188: 'Lake Hylia Island Mirror Spot',
1573186: 'Maze Race Mirror Spot', 1573187: 'Desert Ledge Return Rocks',
59791: 'Desert Palace Entrance (West)',
1573216: 'Desert Palace Entrance (West)',
59830: 'Desert Palace Entrance (West)',
59851: 'Desert Palace Entrance (West)',
59842: 'Desert Palace Entrance (West)',
1573201: 'Desert Palace Entrance (North)', 59767: 'Eastern Palace',
59773: 'Eastern Palace', 59827: 'Eastern Palace', 59833: 'Eastern Palace',
59893: 'Eastern Palace', 1573200: 'Eastern Palace',
166320: 'Master Sword Meadow', 59764: 'Hyrule Castle Entrance (South)',
60172: 'Hyrule Castle Entrance (South)',
60169: 'Hyrule Castle Entrance (South)',
59758: 'Hyrule Castle Entrance (South)',
60253: 'Hyrule Castle Entrance (South)',
60256: 'Hyrule Castle Entrance (South)',
60259: 'Hyrule Castle Entrance (South)', 60025: 'Sanctuary S&Q',
60085: 'Agahnims Tower', 60082: 'Agahnims Tower',
1010170: 'Old Man Cave (West)', 1572866: 'Spectacle Rock Cave',
60202: 'Paradox Cave (Bottom)', 60205: 'Paradox Cave (Bottom)',
60208: 'Paradox Cave (Bottom)', 60211: 'Paradox Cave (Bottom)',
60214: 'Paradox Cave (Bottom)', 60217: 'Paradox Cave (Bottom)',
60220: 'Paradox Cave (Bottom)', 59839: 'Spiral Cave',
1572886: 'Death Mountain (Top)', 1573184: 'Spectacle Rock Mirror Spot',
1573218: 'Tower of Hera', 59821: 'Tower of Hera', 59878: 'Tower of Hera',
59899: 'Tower of Hera', 59896: 'Tower of Hera', 1573202: 'Tower of Hera',
1573191: 'Top of Pyramid', 975237: 'Catfish Entrance Rock',
209095: 'South Dark World Bridge', 1573192: 'South Dark World Bridge',
1572887: 'Bombos Tablet Mirror Spot', 60190: 'Hype Cave',
60193: 'Hype Cave', 60196: 'Hype Cave', 60199: 'Hype Cave',
1572881: 'Hype Cave', 1572870: 'Dark World Hammer Peg Cave',
59776: 'Pyramid Fairy', 59779: 'Pyramid Fairy', 59884: 'Brewery',
59887: 'C-Shaped House', 60840: 'Chest Game',
1573190: 'Bumper Cave (Bottom)', 60019: 'Mire Shed', 60022: 'Mire Shed',
60028: 'Superbunny Cave (Top)', 60031: 'Superbunny Cave (Top)',
60043: 'Spike Cave', 60241: 'Hookshot Cave', 60244: 'Hookshot Cave',
60250: 'Hookshot Cave', 60247: 'Hookshot Cave',
1573185: 'Floating Island Mirror Spot', 59845: 'Mimic Cave',
60061: 'Swamp Palace', 59782: 'Swamp Palace', 59785: 'Swamp Palace',
60064: 'Swamp Palace', 60070: 'Swamp Palace', 60067: 'Swamp Palace',
60073: 'Swamp Palace', 60076: 'Swamp Palace', 60079: 'Swamp Palace',
1573204: 'Swamp Palace', 59908: 'Thieves Town', 59905: 'Thieves Town',
59911: 'Thieves Town', 59914: 'Thieves Town', 59917: 'Thieves Town',
59920: 'Thieves Town', 59923: 'Thieves Town', 1573206: 'Thieves Town',
59803: 'Skull Woods First Section Door',
59848: 'Skull Woods First Section Hole (East)',
59794: 'Skull Woods First Section Hole (West)',
59809: 'Skull Woods First Section Hole (West)',
59800: 'Skull Woods First Section Hole (North)',
59806: 'Skull Woods Second Section Door (East)',
59902: 'Skull Woods Final Section', 1573205: 'Skull Woods Final Section',
59860: 'Ice Palace', 59797: 'Ice Palace', 59818: 'Ice Palace',
59875: 'Ice Palace', 59872: 'Ice Palace', 59812: 'Ice Palace',
59869: 'Ice Palace', 1573207: 'Ice Palace', 60007: 'Misery Mire',
60010: 'Misery Mire', 59998: 'Misery Mire', 60001: 'Misery Mire',
59866: 'Misery Mire', 60004: 'Misery Mire', 60013: 'Misery Mire',
1573208: 'Misery Mire', 59938: 'Turtle Rock', 59932: 'Turtle Rock',
59935: 'Turtle Rock', 59926: 'Turtle Rock',
59941: 'Dark Death Mountain Ledge (West)',
59929: 'Dark Death Mountain Ledge (East)',
59956: 'Dark Death Mountain Ledge (West)',
59953: 'Turtle Rock Isolated Ledge Entrance',
59950: 'Turtle Rock Isolated Ledge Entrance',
59947: 'Turtle Rock Isolated Ledge Entrance',
59944: 'Turtle Rock Isolated Ledge Entrance',
1573209: 'Turtle Rock Isolated Ledge Entrance',
59995: 'Palace of Darkness', 59965: 'Palace of Darkness',
59977: 'Palace of Darkness', 59959: 'Palace of Darkness',
59962: 'Palace of Darkness', 59986: 'Palace of Darkness',
59971: 'Palace of Darkness', 59980: 'Palace of Darkness',
59983: 'Palace of Darkness', 59989: 'Palace of Darkness',
59992: 'Palace of Darkness', 59968: 'Palace of Darkness',
59974: 'Palace of Darkness', 1573203: 'Palace of Darkness',
1573217: 'Ganons Tower', 60121: 'Ganons Tower', 60124: 'Ganons Tower',
60130: 'Ganons Tower', 60133: 'Ganons Tower', 60136: 'Ganons Tower',
60139: 'Ganons Tower', 60142: 'Ganons Tower', 60088: 'Ganons Tower',
60091: 'Ganons Tower', 60094: 'Ganons Tower', 60097: 'Ganons Tower',
60115: 'Ganons Tower', 60112: 'Ganons Tower', 60100: 'Ganons Tower',
60103: 'Ganons Tower', 60106: 'Ganons Tower', 60109: 'Ganons Tower',
60127: 'Ganons Tower', 60118: 'Ganons Tower', 60148: 'Ganons Tower',
60151: 'Ganons Tower', 60145: 'Ganons Tower', 60157: 'Ganons Tower',
60160: 'Ganons Tower', 60163: 'Ganons Tower', 60166: 'Ganons Tower',
0x140037: 'Hyrule Castle Entrance (South)',
0x140034: 'Hyrule Castle Entrance (South)',
0x14000d: 'Hyrule Castle Entrance (South)',
0x14003d: 'Hyrule Castle Entrance (South)',
0x14005b: 'Eastern Palace', 0x140049: 'Eastern Palace',
0x140031: 'Desert Palace Entrance (North)',
0x14002b: 'Desert Palace Entrance (North)',
0x140028: 'Desert Palace Entrance (North)',
0x140061: 'Agahnims Tower', 0x140052: 'Agahnims Tower',
0x140019: 'Swamp Palace', 0x140016: 'Swamp Palace', 0x140013: 'Swamp Palace',
0x140010: 'Swamp Palace', 0x14000a: 'Swamp Palace',
0x14002e: 'Skull Woods Second Section Door (East)',
0x14001c: 'Skull Woods Final Section',
0x14005e: 'Thieves Town', 0x14004f: 'Thieves Town',
0x140004: 'Ice Palace', 0x140022: 'Ice Palace',
0x140025: 'Ice Palace', 0x140046: 'Ice Palace',
0x140055: 'Misery Mire', 0x14004c: 'Misery Mire',
0x140064: 'Misery Mire',
0x140058: 'Turtle Rock', 0x140007: 'Dark Death Mountain Ledge (West)',
0x140040: 'Ganons Tower', 0x140043: 'Ganons Tower',
0x14003a: 'Ganons Tower', 0x14001f: 'Ganons Tower',
0x400000: 'Cave Shop (Dark Death Mountain)', 0x400001: 'Cave Shop (Dark Death Mountain)', 0x400002: 'Cave Shop (Dark Death Mountain)',
0x400003: 'Red Shield Shop', 0x400004: 'Red Shield Shop', 0x400005: 'Red Shield Shop',
0x400006: 'Dark Lake Hylia Shop', 0x400007: 'Dark Lake Hylia Shop', 0x400008: 'Dark Lake Hylia Shop',
0x400009: 'Dark World Lumberjack Shop', 0x40000a: 'Dark World Lumberjack Shop', 0x40000b: 'Dark World Lumberjack Shop',
0x40000c: 'Village of Outcasts Shop', 0x40000d: 'Village of Outcasts Shop', 0x40000e: 'Village of Outcasts Shop',
0x40000f: 'Dark World Potion Shop', 0x400010: 'Dark World Potion Shop', 0x400011: 'Dark World Potion Shop',
0x400012: 'Light World Death Mountain Shop', 0x400013: 'Light World Death Mountain Shop', 0x400014: 'Light World Death Mountain Shop',
0x400015: 'Kakariko Shop', 0x400016: 'Kakariko Shop', 0x400017: 'Kakariko Shop',
0x400018: 'Cave Shop (Lake Hylia)', 0x400019: 'Cave Shop (Lake Hylia)', 0x40001a: 'Cave Shop (Lake Hylia)',
0x40001b: 'Potion Shop', 0x40001c: 'Potion Shop', 0x40001d: 'Potion Shop',
0x40001e: 'Capacity Upgrade', 0x40001f: 'Capacity Upgrade', 0x400020: 'Capacity Upgrade'}
lookup_vanilla_location_to_entrance = {
59758: 'Hyrule Castle Entrance (South)',
59761: 'Hyrule Castle Secret Entrance Drop',
59764: 'Hyrule Castle Entrance (South)',
59767: 'Eastern Palace',
59770: 'Kings Grave',
59773: 'Eastern Palace',
59776: 'Pyramid Fairy',
59779: 'Pyramid Fairy',
59782: 'Swamp Palace',
59785: 'Swamp Palace',
59788: 'Dam',
59791: 'Desert Palace Entrance (West)',
59794: 'Skull Woods First Section Hole (West)',
59797: 'Ice Palace',
59800: 'Skull Woods First Section Hole (North)',
59803: 'Skull Woods First Section Door',
59806: 'Skull Woods Second Section Door (East)',
59809: 'Skull Woods First Section Hole (West)',
59812: 'Ice Palace',
59818: 'Ice Palace',
59821: 'Tower of Hera',
59824: 'Waterfall of Wishing',
59827: 'Eastern Palace',
59830: 'Desert Palace Entrance (West)',
59833: 'Eastern Palace',
59836: 'Links House',
59839: 'Spiral Cave',
59842: 'Desert Palace Entrance (West)',
59845: 'Mimic Cave',
59848: 'Skull Woods First Section Hole (East)',
59851: 'Desert Palace Entrance (West)',
59854: 'Tavern North',
59857: 'Waterfall of Wishing',
59860: 'Ice Palace',
59866: 'Misery Mire',
59869: 'Ice Palace',
59872: 'Ice Palace',
59875: 'Ice Palace',
59878: 'Tower of Hera',
59881: 'Chicken House',
59884: 'Brewery',
59887: 'C-Shaped House',
59890: 'Aginahs Cave',
59893: 'Eastern Palace',
59896: 'Tower of Hera',
59899: 'Tower of Hera',
59902: 'Skull Woods Final Section',
59905: 'Thieves Town',
59908: 'Thieves Town',
59911: 'Thieves Town',
59914: 'Thieves Town',
59917: 'Thieves Town',
59920: 'Thieves Town',
59923: 'Thieves Town',
59926: 'Turtle Rock',
59929: 'Dark Death Mountain Ledge (East)',
59932: 'Turtle Rock',
59935: 'Turtle Rock',
59938: 'Turtle Rock',
59941: 'Dark Death Mountain Ledge (West)',
59944: 'Turtle Rock Isolated Ledge Entrance',
59947: 'Turtle Rock Isolated Ledge Entrance',
59950: 'Turtle Rock Isolated Ledge Entrance',
59953: 'Turtle Rock Isolated Ledge Entrance',
59956: 'Dark Death Mountain Ledge (West)',
59959: 'Palace of Darkness',
59962: 'Palace of Darkness',
59965: 'Palace of Darkness',
59968: 'Palace of Darkness',
59971: 'Palace of Darkness',
59974: 'Palace of Darkness',
59977: 'Palace of Darkness',
59980: 'Palace of Darkness',
59983: 'Palace of Darkness',
59986: 'Palace of Darkness',
59989: 'Palace of Darkness',
59992: 'Palace of Darkness',
59995: 'Palace of Darkness',
59998: 'Misery Mire',
60001: 'Misery Mire',
60004: 'Misery Mire',
60007: 'Misery Mire',
60010: 'Misery Mire',
60013: 'Misery Mire',
60019: 'Mire Shed',
60022: 'Mire Shed',
60025: 'Sanctuary S&Q',
60028: 'Superbunny Cave (Top)',
60031: 'Superbunny Cave (Top)',
60034: 'Sahasrahlas Hut',
60037: 'Sahasrahlas Hut',
60040: 'Sahasrahlas Hut',
60043: 'Spike Cave',
60046: 'Kakariko Well Drop',
60049: 'Kakariko Well Drop',
60052: 'Kakariko Well Drop',
60055: 'Kakariko Well Drop',
60058: 'Kakariko Well Drop',
60061: 'Swamp Palace',
60064: 'Swamp Palace',
60067: 'Swamp Palace',
60070: 'Swamp Palace',
60073: 'Swamp Palace',
60076: 'Swamp Palace',
60079: 'Swamp Palace',
60082: 'Agahnims Tower',
60085: 'Agahnims Tower',
60088: 'Ganons Tower',
60091: 'Ganons Tower',
60094: 'Ganons Tower',
60097: 'Ganons Tower',
60100: 'Ganons Tower',
60103: 'Ganons Tower',
60106: 'Ganons Tower',
60109: 'Ganons Tower',
60112: 'Ganons Tower',
60115: 'Ganons Tower',
60118: 'Ganons Tower',
60121: 'Ganons Tower',
60124: 'Ganons Tower',
60127: 'Ganons Tower',
60130: 'Ganons Tower',
60133: 'Ganons Tower',
60136: 'Ganons Tower',
60139: 'Ganons Tower',
60142: 'Ganons Tower',
60145: 'Ganons Tower',
60148: 'Ganons Tower',
60151: 'Ganons Tower',
60157: 'Ganons Tower',
60160: 'Ganons Tower',
60163: 'Ganons Tower',
60166: 'Ganons Tower',
60169: 'Hyrule Castle Entrance (South)',
60172: 'Hyrule Castle Entrance (South)',
60175: 'Blinds Hideout',
60178: 'Blinds Hideout',
60181: 'Blinds Hideout',
60184: 'Blinds Hideout',
60187: 'Blinds Hideout',
60190: 'Hype Cave',
60193: 'Hype Cave',
60196: 'Hype Cave',
60199: 'Hype Cave',
60202: 'Paradox Cave (Bottom)',
60205: 'Paradox Cave (Bottom)',
60208: 'Paradox Cave (Bottom)',
60211: 'Paradox Cave (Bottom)',
60214: 'Paradox Cave (Bottom)',
60217: 'Paradox Cave (Bottom)',
60220: 'Paradox Cave (Bottom)',
60223: 'Bonk Rock Cave',
60226: 'Mini Moldorm Cave',
60229: 'Mini Moldorm Cave',
60232: 'Mini Moldorm Cave',
60235: 'Mini Moldorm Cave',
60238: 'Ice Rod Cave',
60241: 'Hookshot Cave',
60244: 'Hookshot Cave',
60247: 'Hookshot Cave',
60250: 'Hookshot Cave',
60253: 'Hyrule Castle Entrance (South)',
60256: 'Hyrule Castle Entrance (South)',
60259: 'Hyrule Castle Entrance (South)',
60840: 'Chest Game',
166320: 'Master Sword Meadow',
188229: 'Hyrule Castle Secret Entrance Drop',
191256: 'Kings Grave Inner Rocks',
193020: 'Sahasrahlas Hut',
209095: 'South Dark World Bridge',
211407: 'Sick Kids House',
212328: 'Kings Grave Inner Rocks',
212605: 'Hobo Bridge',
975237: 'Catfish Entrance Rock',
975299: 'Zoras River',
1010170: 'Old Man Cave (West)',
1310724: 'Ice Palace',
1310727: 'Dark Death Mountain Ledge (West)',
1310730: 'Swamp Palace',
1310733: 'Hyrule Castle Entrance (South)',
1310736: 'Swamp Palace',
1310739: 'Swamp Palace',
1310742: 'Swamp Palace',
1310745: 'Swamp Palace',
1310748: 'Skull Woods Final Section',
1310751: 'Ganons Tower',
1310754: 'Ice Palace',
1310757: 'Ice Palace',
1310760: 'Desert Palace Entrance (North)',
1310763: 'Desert Palace Entrance (North)',
1310766: 'Skull Woods Second Section Door (East)',
1310769: 'Desert Palace Entrance (North)',
1310772: 'Hyrule Castle Entrance (South)',
1310775: 'Hyrule Castle Entrance (South)',
1310778: 'Ganons Tower',
1310781: 'Hyrule Castle Entrance (South)',
1310784: 'Ganons Tower',
1310787: 'Ganons Tower',
1310790: 'Ice Palace',
1310793: 'Eastern Palace',
1310796: 'Misery Mire',
1310799: 'Thieves Town',
1310802: 'Agahnims Tower',
1310805: 'Misery Mire',
1310808: 'Turtle Rock',
1310811: 'Eastern Palace',
1310814: 'Thieves Town',
1310817: 'Agahnims Tower',
1310820: 'Misery Mire',
1572864: 'Lost Woods Hideout Drop',
1572865: 'Lumberjack Tree Tree',
1572866: 'Spectacle Rock Cave',
1572867: 'Cave 45',
1572868: 'Graveyard Cave',
1572869: 'Checkerboard Cave',
1572870: 'Dark World Hammer Peg Cave',
1572880: 'Mini Moldorm Cave',
1572881: 'Hype Cave',
1572882: 'Library',
1572883: 'Kings Grave Inner Rocks',
1572884: 'Potion Shop',
1572885: 'Bat Cave Drop',
1572886: 'Death Mountain (Top)',
1572887: 'Bombos Tablet Mirror Spot',
1572906: 'Blacksmiths Hut',
1573184: 'Spectacle Rock Mirror Spot',
1573185: 'Floating Island Mirror Spot',
1573186: 'Maze Race Mirror Spot',
1573187: 'Desert Ledge Return Rocks',
1573188: 'Lake Hylia Island Mirror Spot',
1573189: 'Kings Grave Inner Rocks',
1573190: 'Bumper Cave (Bottom)',
1573191: 'Top of Pyramid',
1573192: 'South Dark World Bridge',
1573193: 'Zoras River',
1573194: 'Kings Grave Inner Rocks',
1573200: 'Eastern Palace',
1573201: 'Desert Palace Entrance (North)',
1573202: 'Tower of Hera',
1573203: 'Palace of Darkness',
1573204: 'Swamp Palace',
1573205: 'Skull Woods Final Section',
1573206: 'Thieves Town',
1573207: 'Ice Palace',
1573208: 'Misery Mire',
1573209: 'Turtle Rock Isolated Ledge Entrance',
1573216: 'Desert Palace Entrance (West)',
1573217: 'Ganons Tower',
1573218: 'Tower of Hera',
4194304: 'Cave Shop (Dark Death Mountain)',
4194305: 'Cave Shop (Dark Death Mountain)',
4194306: 'Cave Shop (Dark Death Mountain)',
4194307: 'Red Shield Shop',
4194308: 'Red Shield Shop',
4194309: 'Red Shield Shop',
4194310: 'Dark Lake Hylia Shop',
4194311: 'Dark Lake Hylia Shop',
4194312: 'Dark Lake Hylia Shop',
4194313: 'Dark World Lumberjack Shop',
4194314: 'Dark World Lumberjack Shop',
4194315: 'Dark World Lumberjack Shop',
4194316: 'Village of Outcasts Shop',
4194317: 'Village of Outcasts Shop',
4194318: 'Village of Outcasts Shop',
4194319: 'Dark World Potion Shop',
4194320: 'Dark World Potion Shop',
4194321: 'Dark World Potion Shop',
4194322: 'Light World Death Mountain Shop',
4194323: 'Light World Death Mountain Shop',
4194324: 'Light World Death Mountain Shop',
4194325: 'Kakariko Shop',
4194326: 'Kakariko Shop',
4194327: 'Kakariko Shop',
4194328: 'Cave Shop (Lake Hylia)',
4194329: 'Cave Shop (Lake Hylia)',
4194330: 'Cave Shop (Lake Hylia)',
4194331: 'Potion Shop',
4194332: 'Potion Shop',
4194333: 'Potion Shop',
4194334: 'Capacity Upgrade',
4194335: 'Capacity Upgrade',
4194336: 'Capacity Upgrade',
# have no vanilla entrance
4194337: "Old Man Sword Cave",
4194338: "Take-Any #1",
4194339: "Take-Any #2",
4194340: "Take-Any #3",
4194341: "Take-Any #4",
}
lookup_prizes = {location for location in location_table if location.endswith(" - Prize")}
lookup_boss_drops = {location for location in location_table if location.endswith(" - Boss")}

View File

@@ -2304,7 +2304,7 @@ def write_strings(rom, world, player):
'dungeonscrossed'] else 8
hint_count = min(hint_count, len(items_to_hint), len(hint_locations))
if hint_count:
locations = world.find_items_in_locations(items_to_hint, player)
locations = world.find_items_in_locations(items_to_hint, player, True)
local_random.shuffle(locations)
for x in range(min(hint_count, len(locations))):
this_location = locations.pop()
@@ -2321,7 +2321,7 @@ def write_strings(rom, world, player):
# We still need the older hints of course. Those are done here.
silverarrows = world.find_item_locations('Silver Bow', player)
silverarrows = world.find_item_locations('Silver Bow', player, True)
local_random.shuffle(silverarrows)
silverarrow_hint = (
' %s?' % hint_text(silverarrows[0]).replace('Ganon\'s', 'my')) if silverarrows else '?\nI think not!'
@@ -2329,13 +2329,13 @@ def write_strings(rom, world, player):
tt['ganon_phase_3_no_silvers_alt'] = 'Did you find the silver arrows%s' % silverarrow_hint
if world.worlds[player].has_progressive_bows and (world.difficulty_requirements[player].progressive_bow_limit >= 2 or (
world.swordless[player] or world.logic[player] == 'noglitches')):
prog_bow_locs = world.find_item_locations('Progressive Bow', player)
prog_bow_locs = world.find_item_locations('Progressive Bow', player, True)
world.slot_seeds[player].shuffle(prog_bow_locs)
found_bow = False
found_bow_alt = False
while prog_bow_locs and not (found_bow and found_bow_alt):
bow_loc = prog_bow_locs.pop()
if bow_loc.item.code == 0x65:
if bow_loc.item.code == 0x65 or (found_bow and not prog_bow_locs):
found_bow_alt = True
target = 'ganon_phase_3_no_silvers'
else:

View File

@@ -1,9 +1,12 @@
import collections
import logging
from typing import Iterator, Set
from worlds.alttp import OverworldGlitchRules
from BaseClasses import RegionType, MultiWorld, Entrance
from worlds.alttp.Items import ItemFactory, progression_items, item_name_groups
from worlds.alttp.Items import ItemFactory, progression_items, item_name_groups, item_table
from worlds.alttp.OverworldGlitchRules import overworld_glitches_rules, no_logic_rules
from worlds.alttp.Regions import location_table
from worlds.alttp.UnderworldGlitchRules import underworld_glitches_rules
from worlds.alttp.Bosses import GanonDefeatRule
from worlds.generic.Rules import set_rule, add_rule, forbid_item, add_item_rule, item_in_locations, \
@@ -176,6 +179,14 @@ def dungeon_boss_rules(world, player):
def global_rules(world, player):
# ganon can only carry triforce
add_item_rule(world.get_location('Ganon', player), lambda item: item.name == 'Triforce' and item.player == player)
# dungeon prizes can only be crystals/pendants
crystals_and_pendants: Set[str] = \
{item for item, item_data in item_table.items() if item_data.type == "Crystal"}
prize_locations: Iterator[str] = \
(locations for locations, location_data in location_table.items() if location_data[2] == True)
for prize_location in prize_locations:
add_item_rule(world.get_location(prize_location, player),
lambda item: item.name in crystals_and_pendants and item.player == player)
# determines which S&Q locations are available - hide from paths since it isn't an in-game location
world.get_region('Menu', player).can_reach_private = lambda state: True
for exit in world.get_region('Menu', player).exits:
@@ -515,7 +526,7 @@ def default_rules(world, player):
set_rule(world.get_entrance('Floating Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has('Moon Pearl', player) and state.has_sword(player) and state.has_turtle_rock_medallion(player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword required to cast magic (!)
set_rule(world.get_entrance('Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or world.open_pyramid[player])
set_rule(world.get_entrance('Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or world.open_pyramid[player].to_bool(world, player))
if world.swordless[player]:
swordless_rules(world, player)

View File

@@ -39,9 +39,9 @@ class Shop():
blacklist: Set[str] = set() # items that don't work, todo: actually check against this
type = ShopType.Shop
slot_names: Dict[int, str] = {
0: "Left",
1: "Center",
2: "Right"
0: " Left",
1: " Center",
2: " Right"
}
def __init__(self, region, room_id: int, shopkeeper_config: int, custom: bool, locked: bool, sram_offset: int):
@@ -142,7 +142,11 @@ class Shop():
class TakeAny(Shop):
type = ShopType.TakeAny
slot_names: Dict[int, str] = {
0: "",
1: "",
2: ""
}
class UpgradeShop(Shop):
type = ShopType.UpgradeShop
@@ -168,8 +172,10 @@ def FillDisabledShopSlots(world):
def ShopSlotFill(world):
shop_slots: Set[ALttPLocation] = {location for shop_locations in (shop.region.locations for shop in world.shops)
shop_slots: Set[ALttPLocation] = {location for shop_locations in
(shop.region.locations for shop in world.shops if shop.type != ShopType.TakeAny)
for location in shop_locations if location.shop_slot is not None}
removed = set()
for location in shop_slots:
shop: Shop = location.parent_region.shop
@@ -318,7 +324,7 @@ def create_shops(world, player: int):
for index, item in enumerate(inventory):
shop.add_inventory(index, *item)
if not locked and num_slots:
slot_name = f"{region.name} {shop.slot_names[index]}"
slot_name = f"{region.name}{shop.slot_names[index]}"
loc = ALttPLocation(player, slot_name, address=shop_table_by_location[slot_name],
parent=region, hint_text="for sale")
loc.shop_slot = index
@@ -376,7 +382,7 @@ total_dynamic_shop_slots = sum(3 for shopname, data in shop_table.items() if not
SHOP_ID_START = 0x400000
shop_table_by_location_id = dict(enumerate(
(f"{name} {Shop.slot_names[num]}" for name, shop_data in
(f"{name}{Shop.slot_names[num]}" for name, shop_data in
sorted(shop_table.items(), key=lambda item: item[1].sram_offset)
for num in range(3)), start=SHOP_ID_START))
@@ -591,3 +597,22 @@ def price_to_funny_price(world, item: dict, player: int):
item['price'] = min(price_chart[p_type](item['price']), 255)
item['price_type'] = p_type
break
def create_dynamic_shop_locations(world, player):
for shop in world.shops:
if shop.region.player == player:
for i, item in enumerate(shop.inventory):
if item is None:
continue
if item['create_location']:
slot_name = f"{shop.region.name}{shop.slot_names[i]}"
loc = ALttPLocation(player, slot_name,
address=shop_table_by_location[slot_name], parent=shop.region)
loc.place_locked_item(ItemFactory(item['item'], player))
if shop.type == ShopType.TakeAny:
loc.shop_slot_disabled = True
shop.region.locations.append(loc)
world.clear_location_cache()
loc.shop_slot = i

View File

@@ -0,0 +1,39 @@
from typing import List
from BaseClasses import Item, Location
from test.TestBase import WorldTestBase
class TestPrizes(WorldTestBase):
game = "A Link to the Past"
def test_item_rules(self):
prize_locations: List[Location] = [
self.multiworld.get_location("Eastern Palace - Prize", 1),
self.multiworld.get_location("Desert Palace - Prize", 1),
self.multiworld.get_location("Tower of Hera - Prize", 1),
self.multiworld.get_location("Palace of Darkness - Prize", 1),
self.multiworld.get_location("Swamp Palace - Prize", 1),
self.multiworld.get_location("Thieves\' Town - Prize", 1),
self.multiworld.get_location("Skull Woods - Prize", 1),
self.multiworld.get_location("Ice Palace - Prize", 1),
self.multiworld.get_location("Misery Mire - Prize", 1),
self.multiworld.get_location("Turtle Rock - Prize", 1),
]
prize_items: List[Item] = [
self.get_item_by_name("Green Pendant"),
self.get_item_by_name("Blue Pendant"),
self.get_item_by_name("Red Pendant"),
self.get_item_by_name("Crystal 1"),
self.get_item_by_name("Crystal 2"),
self.get_item_by_name("Crystal 3"),
self.get_item_by_name("Crystal 4"),
self.get_item_by_name("Crystal 5"),
self.get_item_by_name("Crystal 6"),
self.get_item_by_name("Crystal 7"),
]
for item in self.multiworld.get_items():
for prize_location in prize_locations:
self.assertEqual(item in prize_items, prize_location.item_rule(item),
f"{item} must {'' if item in prize_items else 'not '}be allowed in {prize_location}.")

View File

@@ -0,0 +1,37 @@
from test.TestBase import WorldTestBase
from ...Items import ItemFactory
class PyramidTestBase(WorldTestBase):
game = "A Link to the Past"
class OpenPyramidTest(PyramidTestBase):
options = {
"open_pyramid": "open"
}
def testAccess(self):
self.assertFalse(self.can_reach_entrance("Pyramid Hole"))
self.collect_by_name(["Hammer", "Progressive Glove", "Moon Pearl"])
self.assertTrue(self.can_reach_entrance("Pyramid Hole"))
class GoalPyramidTest(PyramidTestBase):
options = {
"open_pyramid": "goal"
}
def testCrystalsGoalAccess(self):
self.multiworld.goal[1] = "crystals"
self.assertFalse(self.can_reach_entrance("Pyramid Hole"))
self.collect_by_name(["Hammer", "Progressive Glove", "Moon Pearl"])
self.assertTrue(self.can_reach_entrance("Pyramid Hole"))
def testGanonGoalAccess(self):
self.assertFalse(self.can_reach_entrance("Pyramid Hole"))
self.collect_by_name(["Hammer", "Progressive Glove", "Moon Pearl"])
self.assertFalse(self.can_reach_entrance("Pyramid Hole"))
self.multiworld.state.collect(ItemFactory("Beat Agahnim 2", 1))
self.assertTrue(self.can_reach_entrance("Pyramid Hole"))

View File

@@ -113,8 +113,8 @@ class TestPlandoBosses(unittest.TestCase):
"""Test automatic singularity mode"""
self.assertIn(";singularity", MultiBosses.from_any("b2").value)
def testPlandoSettings(self):
"""Test that plando settings verification works"""
def testPlandoOptions(self):
"""Test that plando options verification works"""
plandoed_string = "l1-b2;l2-b1"
mixed_string = "l1-b2;shuffle"
regular_string = "shuffle"
@@ -123,14 +123,14 @@ class TestPlandoBosses(unittest.TestCase):
regular = MultiBosses.from_any(regular_string)
# plando should work with boss plando
plandoed.verify(None, "Player", Generate.PlandoSettings.bosses)
plandoed.verify(None, "Player", Generate.PlandoOptions.bosses)
self.assertTrue(plandoed.value.startswith(plandoed_string))
# plando should fall back to default without boss plando
plandoed.verify(None, "Player", Generate.PlandoSettings.items)
plandoed.verify(None, "Player", Generate.PlandoOptions.items)
self.assertEqual(plandoed, MultiBosses.option_vanilla)
# mixed should fall back to mode
mixed.verify(None, "Player", Generate.PlandoSettings.items) # should produce a warning and still work
mixed.verify(None, "Player", Generate.PlandoOptions.items) # should produce a warning and still work
self.assertEqual(mixed, MultiBosses.option_shuffle)
# mode stuff should just work
regular.verify(None, "Player", Generate.PlandoSettings.items)
regular.verify(None, "Player", Generate.PlandoOptions.items)
self.assertEqual(regular, MultiBosses.option_shuffle)

View File

@@ -6,7 +6,7 @@ BK Sudoku is not a typical Archipelago game; instead, it is a generic Sudoku cli
## What hints are unlocked?
After completing a Sudoku puzzle, the game will unlock 1 random hint for an unchecked location in the slot you are connected to. It is possible to hint a location that was previously hinted for using the !hint command.
After completing a Sudoku puzzle, the game will unlock 1 random hint for an unchecked location in the slot you are connected to.
## Where is the settings page?

View File

@@ -61,6 +61,19 @@ class MaxTechCost(TechCost):
default = 500
class TechCostDistribution(Choice):
"""Random distribution of costs of the Science Packs.
Even: any number between min and max is equally likely.
Low: low costs, near the minimum, are more likely.
Middle: medium costs, near the average, are more likely.
High: high costs, near the maximum, are more likely."""
display_name = "Tech Cost Distribution"
option_even = 0
option_low = 1
option_middle = 2
option_high = 3
class TechCostMix(Range):
"""Percent chance that a preceding Science Pack is also required.
Chance is rolled per preceding pack."""
@@ -69,6 +82,14 @@ class TechCostMix(Range):
default = 70
class RampingTechCosts(Toggle):
"""Forces the amount of Science Packs required to ramp up with the highest involved Pack. Average is preserved.
For example:
off: Automation (red)/Logistics (green) sciences can range from 1 to 1000 Science Packs,
on: Automation (red) ranges to ~500 packs and Logistics (green) from ~500 to 1000 Science Packs"""
display_name = "Ramping Tech Costs"
class Silo(Choice):
"""Ingredients to craft rocket silo or auto-place if set to spawn."""
display_name = "Rocket Silo"
@@ -371,7 +392,9 @@ factorio_options: typing.Dict[str, type(Option)] = {
"tech_tree_layout": TechTreeLayout,
"min_tech_cost": MinTechCost,
"max_tech_cost": MaxTechCost,
"tech_cost_distribution": TechCostDistribution,
"tech_cost_mix": TechCostMix,
"ramping_tech_costs": RampingTechCosts,
"silo": Silo,
"satellite": Satellite,
"free_samples": FreeSamples,

View File

@@ -7,7 +7,7 @@ import typing
from BaseClasses import Region, Entrance, Location, Item, RegionType, Tutorial, ItemClassification
from worlds.AutoWorld import World, WebWorld
from .Mod import generate_mod
from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal
from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal, TechCostDistribution
from .Shapes import get_shapes
from .Technologies import base_tech_table, recipe_sources, base_technology_table, \
all_ingredient_names, all_product_sources, required_technologies, get_rocket_requirements, \
@@ -97,9 +97,24 @@ class Factorio(World):
location_names = self.multiworld.random.sample(location_pool, location_count)
self.locations = [FactorioScienceLocation(player, loc_name, self.location_name_to_id[loc_name], nauvis)
for loc_name in location_names]
rand_values = sorted(random.randint(self.multiworld.min_tech_cost[self.player],
self.multiworld.max_tech_cost[self.player]) for _ in self.locations)
for i, location in enumerate(sorted(self.locations, key=lambda loc: loc.rel_cost)):
distribution: TechCostDistribution = self.multiworld.tech_cost_distribution[self.player]
min_cost = self.multiworld.min_tech_cost[self.player]
max_cost = self.multiworld.max_tech_cost[self.player]
if distribution == distribution.option_even:
rand_values = (random.randint(min_cost, max_cost) for _ in self.locations)
else:
mode = {distribution.option_low: min_cost,
distribution.option_middle: (min_cost+max_cost)//2,
distribution.option_high: max_cost}[distribution.value]
rand_values = (random.triangular(min_cost, max_cost, mode) for _ in self.locations)
rand_values = sorted(rand_values)
if self.multiworld.ramping_tech_costs[self.player]:
def sorter(loc: FactorioScienceLocation):
return loc.complexity, loc.rel_cost
else:
def sorter(loc: FactorioScienceLocation):
return loc.rel_cost
for i, location in enumerate(sorted(self.locations, key=sorter)):
location.count = rand_values[i]
del rand_values
nauvis.locations.extend(self.locations)
@@ -198,6 +213,10 @@ class Factorio(World):
self.multiworld.itempool.append(tech_item)
else:
loc = cost_sorted_locations[index]
if index >= 0:
# beginning techs - limit cost to 10
# as automation is not achievable yet and hand-crafting for hours is not fun gameplay
loc.count = min(loc.count, 10)
loc.place_locked_item(tech_item)
loc.revealed = True
@@ -448,7 +467,7 @@ class FactorioScienceLocation(FactorioLocation):
# Factorio technology properties:
ingredients: typing.Dict[str, int]
count: int
count: int = 0
def __init__(self, player: int, name: str, address: int, parent: Region):
super(FactorioScienceLocation, self).__init__(player, name, address, parent)
@@ -460,8 +479,6 @@ class FactorioScienceLocation(FactorioLocation):
for complexity in range(self.complexity):
if parent.multiworld.tech_cost_mix[self.player] > parent.multiworld.random.randint(0, 99):
self.ingredients[Factorio.ordered_science_packs[complexity]] = 1
self.count = parent.multiworld.random.randint(parent.multiworld.min_tech_cost[self.player],
parent.multiworld.max_tech_cost[self.player])
@property
def factorio_ingredients(self) -> typing.List[typing.Tuple[str, int]]:

View File

@@ -63,7 +63,14 @@ games you want settings for.
using this to detail the intention of the file.
* `name` is the player name you would like to use and is used for your slot data to connect with most games. This can
also be filled with multiple names each having a weight to it.
also be filled with multiple names each having a weight to it. Names can also contain certain keywords, surrounded by
curly-braces, which will be replaced on generation with a number:
* `{player}` will be replaced with the player's slot number.
* `{PLAYER}` will be replaced with the player's slot number if that slot number is greater than 1, otherwise blank.
* `{number}` will be replaced with the counter value of the name.
* `{NUMBER}` will be replaced with the counter value of the name if the counter value is greater than 1, otherwise
blank.
* `game` is where either your chosen game goes or if you would like can be filled with multiple games each with
different weights.

View File

@@ -70,10 +70,9 @@ including the exclamation point.
names such as Factorio.
- `!hint <item name>` Tells you at which location in whose game your Item is. Note you need to have checked some
locations to earn a hint. You can check how many you have by just running `!hint`
- `!forfeit` If you didn't turn on auto-forfeit or if you allowed forfeiting prior to goal completion. Remember that "
forfeiting" actually means sending out your remaining items in your world.
- `!collect` Grants you all the remaining checks in your world. Can only be used after your goal is complete or when you
have forfeited.
- `!release` If you didn't turn on auto-release or if you allowed releasing prior to goal completion. Remember that "
releasing" actually means sending out your remaining items in your world.
- `!collect` Grants you all the remaining checks in your world. Typically used after goal completion.
#### Host only (on Archipelago.gg or in your server console)
@@ -87,9 +86,9 @@ including the exclamation point.
- `/exit` Shutdown the server
- `/alias <player name> <alias name>` Assign a player an alias.
- `/collect <player name>` Send out any items remaining in the multiworld belonging to the given player.
- `/forfeit <player name>` Forfeits someone regardless of settings and game completion status
- `/allow_forfeit <player name>` Allows the given player to use the `!forfeit` command.
- `/forbid_forfeit <player name>` Bars the given player from using the `!forfeit` command.
- `/release <player name>` Releases someone regardless of settings and game completion status
- `/allow_release <player name>` Allows the given player to use the `!release` command.
- `/forbid_release <player name>` Bars the given player from using the `!release` command.
- `/send <player name> <item name>` Grants the given player the specified item.
- `/send_multiple <amount> <player name> <item name>` Grants the given player the stated amount of the specified item.
- `/send_location <player name> <location name>` Send out the given location for the specified player as if the player checked it

View File

@@ -69,7 +69,7 @@ multiworld. The output of this process is placed in the `output` folder.
#### Changing local host settings for generation
Sometimes there are various settings that you may want to change before rolling a seed such as enabling race mode,
auto-forfeit, plando support, or setting a password.
auto-release, plando support, or setting a password.
All of these settings plus other options are able to be changed by modifying the `host.yaml` file in the Archipelago
installation folder. The settings chosen here are baked into the `.archipelago` file that gets output with the other

View File

@@ -8,7 +8,7 @@ should only take a couple of minutes to read.
1. After gathering the YAML files together in one location, select all the files and compress them into a `.ZIP` file.
2. Next go to the "Generate Game" page. Generate game
page: [Archipelago Seed Generation Page](/generate). Here, you can adjust some server settings
such as forfeit rules and the cost for a player to use a hint before generation.
such as release rules and the cost for a player to use a hint before generation.
3. After adjusting the host settings to your liking click on the Upload File button and using the explorer window that
opens, navigate to the location where you zipped the player files and upload this zip. The page will generate your
game and refresh multiple times to check on completion status.

View File

@@ -26,7 +26,7 @@ item_name_groups = {group: lookup_type_to_names[group] for group in ("Skill", "C
directionals = ('', 'Left_', 'Right_')
item_name_groups.update({
"Dreamer": {"Herrah", "Monomon", "Lurien"},
"Dreamers": {"Herrah", "Monomon", "Lurien"},
"Cloak": {x + 'Mothwing_Cloak' for x in directionals} | {'Shade_Cloak', 'Split_Shade_Cloak'},
"Claw": {x + 'Mantis_Claw' for x in directionals},
"CDash": {x + 'Crystal_Heart' for x in directionals},

View File

@@ -48,7 +48,7 @@ At minimum, every seed will require you to find the Cursed Seal and bring it bac
- `any_ending`: You must defeat the final boss.
- `true_ending`: You must first explore all 3000 rooms of the Atlas Dome and find the Agate Knife, then fight the final boss' true form.
Once the goal has been completed, you may press F to send a forfeit, sending out all of your world's remaining items to their respective players, and C to send a collect, which gathers up all of your world's items from their shuffled locations in other player's worlds. You may also press S to view your statistics, if you're a fan of numbers.
Once the goal has been completed, you may press F to send a release, sending out all of your world's remaining items to their respective players, and C to send a collect, which gathers up all of your world's items from their shuffled locations in other player's worlds. You may also press S to view your statistics, if you're a fan of numbers.
More in-depth information about the game can be found in the game's help file, accessed by pressing H while playing.

View File

@@ -458,11 +458,11 @@ def shuffle_random_entrances(ootworld):
one_way_entrance_pools['OwlDrop'] = ootworld.get_shufflable_entrances(type='OwlDrop')
if ootworld.warp_songs:
one_way_entrance_pools['WarpSong'] = ootworld.get_shufflable_entrances(type='WarpSong')
if ootworld.logic_rules == 'glitchless':
one_way_priorities['Bolero'] = priority_entrance_table['Bolero']
one_way_priorities['Nocturne'] = priority_entrance_table['Nocturne']
if not ootworld.shuffle_dungeon_entrances and not ootworld.shuffle_overworld_entrances:
one_way_priorities['Requiem'] = priority_entrance_table['Requiem']
# No more exceptions for NL here, causes cascading failures later
one_way_priorities['Bolero'] = priority_entrance_table['Bolero']
one_way_priorities['Nocturne'] = priority_entrance_table['Nocturne']
if not ootworld.shuffle_dungeon_entrances and not ootworld.shuffle_overworld_entrances:
one_way_priorities['Requiem'] = priority_entrance_table['Requiem']
if ootworld.spawn_positions:
one_way_entrance_pools['Spawn'] = ootworld.get_shufflable_entrances(type='Spawn')
if 'child' not in ootworld.spawn_positions:
@@ -480,7 +480,7 @@ def shuffle_random_entrances(ootworld):
if ootworld.shuffle_dungeon_entrances:
entrance_pools['Dungeon'] = ootworld.get_shufflable_entrances(type='Dungeon', only_primary=True)
if ootworld.open_forest == 'closed':
entrance_pools['Dungeon'].remove(ootworld.get_entrance('KF Outside Deku Tree -> Deku Tree Lobby', player))
entrance_pools['Dungeon'].remove(ootworld.get_entrance('KF Outside Deku Tree -> Deku Tree Lobby'))
if ootworld.shuffle_special_dungeon_entrances:
entrance_pools['Dungeon'] += ootworld.get_shufflable_entrances(type='DungeonSpecial', only_primary=True)
if ootworld.decouple_entrances:
@@ -500,7 +500,7 @@ def shuffle_random_entrances(ootworld):
exclude_overworld_reverse = ootworld.mix_entrance_pools == 'all' and not ootworld.decouple_entrances
entrance_pools['Overworld'] = ootworld.get_shufflable_entrances(type='Overworld', only_primary=exclude_overworld_reverse)
if not ootworld.decouple_entrances:
entrance_pools['Overworld'].remove(world.get_entrance('GV Lower Stream -> Lake Hylia', player))
entrance_pools['Overworld'].remove(ootworld.get_entrance('GV Lower Stream -> Lake Hylia'))
# Mark shuffled entrances
for entrance in chain(chain.from_iterable(one_way_entrance_pools.values()), chain.from_iterable(entrance_pools.values())):
@@ -822,7 +822,7 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all
raise EntranceShuffleError(f'{entrance.name} potentially accessible as adult')
# Check if all locations are reachable if not NL
if ootworld.logic_rules != 'no_logic' and locations_to_ensure_reachable:
if locations_to_ensure_reachable:
for loc in locations_to_ensure_reachable:
if not all_state.can_reach(loc, 'Location', player):
raise EntranceShuffleError(f'{loc} is unreachable')

View File

@@ -1136,14 +1136,14 @@ def buildMiscItemHints(world, messages):
if world.multiworld.state.has(data['default_item'], world.player) > 0:
text = data['default_item_text'].format(area='#your pocket#')
elif item_locations:
location = item_locations[0]
location = world.hint_rng.choice(item_locations)
player_text = ''
if location.player != world.player:
player_text = world.multiworld.get_player_name(location.player) + "'s "
if location.game == 'Ocarina of Time':
area = HintArea.at(location, use_alt_hint=data['use_alt_hint']).text(world.clearer_hints, world=None)
else:
area = location.name
area = location.name
text = data['default_item_text'].format(area=(player_text + area))
elif 'default_item_fallback' in data:
text = data['default_item_fallback']

View File

@@ -41,7 +41,7 @@ class OOTItem(Item):
classification = ItemClassification.useful
elif name == "Ice Trap":
classification = ItemClassification.trap
elif name == 'Gold Skulltula Token':
elif name in {'Gold Skulltula Token', 'Triforce Piece'}:
classification = ItemClassification.progression_skip_balancing
elif advancement:
classification = ItemClassification.progression

View File

@@ -109,7 +109,7 @@ class OOTWorld(World):
data_version = 3
required_client_version = (0, 3, 6)
required_client_version = (0, 3, 7)
item_name_groups = {
# internal groups
@@ -171,12 +171,13 @@ class OOTWorld(World):
# ER and glitched logic are not compatible; glitched takes priority
if self.logic_rules == 'glitched':
self.shuffle_interior_entrances = 'off'
self.shuffle_dungeon_entrances = 'off'
self.spawn_positions = 'off'
self.shuffle_bosses = 'off'
self.shuffle_grotto_entrances = False
self.shuffle_dungeon_entrances = False
self.shuffle_overworld_entrances = False
self.owl_drops = False
self.warp_songs = False
self.spawn_positions = 'off'
# Fix spawn positions option
new_sp = []
@@ -197,6 +198,17 @@ class OOTWorld(World):
if self.triforce_hunt:
self.shuffle_ganon_bosskey = 'triforce'
# Force itempool to higher settings if it doesn't have enough hearts
max_required_hearts = 3
if self.bridge == 'hearts':
max_required_hearts = max(max_required_hearts, self.bridge_hearts)
if self.shuffle_ganon_bosskey == 'hearts':
max_required_hearts = max(max_required_hearts, self.ganon_bosskey_hearts)
if max_required_hearts > 3 and self.item_pool_value == 'minimal':
self.item_pool_value = 'scarce'
if max_required_hearts > 12 and self.item_pool_value == 'scarce':
self.item_pool_value = 'balanced'
# If songs/keys locked to own world by settings, add them to local_items
local_types = []
if self.shuffle_song_items != 'any':
@@ -283,8 +295,17 @@ class OOTWorld(World):
self.shuffle_special_dungeon_entrances = self.shuffle_dungeon_entrances == 'all'
self.shuffle_dungeon_entrances = self.shuffle_dungeon_entrances != 'off'
self.ensure_tod_access = (self.shuffle_interior_entrances != 'off') or self.shuffle_overworld_entrances or self.spawn_positions
self.entrance_shuffle = (self.shuffle_interior_entrances != 'off') or self.shuffle_grotto_entrances or self.shuffle_dungeon_entrances or \
self.shuffle_overworld_entrances or self.owl_drops or self.warp_songs or self.spawn_positions
self.entrance_shuffle = (
self.shuffle_interior_entrances != 'off'
or self.shuffle_bosses != 'off'
or self.shuffle_dungeon_entrances
or self.shuffle_special_dungeon_entrances
or self.spawn_positions
or self.shuffle_grotto_entrances
or self.shuffle_overworld_entrances
or self.owl_drops
or self.warp_songs
)
self.disable_trade_revert = (self.shuffle_interior_entrances != 'off') or self.shuffle_overworld_entrances
self.shuffle_special_interior_entrances = self.shuffle_interior_entrances == 'all'
@@ -317,13 +338,14 @@ class OOTWorld(World):
# Determine which dungeons are MQ. Not compatible with glitched logic.
mq_dungeons = set()
all_dungeons = [d['name'] for d in dungeon_table]
if self.logic_rules != 'glitched':
if self.mq_dungeons_mode == 'mq':
mq_dungeons = dungeon_table.keys()
mq_dungeons = all_dungeons
elif self.mq_dungeons_mode == 'specific':
mq_dungeons = self.mq_dungeons_specific
elif self.mq_dungeons_mode == 'count':
mq_dungeons = self.multiworld.random.sample(dungeon_table, self.mq_dungeons_count)
mq_dungeons = self.multiworld.random.sample(all_dungeons, self.mq_dungeons_count)
else:
self.mq_dungeons_mode = 'count'
self.mq_dungeons_count = 0
@@ -441,6 +463,7 @@ class OOTWorld(World):
new_region.scene = region['scene']
if 'dungeon' in region:
new_region.dungeon = region['dungeon']
new_region.set_hint_data(region['dungeon'])
if 'is_boss_room' in region:
new_region.is_boss_room = region['is_boss_room']
if 'hint' in region:
@@ -842,10 +865,13 @@ class OOTWorld(World):
impa = self.multiworld.get_location("Song from Impa", self.player)
if self.shuffle_child_trade == 'skip_child_zelda':
if impa.item is None:
item_to_place = self.multiworld.random.choice(
list(item for item in self.multiworld.itempool if item.player == self.player))
candidate_items = list(item for item in self.multiworld.itempool if item.player == self.player)
if candidate_items:
item_to_place = self.multiworld.random.choice(candidate_items)
self.multiworld.itempool.remove(item_to_place)
else:
item_to_place = self.create_item("Recovery Heart")
impa.place_locked_item(item_to_place)
self.multiworld.itempool.remove(item_to_place)
# Give items to startinventory
self.multiworld.push_precollected(impa.item)
self.multiworld.push_precollected(self.create_item("Zeldas Letter"))
@@ -1269,6 +1295,13 @@ def gather_locations(multiworld: MultiWorld,
'HideoutSmallKey': 'shuffle_hideoutkeys',
'GanonBossKey': 'shuffle_ganon_bosskey',
}
# Special handling for atypical item types
if item_type == 'HideoutSmallKey':
dungeon = 'Thieves Hideout'
elif item_type == 'GanonBossKey':
dungeon = 'Ganons Castle'
if isinstance(players, int):
players = {players}
fill_opts = {p: getattr(multiworld.worlds[p], type_to_setting[item_type]) for p in players}

View File

@@ -135,10 +135,10 @@ def level_shuffle_factory(
shuffle_horde_levels: bool,
) -> Dict[int, Overcooked2GenericLevel]: # return <story_level_id, level>
# Create a list of all valid levels for selection
# (excludes tutorial, throne, kevin and sometimes horde levels)
# (excludes tutorial, throne and sometimes horde/prep levels)
pool = list()
for dlc in Overcooked2Dlc:
for level_id in range(dlc.start_level_id(), dlc.end_level_id()):
for level_id in range(dlc.start_level_id, dlc.end_level_id):
if level_id in dlc.excluded_levels():
continue
@@ -165,11 +165,12 @@ def level_shuffle_factory(
rng.shuffle(pool)
# Return the first 44 levels and assign those to each level
for level_id in range(story.start_level_id(), story.end_level_id()):
for level_id in range(story.start_level_id, story.end_level_id):
if level_id not in story.excluded_levels():
result[level_id] = pool[level_id-1]
else:
result[level_id] = Overcooked2GenericLevel(level_id) # This is just 6-6 right now
elif level_id == 36:
# Level 6-6 is exempt from shuffling
result[level_id] = Overcooked2GenericLevel(level_id)
return result
@@ -2599,7 +2600,9 @@ level_logic = {
{ # Exclusive
},
horde_logic
{ # Additive
},
),
( # 2-star
{ # Exclusive

View File

@@ -92,7 +92,7 @@ class StarsToWin(Range):
display_name = "Stars to Win"
range_start = 0
range_end = 100
default = 66
default = 60
class StarThresholdScale(Range):
@@ -101,7 +101,7 @@ class StarThresholdScale(Range):
display_name = "Star Difficulty %"
range_start = 1
range_end = 100
default = 45
default = 35
overcooked_options = {

View File

@@ -32,16 +32,16 @@ class Overcooked2Dlc(Enum):
assert False
# inclusive
@property
def start_level_id(self) -> int:
if self == Overcooked2Dlc.STORY:
return 1
return 0
# exclusive
@property
def end_level_id(self) -> int:
id = None
if self == Overcooked2Dlc.STORY:
id = 6*6 + 8 # world_count*level_count + kevin count
id = 1 + 6*6 + 8 # tutorial + world_count*level_count + kevin count
if self == Overcooked2Dlc.SURF_N_TURF:
id = 3*4 + 1
if self == Overcooked2Dlc.CAMPFIRE_COOK_OFF:
@@ -51,9 +51,9 @@ class Overcooked2Dlc(Enum):
if self == Overcooked2Dlc.CARNIVAL_OF_CHAOS:
id = 3*4 + 3
if self == Overcooked2Dlc.SEASONAL:
id = 31
id = 31 + 1
return self.start_level_id() + id
return self.start_level_id + id
# Tutorial + Horde Levels + Endgame
def excluded_levels(self) -> List[int]:

View File

@@ -147,3 +147,26 @@ class Overcooked2Test(unittest.TestCase):
for item in ITEMS_TO_EXCLUDE_IF_NO_DLC:
self.assertIn(item, item_table.keys())
def testLevelCounts(self):
for dlc in Overcooked2Dlc:
level_id_range = range(dlc.start_level_id, dlc.end_level_id)
for level_id in dlc.excluded_levels():
self.assertIn(level_id, level_id_range, f"Excluded level {dlc.name} - {level_id} out of range")
for level_id in dlc.horde_levels():
self.assertIn(level_id, level_id_range, f"Horde level {dlc.name} - {level_id} out of range")
for level_id in dlc.prep_levels():
self.assertIn(level_id, level_id_range, f"Prep level {dlc.name} - {level_id} out of range")
for level_id in level_id_range:
self.assertIn((dlc, level_id), level_id_to_shortname, "A valid level is not represented in level directory")
count = 0
for (dlc_key, _) in level_id_to_shortname:
if dlc == dlc_key:
count += 1
self.assertEqual(count, len(level_id_range), f"Number of levels in {dlc.name} has discrepancy between level_id range and directory")

View File

@@ -40,13 +40,11 @@ class PokemonRedBlueWorld(World):
game = "Pokemon Red and Blue"
option_definitions = pokemon_rb_options
data_version = 3
data_version = 5
required_client_version = (0, 3, 7)
topology_present = False
item_name_to_id = {name: data.id for name, data in item_table.items()}
location_name_to_id = {location.name: location.address for location in location_data if location.type == "Item"}
item_name_groups = item_groups
@@ -192,8 +190,9 @@ class PokemonRedBlueWorld(World):
item = self.create_filler()
else:
item = self.create_item(location.original_item)
combined_traps = self.multiworld.poison_trap_weight[self.player].value + self.multiworld.fire_trap_weight[self.player].value + self.multiworld.paralyze_trap_weight[self.player].value + self.multiworld.ice_trap_weight[self.player].value
if (item.classification == ItemClassification.filler and self.multiworld.random.randint(1, 100)
<= self.multiworld.trap_percentage[self.player].value):
<= self.multiworld.trap_percentage[self.player].value and combined_traps != 0):
item = self.create_item(self.select_trap())
if location.event:
self.multiworld.get_location(location.name, self.player).place_locked_item(item)
@@ -319,7 +318,8 @@ class PokemonRedBlueWorld(World):
spoiler_handle.write(f"{matchup[0]} deals {matchup[2] * 10}% damage to {matchup[1]}\n")
def get_filler_item_name(self) -> str:
if self.multiworld.random.randint(1, 100) <= self.multiworld.trap_percentage[self.player].value:
combined_traps = self.multiworld.poison_trap_weight[self.player].value + self.multiworld.fire_trap_weight[self.player].value + self.multiworld.paralyze_trap_weight[self.player].value + self.multiworld.ice_trap_weight[self.player].value
if self.multiworld.random.randint(1, 100) <= self.multiworld.trap_percentage[self.player].value and combined_traps != 0:
return self.select_trap()
return self.multiworld.random.choice([item for item in item_table if item_table[
@@ -351,6 +351,7 @@ class PokemonRedBlueWorld(World):
"elite_four_condition": self.multiworld.elite_four_condition[self.player].value,
"victory_road_condition": self.multiworld.victory_road_condition[self.player].value,
"viridian_gym_condition": self.multiworld.viridian_gym_condition[self.player].value,
"cerulean_cave_condition": self.multiworld.cerulean_cave_condition[self.player].value,
"free_fly_map": self.fly_map_code,
"extra_badges": self.extra_badges,
"type_chart": self.type_chart,

View File

@@ -357,7 +357,7 @@ location_data = [
LocationData("Route 10 South", "Hidden Item Bush", "Max Ether", rom_addresses['Hidden_Item_Route_10_2'], Hidden(8), inclusion=hidden_items),
LocationData("Rocket Hideout B1F", "Hidden Item Pot Plant", "PP Up", rom_addresses['Hidden_Item_Rocket_Hideout_B1F'], Hidden(9), inclusion=hidden_items),
LocationData("Rocket Hideout B3F", "Hidden Item Near East Item", "Nugget", rom_addresses['Hidden_Item_Rocket_Hideout_B3F'], Hidden(10), inclusion=hidden_items),
LocationData("Rocket Hideout B4F", "Hidden Item Behind Giovanni", "Super Potion", rom_addresses['Hidden_Item_Rocket_Hideout_B4F'], Hidden(11), inclusion=hidden_items),
LocationData("Rocket Hideout B4F", "Hidden Item Behind Giovanni (Lift Key)", "Super Potion", rom_addresses['Hidden_Item_Rocket_Hideout_B4F'], Hidden(11), inclusion=hidden_items),
LocationData("Pokemon Tower 5F", "Hidden Item Near West Staircase", "Elixir", rom_addresses['Hidden_Item_Pokemon_Tower_5F'], Hidden(12), inclusion=hidden_items),
LocationData("Route 13", "Hidden Item Dead End Bush", "PP Up", rom_addresses['Hidden_Item_Route_13_1'], Hidden(13), inclusion=hidden_items),
LocationData("Route 13", "Hidden Item Dead End By Water Corner", "Calcium", rom_addresses['Hidden_Item_Route_13_2'], Hidden(14), inclusion=hidden_items),
@@ -506,7 +506,7 @@ location_data = [
LocationData("Route 20 West", "Beauty 1", None, rom_addresses["Trainersanity_EVENT_BEAT_ROUTE_20_TRAINER_9_ITEM"], EventFlag(175), inclusion=trainersanity),
LocationData("Route 20 West", "Jr. Trainer F 1", None, rom_addresses["Trainersanity_EVENT_BEAT_ROUTE_20_TRAINER_8_ITEM"], EventFlag(176), inclusion=trainersanity),
LocationData("Route 20 West", "Beauty 2", None, rom_addresses["Trainersanity_EVENT_BEAT_ROUTE_20_TRAINER_7_ITEM"], EventFlag(177), inclusion=trainersanity),
LocationData("Route 20 West", "Cooltrainer M", None, rom_addresses["Trainersanity_EVENT_BEAT_ROUTE_20_TRAINER_6_ITEM"], EventFlag(178), inclusion=trainersanity),
LocationData("Route 20 West", "Bird Keeper", None, rom_addresses["Trainersanity_EVENT_BEAT_ROUTE_20_TRAINER_6_ITEM"], EventFlag(178), inclusion=trainersanity),
LocationData("Route 20 West", "Swimmer 1", None, rom_addresses["Trainersanity_EVENT_BEAT_ROUTE_20_TRAINER_4_ITEM"], EventFlag(180), inclusion=trainersanity),
LocationData("Route 20 West", "Jr. Trainer F 2", None, rom_addresses["Trainersanity_EVENT_BEAT_ROUTE_20_TRAINER_3_ITEM"], EventFlag(181), inclusion=trainersanity),
LocationData("Route 20 East", "Beauty 3", None, rom_addresses["Trainersanity_EVENT_BEAT_ROUTE_20_TRAINER_2_ITEM"], EventFlag(182), inclusion=trainersanity),

View File

@@ -518,6 +518,7 @@ class TrapWeight(Choice):
option_low = 1
option_medium = 3
option_high = 5
option_disabled = 0
default = 3
@@ -539,7 +540,6 @@ class ParalyzeTrapWeight(TrapWeight):
class IceTrapWeight(TrapWeight):
"""Weights for Ice Traps. These apply the Ice status to all your party members. Don't forget to buy Ice Heals!"""
display_name = "Ice Trap Weight"
option_disabled = 0
default = 0
@@ -605,4 +605,4 @@ pokemon_rb_options = {
"paralyze_trap_weight": ParalyzeTrapWeight,
"ice_trap_weight": IceTrapWeight,
"death_link": DeathLink
}
}

View File

@@ -54,7 +54,7 @@ def get_base_stat_total(mon):
+ poke_data.pokemon_data[mon]["spc"])
def randomize_pokemon(self, mon, mons_list, randomize_type):
def randomize_pokemon(self, mon, mons_list, randomize_type, random):
if randomize_type in [1, 3]:
type_mons = [pokemon for pokemon in mons_list if any([poke_data.pokemon_data[mon][
"type1"] in [self.local_poke_data[pokemon]["type1"], self.local_poke_data[pokemon]["type2"]],
@@ -65,17 +65,17 @@ def randomize_pokemon(self, mon, mons_list, randomize_type):
if randomize_type == 3:
stat_base = get_base_stat_total(mon)
type_mons.sort(key=lambda mon: abs(get_base_stat_total(mon) - stat_base))
mon = type_mons[round(self.multiworld.random.triangular(0, len(type_mons) - 1, 0))]
mon = type_mons[round(random.triangular(0, len(type_mons) - 1, 0))]
if randomize_type == 2:
stat_base = get_base_stat_total(mon)
mons_list.sort(key=lambda mon: abs(get_base_stat_total(mon) - stat_base))
mon = mons_list[round(self.multiworld.random.triangular(0, 50, 0))]
mon = mons_list[round(random.triangular(0, 50, 0))]
elif randomize_type == 4:
mon = self.multiworld.random.choice(mons_list)
mon = random.choice(mons_list)
return mon
def process_trainer_data(self, data):
def process_trainer_data(self, data, random):
mons_list = [pokemon for pokemon in poke_data.pokemon_data.keys() if pokemon not in poke_data.legendary_pokemon
or self.multiworld.trainer_legendaries[self.player].value]
address = rom_addresses["Trainer_Data"]
@@ -94,14 +94,16 @@ def process_trainer_data(self, data):
for i in range(1, 4):
for l in ["A", "B", "C", "D", "E", "F", "G", "H"]:
if rom_addresses[f"Rival_Starter{i}_{l}"] == address:
mon = " ".join(self.multiworld.get_location(f"Pallet Town - Starter {i}", self.player).item.name.split()[1:])
mon = " ".join(self.multiworld.get_location(f"Pallet Town - Starter {i}",
self.player).item.name.split()[1:])
if l in ["D", "E", "F", "G", "H"] and mon in poke_data.evolves_to:
mon = poke_data.evolves_to[mon]
if l in ["F", "G", "H"] and mon in poke_data.evolves_to:
mon = poke_data.evolves_to[mon]
if mon is None and self.multiworld.randomize_trainer_parties[self.player].value:
mon = poke_data.id_to_mon[data[address]]
mon = randomize_pokemon(self, mon, mons_list, self.multiworld.randomize_trainer_parties[self.player].value)
mon = randomize_pokemon(self, mon, mons_list,
self.multiworld.randomize_trainer_parties[self.player].value, random)
if mon is not None:
data[address] = poke_data.pokemon_data[mon]["id"]
@@ -154,10 +156,11 @@ def process_static_pokemon(self):
location.place_locked_item(self.create_item(slot_type + " " + slot.original_item))
else:
mon = self.create_item(slot_type + " " +
randomize_pokemon(self, slot.original_item, mons_list, randomize_type))
randomize_pokemon(self, slot.original_item, mons_list, randomize_type,
self.multiworld.random))
while location.name == "Pokemon Tower 6F - Restless Soul" and mon in tower_6F_mons:
mon = self.create_item(slot_type + " " + randomize_pokemon(self, slot.original_item, mons_list,
randomize_type))
randomize_type, self.multiworld.random))
location.place_locked_item(mon)
for slot in starter_slots:
@@ -168,7 +171,8 @@ def process_static_pokemon(self):
location.place_locked_item(self.create_item(slot_type + " " + slot.original_item))
else:
location.place_locked_item(self.create_item(slot_type + " " +
randomize_pokemon(self, slot.original_item, mons_list, randomize_type)))
randomize_pokemon(self, slot.original_item, mons_list, randomize_type,
self.multiworld.random)))
def process_wild_pokemon(self):
@@ -182,13 +186,14 @@ def process_wild_pokemon(self):
self.multiworld.random.shuffle(encounter_slots)
locations = []
for slot in encounter_slots:
mon = randomize_pokemon(self, slot.original_item, mons_list, self.multiworld.randomize_wild_pokemon[self.player].value)
mon = randomize_pokemon(self, slot.original_item, mons_list,
self.multiworld.randomize_wild_pokemon[self.player].value, self.multiworld.random)
# if static Pokemon are not randomized, we make sure nothing on Pokemon Tower 6F is a Marowak
# if static Pokemon are randomized we deal with that during static encounter randomization
while (self.multiworld.randomize_static_pokemon[self.player].value == 0 and mon == "Marowak"
and "Pokemon Tower 6F" in slot.name):
# to account for the possibility that only one ground type Pokemon exists, match only stats for this fix
mon = randomize_pokemon(self, slot.original_item, mons_list, 2)
mon = randomize_pokemon(self, slot.original_item, mons_list, 2, self.multiworld.random)
placed_mons[mon] += 1
location = self.multiworld.get_location(slot.name, self.player)
location.item = self.create_item(mon)
@@ -326,16 +331,20 @@ def process_pokemon_data(self):
else:
mon_data["catch rate"] = max(self.multiworld.minimum_catch_rate[self.player], mon_data["catch rate"])
if mon in poke_data.evolves_from.keys() and mon_data["type1"] == local_poke_data[poke_data.evolves_from[mon]]["type1"] and mon_data["type2"] == local_poke_data[poke_data.evolves_from[mon]]["type2"]:
mon_data["tms"] = local_poke_data[poke_data.evolves_from[mon]]["tms"]
elif mon != "Mew":
if mon != "Mew":
tms_hms = poke_data.tm_moves + poke_data.hm_moves
for flag, tm_move in enumerate(tms_hms):
if (flag < 50 and self.multiworld.tm_compatibility[self.player].value == 1) or (flag >= 50 and self.multiworld.hm_compatibility[self.player].value == 1):
if ((mon in poke_data.evolves_from.keys() and mon_data["type1"] ==
local_poke_data[poke_data.evolves_from[mon]]["type1"] and mon_data["type2"] ==
local_poke_data[poke_data.evolves_from[mon]]["type2"]) and (
(flag < 50 and self.multiworld.tm_compatibility[self.player].value in [1, 2]) or (
flag >= 51 and self.multiworld.hm_compatibility[self.player].value in [1, 2]))):
bit = 1 if local_poke_data[poke_data.evolves_from[mon]]["tms"][int(flag / 8)] & 1 << (flag % 8) else 0
elif (flag < 50 and self.multiworld.tm_compatibility[self.player].value == 1) or (flag >= 50 and self.multiworld.hm_compatibility[self.player].value == 1):
type_match = poke_data.moves[tm_move]["type"] in [mon_data["type1"], mon_data["type2"]]
bit = int(self.multiworld.random.randint(1, 100) < [[90, 50, 25], [100, 75, 25]][flag >= 50][0 if type_match else 1 if poke_data.moves[tm_move]["type"] == "Normal" else 2])
elif (flag < 50 and self.multiworld.tm_compatibility[self.player].value == 2) or (flag >= 50 and self.multiworld.hm_compatibility[self.player].value == 2):
bit = [0, 1][self.multiworld.random.randint(0, 1)]
bit = self.multiworld.random.randint(0, 1)
elif (flag < 50 and self.multiworld.tm_compatibility[self.player].value == 3) or (flag >= 50 and self.multiworld.hm_compatibility[self.player].value == 3):
bit = 1
else:
@@ -390,6 +399,8 @@ def generate_output(self, output_directory: str):
data[rom_addresses["Fossils_Needed_For_Second_Item"]] = (
self.multiworld.second_fossil_check_condition[self.player].value)
data[rom_addresses["Option_Lose_Money"]] = int(not self.multiworld.lose_money_on_blackout[self.player].value)
if self.multiworld.extra_key_items[self.player].value:
data[rom_addresses['Options']] |= 4
data[rom_addresses["Option_Blind_Trainers"]] = round(self.multiworld.blind_trainers[self.player].value * 2.55)
@@ -505,9 +516,9 @@ def generate_output(self, output_directory: str):
inventory = ["Poke Ball", "Great Ball", "Ultra Ball"]
if self.multiworld.better_shops[self.player].value == 2:
inventory.append("Master Ball")
inventory += ["Potion", "Super Potion", "Hyper Potion", "Max Potion", "Full Restore", "Antidote", "Awakening",
"Burn Heal", "Ice Heal", "Paralyze Heal", "Full Heal", "Repel", "Super Repel", "Max Repel",
"Escape Rope"]
inventory += ["Potion", "Super Potion", "Hyper Potion", "Max Potion", "Full Restore", "Revive", "Antidote",
"Awakening", "Burn Heal", "Ice Heal", "Paralyze Heal", "Full Heal", "Repel", "Super Repel",
"Max Repel", "Escape Rope"]
shop_data = bytearray([0xFE, len(inventory)])
shop_data += bytearray([item_table[item].id - 172000000 for item in inventory])
shop_data.append(0xFF)
@@ -521,7 +532,7 @@ def generate_output(self, output_directory: str):
if data[rom_addresses["Start_Inventory"] + item.code - 172000000] < 255:
data[rom_addresses["Start_Inventory"] + item.code - 172000000] += 1
process_trainer_data(self, data)
process_trainer_data(self, data, random)
mons = [mon["id"] for mon in poke_data.pokemon_data.values()]
random.shuffle(mons)

View File

@@ -113,27 +113,27 @@ rom_addresses = {
"Event_Rocket_Thief": 0x196cc,
"Option_Cerulean_Cave_Condition": 0x1986c,
"Event_Stranded_Man": 0x19b1f,
"Event_Rivals_Sister": 0x19ced,
"Option_Pokemon_League_Badges": 0x19e0a,
"Shop10": 0x19ee1,
"Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_0_ITEM": 0x1a035,
"Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_1_ITEM": 0x1a043,
"Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_2_ITEM": 0x1a051,
"Missable_Silph_Co_4F_Item_1": 0x1a0f9,
"Missable_Silph_Co_4F_Item_2": 0x1a100,
"Missable_Silph_Co_4F_Item_3": 0x1a107,
"Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_0_ITEM": 0x1a25f,
"Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_1_ITEM": 0x1a26d,
"Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_2_ITEM": 0x1a27b,
"Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_3_ITEM": 0x1a289,
"Missable_Silph_Co_5F_Item_1": 0x1a361,
"Missable_Silph_Co_5F_Item_2": 0x1a368,
"Missable_Silph_Co_5F_Item_3": 0x1a36f,
"Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_0_ITEM": 0x1a49f,
"Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_1_ITEM": 0x1a4ad,
"Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_2_ITEM": 0x1a4bb,
"Missable_Silph_Co_6F_Item_1": 0x1a5dd,
"Missable_Silph_Co_6F_Item_2": 0x1a5e4,
"Event_Rivals_Sister": 0x19cf2,
"Option_Pokemon_League_Badges": 0x19e0f,
"Shop10": 0x19ee6,
"Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_0_ITEM": 0x1a03a,
"Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_1_ITEM": 0x1a048,
"Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_2_ITEM": 0x1a056,
"Missable_Silph_Co_4F_Item_1": 0x1a0fe,
"Missable_Silph_Co_4F_Item_2": 0x1a105,
"Missable_Silph_Co_4F_Item_3": 0x1a10c,
"Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_0_ITEM": 0x1a264,
"Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_1_ITEM": 0x1a272,
"Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_2_ITEM": 0x1a280,
"Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_3_ITEM": 0x1a28e,
"Missable_Silph_Co_5F_Item_1": 0x1a366,
"Missable_Silph_Co_5F_Item_2": 0x1a36d,
"Missable_Silph_Co_5F_Item_3": 0x1a374,
"Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_0_ITEM": 0x1a4a4,
"Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_1_ITEM": 0x1a4b2,
"Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_2_ITEM": 0x1a4c0,
"Missable_Silph_Co_6F_Item_1": 0x1a5e2,
"Missable_Silph_Co_6F_Item_2": 0x1a5e9,
"Event_Free_Sample": 0x1cad6,
"Starter1_F": 0x1cca2,
"Starter2_F": 0x1cca6,
@@ -838,46 +838,46 @@ rom_addresses = {
"Ghost_Battle2": 0x60c5c,
"Missable_Pokemon_Tower_6F_Item_1": 0x60cd7,
"Missable_Pokemon_Tower_6F_Item_2": 0x60cde,
"Trainersanity_EVENT_BEAT_POKEMONTOWER_7_TRAINER_0_ITEM": 0x60ea6,
"Trainersanity_EVENT_BEAT_POKEMONTOWER_7_TRAINER_1_ITEM": 0x60eb4,
"Trainersanity_EVENT_BEAT_POKEMONTOWER_7_TRAINER_2_ITEM": 0x60ec2,
"Gift_Aerodactyl": 0x610bc,
"Gift_Omanyte": 0x610c0,
"Gift_Kabuto": 0x610c4,
"Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_0_ITEM": 0x611a7,
"Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_1_ITEM": 0x611b5,
"Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_2_ITEM": 0x611c3,
"Missable_Viridian_Forest_Item_1": 0x6128a,
"Missable_Viridian_Forest_Item_2": 0x61291,
"Missable_Viridian_Forest_Item_3": 0x61298,
"Starter2_M": 0x614ae,
"Starter3_M": 0x614b6,
"Trainersanity_EVENT_BEAT_SS_ANNE_5_TRAINER_0_ITEM": 0x6173c,
"Trainersanity_EVENT_BEAT_SS_ANNE_5_TRAINER_1_ITEM": 0x6174a,
"Event_SS_Anne_Captain": 0x61925,
"Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_0_ITEM": 0x61a14,
"Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_1_ITEM": 0x61a22,
"Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_2_ITEM": 0x61a30,
"Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_3_ITEM": 0x61a3e,
"Missable_SS_Anne_1F_Item": 0x61b2a,
"Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_0_ITEM": 0x61bfb,
"Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_1_ITEM": 0x61c09,
"Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_2_ITEM": 0x61c17,
"Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_3_ITEM": 0x61c25,
"Missable_SS_Anne_2F_Item_1": 0x61d5f,
"Missable_SS_Anne_2F_Item_2": 0x61d72,
"Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_0_ITEM": 0x61e03,
"Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_1_ITEM": 0x61e11,
"Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_2_ITEM": 0x61e1f,
"Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_3_ITEM": 0x61e2d,
"Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_4_ITEM": 0x61e3b,
"Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_5_ITEM": 0x61e49,
"Missable_SS_Anne_B1F_Item_1": 0x61f61,
"Missable_SS_Anne_B1F_Item_2": 0x61f68,
"Missable_SS_Anne_B1F_Item_3": 0x61f6f,
"Trainersanity_EVENT_BEAT_SILPH_CO_11F_TRAINER_0_ITEM": 0x62330,
"Trainersanity_EVENT_BEAT_SILPH_CO_11F_TRAINER_1_ITEM": 0x6233e,
"Event_Silph_Co_President": 0x62351,
"Trainersanity_EVENT_BEAT_POKEMONTOWER_7_TRAINER_0_ITEM": 0x60eb2,
"Trainersanity_EVENT_BEAT_POKEMONTOWER_7_TRAINER_1_ITEM": 0x60ec0,
"Trainersanity_EVENT_BEAT_POKEMONTOWER_7_TRAINER_2_ITEM": 0x60ece,
"Gift_Aerodactyl": 0x610c8,
"Gift_Omanyte": 0x610cc,
"Gift_Kabuto": 0x610d0,
"Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_0_ITEM": 0x611b3,
"Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_1_ITEM": 0x611c1,
"Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_2_ITEM": 0x611cf,
"Missable_Viridian_Forest_Item_1": 0x61296,
"Missable_Viridian_Forest_Item_2": 0x6129d,
"Missable_Viridian_Forest_Item_3": 0x612a4,
"Starter2_M": 0x614ba,
"Starter3_M": 0x614c2,
"Trainersanity_EVENT_BEAT_SS_ANNE_5_TRAINER_0_ITEM": 0x61748,
"Trainersanity_EVENT_BEAT_SS_ANNE_5_TRAINER_1_ITEM": 0x61756,
"Event_SS_Anne_Captain": 0x61931,
"Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_0_ITEM": 0x61a20,
"Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_1_ITEM": 0x61a2e,
"Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_2_ITEM": 0x61a3c,
"Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_3_ITEM": 0x61a4a,
"Missable_SS_Anne_1F_Item": 0x61b36,
"Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_0_ITEM": 0x61c07,
"Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_1_ITEM": 0x61c15,
"Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_2_ITEM": 0x61c23,
"Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_3_ITEM": 0x61c31,
"Missable_SS_Anne_2F_Item_1": 0x61d6b,
"Missable_SS_Anne_2F_Item_2": 0x61d7e,
"Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_0_ITEM": 0x61e0f,
"Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_1_ITEM": 0x61e1d,
"Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_2_ITEM": 0x61e2b,
"Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_3_ITEM": 0x61e39,
"Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_4_ITEM": 0x61e47,
"Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_5_ITEM": 0x61e55,
"Missable_SS_Anne_B1F_Item_1": 0x61f6d,
"Missable_SS_Anne_B1F_Item_2": 0x61f74,
"Missable_SS_Anne_B1F_Item_3": 0x61f7b,
"Trainersanity_EVENT_BEAT_SILPH_CO_11F_TRAINER_0_ITEM": 0x6233c,
"Trainersanity_EVENT_BEAT_SILPH_CO_11F_TRAINER_1_ITEM": 0x6234a,
"Event_Silph_Co_President": 0x6235d,
"Ghost_Battle4": 0x708e1,
"Badge_Viridian_Gym": 0x749f7,
"Event_Viridian_Gym": 0x74a0b,

View File

@@ -119,7 +119,8 @@ def set_rules(world, player):
"Route 10 - Hidden Item Bush": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Rocket Hideout B1F - Hidden Item Pot Plant": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Rocket Hideout B3F - Hidden Item Near East Item": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Rocket Hideout B4F - Hidden Item Behind Giovanni": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Rocket Hideout B4F - Hidden Item Behind Giovanni (Lift Key)": lambda state:
state.pokemon_rb_can_get_hidden_items(player) and state.has("Lift Key", player),
"Pokemon Tower 5F - Hidden Item Near West Staircase": lambda state: state.pokemon_rb_can_get_hidden_items(
player),
"Route 13 - Hidden Item Dead End Bush": lambda state: state.pokemon_rb_can_get_hidden_items(player),

View File

@@ -1,4 +1,4 @@
from BaseClasses import MultiWorld, CollectionState
from BaseClasses import CollectionState, MultiWorld
def get_upgrade_total(multiworld: MultiWorld, player: int) -> int:
@@ -20,7 +20,7 @@ def has_upgrade_amount(state: CollectionState, player: int, amount: int) -> bool
def has_upgrades_percentage(state: CollectionState, player: int, percentage: float) -> bool:
return has_upgrade_amount(state, player, get_upgrade_total(state.multiworld, player) * (round(percentage) // 100))
return has_upgrade_amount(state, player, round(get_upgrade_total(state.multiworld, player) * (percentage / 100)))
def has_movement_rune(state: CollectionState, player: int) -> bool:

View File

@@ -1,12 +1,12 @@
import string
from typing import Dict, List
from .Items import RiskOfRainItem, item_table, item_pool_weights
from .Locations import RiskOfRainLocation, item_pickups
from .Rules import set_rules
from BaseClasses import Region, RegionType, Entrance, Item, ItemClassification, MultiWorld, Tutorial
from .Options import ror2_options, ItemWeights
from worlds.AutoWorld import World, WebWorld
from BaseClasses import Entrance, Item, ItemClassification, MultiWorld, Region, RegionType, Tutorial
from worlds.AutoWorld import WebWorld, World
from .Items import RiskOfRainItem, item_pool_weights, item_table
from .Locations import RiskOfRainLocation, item_pickups
from .Options import ItemWeights, ror2_options
from .Rules import set_rules
client_version = 1
@@ -36,7 +36,6 @@ class RiskOfRainWorld(World):
location_name_to_id = item_pickups
data_version = 4
forced_auto_forfeit = True
web = RiskOfWeb()
total_revivals: int

View File

@@ -873,7 +873,7 @@ def connect_regions(world, player, gates: typing.List[LevelGate], cannon_core_em
gates_len = len(gates)
if gates_len >= 2:
connect(world, player, names, 'Menu', LocationName.gate_1_boss_region,
connect(world, player, names, LocationName.gate_0_region, LocationName.gate_1_boss_region,
lambda state: (state.has(ItemName.emblem, player, gates[1].gate_emblem_count)))
if gate_bosses[1] == all_gate_bosses_table[king_boom_boo]:
@@ -886,7 +886,7 @@ def connect_regions(world, player, gates: typing.List[LevelGate], cannon_core_em
connect(world, player, names, LocationName.gate_1_region, shuffleable_regions[gates[1].gate_levels[i]])
if gates_len >= 3:
connect(world, player, names, 'Menu', LocationName.gate_2_boss_region,
connect(world, player, names, LocationName.gate_1_region, LocationName.gate_2_boss_region,
lambda state: (state.has(ItemName.emblem, player, gates[2].gate_emblem_count)))
if gate_bosses[2] == all_gate_bosses_table[king_boom_boo]:
@@ -899,7 +899,7 @@ def connect_regions(world, player, gates: typing.List[LevelGate], cannon_core_em
connect(world, player, names, LocationName.gate_2_region, shuffleable_regions[gates[2].gate_levels[i]])
if gates_len >= 4:
connect(world, player, names, 'Menu', LocationName.gate_3_boss_region,
connect(world, player, names, LocationName.gate_2_region, LocationName.gate_3_boss_region,
lambda state: (state.has(ItemName.emblem, player, gates[3].gate_emblem_count)))
if gate_bosses[3] == all_gate_bosses_table[king_boom_boo]:
@@ -912,7 +912,7 @@ def connect_regions(world, player, gates: typing.List[LevelGate], cannon_core_em
connect(world, player, names, LocationName.gate_3_region, shuffleable_regions[gates[3].gate_levels[i]])
if gates_len >= 5:
connect(world, player, names, 'Menu', LocationName.gate_4_boss_region,
connect(world, player, names, LocationName.gate_3_region, LocationName.gate_4_boss_region,
lambda state: (state.has(ItemName.emblem, player, gates[4].gate_emblem_count)))
if gate_bosses[4] == all_gate_bosses_table[king_boom_boo]:
@@ -925,7 +925,7 @@ def connect_regions(world, player, gates: typing.List[LevelGate], cannon_core_em
connect(world, player, names, LocationName.gate_4_region, shuffleable_regions[gates[4].gate_levels[i]])
if gates_len >= 6:
connect(world, player, names, 'Menu', LocationName.gate_5_boss_region,
connect(world, player, names, LocationName.gate_4_region, LocationName.gate_5_boss_region,
lambda state: (state.has(ItemName.emblem, player, gates[5].gate_emblem_count)))
if gate_bosses[5] == all_gate_bosses_table[king_boom_boo]:

View File

@@ -24,7 +24,7 @@ class SA2BWeb(WebWorld):
"English",
"setup_en.md",
"setup/en",
["RaspberrySpaceJam", "PoryGone"]
["RaspberrySpaceJam", "PoryGone", "Entiss"]
)
tutorials = [setup_en]

View File

@@ -16,7 +16,7 @@
- Quality of life mods
- SA2 Volume Controls from: [SA2 Volume Controls Release Page] (https://gamebanana.com/mods/381193)
## Installation Procedures
## Installation Procedures (Windows)
1. Install Sonic Adventure 2: Battle from Steam.
@@ -32,6 +32,26 @@
7. Launch the `SA2ModManager.exe` and make sure the SA2B_Archipelago mod is listed and enabled.
## Installation Procedures (Linux and Steam Deck)
1. Install Sonic Adventure 2: Battle from Steam.
2. In the properties for Sonic Adventure 2 on Steam, force the use of Proton Experimental as the compatibility tool.
3. Launch the game at least once without mods.
4. Install Sonic Adventure 2 Mod Loader as per its instructions. To launch it, add ``SA2ModManager.exe`` as a non-Steam game. In the properties on Steam for Sonic Adventure 2 Mod Loader, set it to use Proton as the compatibility tool.
5. The folder you installed the Sonic Adventure 2 Mod Loader into will now have a `/mods` directory.
6. Unpack the Archipelago Mod into this folder, so that `/mods/SA2B_Archipelago` is a valid path.
7. In the SA2B_Archipelago folder, copy the `APCpp.dll` file and paste it in the Sonic Adventure 2 install folder (where `SA2ModManager.exe` is).
8. Launch the `SA2ModManager.exe` from Steam and make sure the SA2B_Archipelago mod is listed and enabled.
Note: Ensure that you launch Sonic Adventure 2 from Steam directly on Linux, rather than launching using the `Save & Play` button in Sonic Adventure 2 Mod Loader.
## Joining a MultiWorld Game
1. Before launching the game, run the `SA2ModManager.exe`, select the SA2B_Archipelago mod, and hit the `Configure...` button.
@@ -42,7 +62,7 @@
4. For the `Password` field under `AP Settings`, enter the server password if one exists, otherwise leave blank.
5. Click The `Save` button then hit `Save & Play` to launch the game.
5. Click The `Save` button then hit `Save & Play` to launch the game. On Linux, launch Sonic Adventure 2 from Steam directly rather than using `Save & Play`.
6. Create a new save to connect to the MultiWorld game. A "Connected to Archipelago" message will appear if you sucessfully connect. If you close the game during play, you can reconnect to the MultiWorld game by selecting the same save file slot.

View File

@@ -39,7 +39,7 @@ class SMCollectionState(metaclass=AutoLogicRegister):
# for unit tests where MultiWorld is instantiated before worlds
if hasattr(parent, "state"):
self.smbm = {player: SMBoolManager(player, parent.state.smbm[player].maxDiff,
parent.state.smbm[player].onlyBossLeft) for player in
parent.state.smbm[player].onlyBossLeft, parent.state.smbm[player].lastAP) for player in
parent.get_game_players("Super Metroid")}
for player, group in parent.groups.items():
if (group["game"] == "Super Metroid"):
@@ -116,7 +116,7 @@ class SMWorld(World):
Logic.factory('vanilla')
self.variaRando = VariaRandomizer(self.multiworld, get_base_rom_path(), self.player)
self.multiworld.state.smbm[self.player] = SMBoolManager(self.player, self.variaRando.maxDifficulty)
self.multiworld.state.smbm[self.player] = SMBoolManager(self.player, self.variaRando.maxDifficulty, lastAP = self.variaRando.args.startLocation)
# keeps Nothing items local so no player will ever pickup Nothing
# doing so reduces contribution of this world to the Multiworld the more Nothing there is though
@@ -628,6 +628,11 @@ class SMWorld(World):
def collect(self, state: CollectionState, item: Item) -> bool:
state.smbm[self.player].addItem(item.type)
if (item.location != None and item.location.player == self.player):
for entrance in self.multiworld.get_region(item.location.parent_region.name, self.player).entrances:
if (entrance.parent_region.can_reach(state)):
state.smbm[self.player].lastAP = entrance.parent_region.name
break
return super(SMWorld, self).collect(state, item)
def remove(self, state: CollectionState, item: Item) -> bool:
@@ -736,18 +741,34 @@ class SMLocation(Location):
super(SMLocation, self).__init__(player, name, address, parent)
def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool:
return self.always_allow(state, item) or (self.item_rule(item) and (not check_access or (self.can_reach(state) and self.can_comeback(state, item))))
return self.always_allow(state, item) or (self.item_rule(item) and (not check_access or self.can_reach(state)))
def can_reach(self, state: CollectionState) -> bool:
# self.access_rule computes faster on average, so placing it first for faster abort
assert self.parent_region, "Can't reach location without region"
return self.access_rule(state) and self.parent_region.can_reach(state) and self.can_comeback(state)
def can_comeback(self, state: CollectionState):
# some specific early/late game checks
if self.name == 'Bomb' or self.name == 'Mother Brain':
return True
def can_comeback(self, state: CollectionState, item: Item):
randoExec = state.multiworld.worlds[self.player].variaRando.randoExec
n = 2 if GraphUtils.isStandardStart(randoExec.graphSettings.startAP) else 3
# is early game
if (len([loc for loc in state.locations_checked if loc.player == self.player]) <= n):
return True
for key in locationsDict[self.name].AccessFrom.keys():
if (randoExec.areaGraph.canAccessList( state.smbm[self.player],
key,
[randoExec.graphSettings.startAP, 'Landing Site'] if not GraphUtils.isStandardStart(randoExec.graphSettings.startAP) else ['Landing Site'],
state.smbm[self.player].maxDiff)):
smbm = state.smbm[self.player]
if (randoExec.areaGraph.canAccess( smbm,
smbm.lastAP,
key,
smbm.maxDiff,
None)):
return True
return False
class SMItem(Item):
game = "Super Metroid"

View File

@@ -367,22 +367,6 @@ class AccessGraph(object):
#print("canAccess: {}".format(can))
return can
# test access from an access point to a list of others, given an optional item
def canAccessList(self, smbm, srcAccessPointName, destAccessPointNameList, maxDiff, item=None):
if item is not None:
smbm.addItem(item)
#print("canAccess: item: {}, src: {}, dest: {}".format(item, srcAccessPointName, destAccessPointName))
destAccessPointList = [self.accessPoints[destAccessPointName] for destAccessPointName in destAccessPointNameList]
srcAccessPoint = self.accessPoints[srcAccessPointName]
availAccessPoints = self.getAvailableAccessPoints(srcAccessPoint, smbm, maxDiff, item)
can = any(ap in availAccessPoints for ap in destAccessPointList)
# if not can:
# self.log.debug("canAccess KO: avail = {}".format([ap.Name for ap in availAccessPoints.keys()]))
if item is not None:
smbm.removeItem(item)
#print("canAccess: {}".format(can))
return can
# returns a list of AccessPoint instances from srcAccessPointName to destAccessPointName
# (not including source ap)
# or None if no possible path

View File

@@ -13,7 +13,7 @@ class SMBoolManager(object):
items = ['ETank', 'Missile', 'Super', 'PowerBomb', 'Bomb', 'Charge', 'Ice', 'HiJump', 'SpeedBooster', 'Wave', 'Spazer', 'SpringBall', 'Varia', 'Plasma', 'Grapple', 'Morph', 'Reserve', 'Gravity', 'XRayScope', 'SpaceJump', 'ScrewAttack', 'Nothing', 'NoEnergy', 'MotherBrain', 'Hyper'] + Bosses.Golden4()
countItems = ['Missile', 'Super', 'PowerBomb', 'ETank', 'Reserve']
def __init__(self, player=0, maxDiff=sys.maxsize, onlyBossLeft = False):
def __init__(self, player=0, maxDiff=sys.maxsize, onlyBossLeft = False, lastAP = 'Landing Site'):
self._items = { }
self._counts = { }
@@ -21,6 +21,8 @@ class SMBoolManager(object):
self.maxDiff = maxDiff
self.onlyBossLeft = onlyBossLeft
self.lastAP = lastAP
# cache related
self.cacheKey = 0
self.computeItemsPositions()

View File

@@ -10,7 +10,7 @@ from rando.ItemLocContainer import ItemLocation
# Holds settings not related to graph layout.
class RandoSettings(object):
def __init__(self, maxDiff, progSpeed, progDiff, qty, restrictions,
superFun, runtimeLimit_s, plandoSettings, minDiff):
superFun, runtimeLimit_s, PlandoOptions, minDiff):
self.progSpeed = progSpeed.lower()
self.progDiff = progDiff.lower()
self.maxDiff = maxDiff
@@ -20,7 +20,7 @@ class RandoSettings(object):
self.runtimeLimit_s = runtimeLimit_s
if self.runtimeLimit_s <= 0:
self.runtimeLimit_s = sys.maxsize
self.plandoSettings = plandoSettings
self.PlandoOptions = PlandoOptions
self.minDiff = minDiff
def getSuperFun(self):
@@ -30,7 +30,7 @@ class RandoSettings(object):
self.superFun = superFun[:]
def isPlandoRando(self):
return self.plandoSettings is not None
return self.PlandoOptions is not None
def getItemManager(self, smbm, nLocs):
if not self.isPlandoRando():
@@ -43,20 +43,20 @@ class RandoSettings(object):
return None
exclude = {'alreadyPlacedItems': defaultdict(int), 'forbiddenItems': []}
# locsItems is a dict {'loc name': 'item type'}
for locName,itemType in self.plandoSettings["locsItems"].items():
for locName,itemType in self.PlandoOptions["locsItems"].items():
if not any(loc.Name == locName for loc in locations):
continue
exclude['alreadyPlacedItems'][itemType] += 1
exclude['alreadyPlacedItems']['total'] += 1
exclude['forbiddenItems'] = self.plandoSettings['forbiddenItems']
exclude['forbiddenItems'] = self.PlandoOptions['forbiddenItems']
return exclude
def collectAlreadyPlacedItemLocations(self, container):
if not self.isPlandoRando():
return
for locName,itemType in self.plandoSettings["locsItems"].items():
for locName,itemType in self.PlandoOptions["locsItems"].items():
if not any(loc.Name == locName for loc in container.unusedLocations):
continue
item = container.getNextItemInPool(itemType)

View File

@@ -621,7 +621,7 @@ class VariaRandomizer:
self.ctrlDict = { getattr(ctrl, button) : button for button in ctrlButton }
args.moonWalk = ctrl.Moonwalk
plandoSettings = None
PlandoOptions = None
if args.plandoRando is not None:
forceArg('progressionSpeed', 'speedrun', "'Progression Speed' forced to speedrun")
progSpeed = 'speedrun'
@@ -632,10 +632,10 @@ class VariaRandomizer:
args.plandoRando = json.loads(args.plandoRando)
RomPatches.ActivePatches[self.player] = args.plandoRando["patches"]
DoorsManager.unserialize(args.plandoRando["doors"])
plandoSettings = {"locsItems": args.plandoRando['locsItems'], "forbiddenItems": args.plandoRando['forbiddenItems']}
PlandoOptions = {"locsItems": args.plandoRando['locsItems'], "forbiddenItems": args.plandoRando['forbiddenItems']}
randoSettings = RandoSettings(self.maxDifficulty, progSpeed, progDiff, qty,
restrictions, args.superFun, args.runtimeLimit_s,
plandoSettings, minDifficulty)
PlandoOptions, minDifficulty)
# print some parameters for jm's stats
if args.jm == True:

Binary file not shown.

View File

@@ -1,12 +1,12 @@
import string
from BaseClasses import Item, MultiWorld, Region, Location, Entrance, Tutorial, ItemClassification, RegionType
from .Items import item_table, item_pool, event_item_pairs
from BaseClasses import Entrance, Item, ItemClassification, Location, MultiWorld, Region, RegionType, Tutorial
from .Items import event_item_pairs, item_pool, item_table
from .Locations import location_table
from .Options import spire_options
from .Regions import create_regions
from .Rules import set_rules
from ..AutoWorld import World, WebWorld
from .Options import spire_options
from ..AutoWorld import WebWorld, World
class SpireWeb(WebWorld):
@@ -36,8 +36,6 @@ class SpireWorld(World):
item_name_to_id = {name: data.code for name, data in item_table.items()}
location_name_to_id = location_table
forced_auto_forfeit = True
def _get_slot_data(self):
return {
'seed': "".join(self.multiworld.slot_seeds[self.player].choice(string.ascii_letters) for i in range(16)),

View File

@@ -31,8 +31,8 @@ class SwimRule(Choice):
class EarlySeaglide(DefaultOnToggle):
display_name = "Early Seaglide"
"""Make sure 2 of the Seaglide Fragments are available in or near the Safe Shallows (Sphere 1 Locations)."""
display_name = "Early Seaglide"
class ItemPool(Choice):

View File

@@ -152,7 +152,7 @@ def has_ultra_glide_fins(state: "CollectionState", player: int) -> bool:
def get_max_swim_depth(state: "CollectionState", player: int) -> int:
swim_rule: SwimRule = state.multiworld.swim_rule[player]
depth: int = swim_rule.base_depth
if swim_rule == swim_rule.consider_items:
if swim_rule.consider_items:
if has_seaglide(state, player):
if has_ultra_high_capacity_tank(state, player):
depth += 350 # It's about 800m. Give some room

View File

@@ -42,12 +42,12 @@ class SubnauticaWorld(World):
option_definitions = Options.options
data_version = 8
required_client_version = (0, 3, 7)
required_client_version = (0, 3, 8)
creatures_to_scan: List[str]
def generate_early(self) -> None:
if self.multiworld.early_seaglide:
if self.multiworld.early_seaglide[self.player]:
self.multiworld.local_early_items[self.player]["Seaglide Fragment"] = 2
scan_option: Options.AggressiveScanLogic = self.multiworld.creature_scan_logic[self.player]

View File

@@ -12,6 +12,8 @@
2. Start Subnautica. You should see a connect form with three text boxes in the top left of your main menu.
**If using Linux,** add ``WINEDLLOVERRIDES="winhttp=n,b" %command%`` to your Subnautica launch arguments on Steam. If you bought Subnautica elsewhere, you can either add it as a non-steam game and use those launch arguments or use winecfg to set the dll override.
## Connecting
Use the connect form in Subnautica's main menu to enter your connection information to connect to an Archipelago multiworld.
@@ -33,18 +35,19 @@ Warning: Currently it is not checked whether a loaded savegame belongs to the mu
## Console Commands
The mod adds the following console commands:
- `say` sends the text following it to Archipelago as a chat message. ! is not an allowed character, use / instead.
- `silent` toggles Archipelago chat messages appearing.
- `say` sends the text following it to Archipelago as a chat message.
- `!` is not an allowed character, use `/` in its place. For example, to use the [`!hint` command](/tutorial/Archipelago/commands/en#remote-commands), type `say /hint`.
- `silent` toggles Archipelago messages appearing.
- `tracker` rotates through the possible settings for the in-game tracker that displays the closest uncollected location.
- `deathlink` toggles death link.
To enable the console in Subnautica, press `F3` and `F8`, then uncheck "Disable Console" in the top left. Press `F3` and `F8` again to close the menus.
To enter a console command, press `Enter`.
To enable the console in Subnautica, press `Shift+Enter`.
## Known Issues
- Do not attempt playing vanilla saves while the mod is installed, as the mod will override the scan information of the savegame.
- When exiting to the main menu the mod's state is not properly reset. Loading a savegame from here will break various things.
If you want to reload a save it is recommended you restart the game entirely.
If you want to reload a save it is recommended you relaunch the game entirely.
- Attempting to load a savegame containing no longer valid connection information without entering valid information on the main menu will hang on the loading screen.
## Troubleshooting

View File

@@ -60,7 +60,7 @@ class ShuffleVaultBoxes(Toggle):
class ShufflePostgame(Toggle):
"""Adds locations into the pool that are guaranteed to become accessible after or at the same time as your goal.
Use this if you don't play with forfeit on victory. IMPORTANT NOTE: The possibility of your second
Use this if you don't play with release on victory. IMPORTANT NOTE: The possibility of your second
"Progressive Dots" showing up in the Caves is ignored, they will still be considered "postgame" in base settings."""
display_name = "Shuffle Postgame"

View File

@@ -1 +1 @@
git+https://github.com/beauxq/zilliandomizer@a0fd0a1199c0234756b8637420c1e46434c687f0#egg=zilliandomizer==0.5.0
git+https://github.com/beauxq/zilliandomizer@23d938daa5aaeaa3ec2255c50f2729b33a9e7f87#egg=zilliandomizer==0.5.1