Compare commits

...

39 Commits
0.3.0 ... 0.3.1

Author SHA1 Message Date
Chris Wilson
07ff0f1026 [WebHost] Fix /user-content styles (#408) 2022-04-03 20:16:15 -04:00
Fabian Dill
a080288e3e Core: update version (#407) 2022-04-03 19:39:01 -04:00
Fabian Dill
71bd87f293 HK: don't flag maps as progression 2022-04-03 19:38:39 -04:00
Fabian Dill
574e2abba8 HK: write shop prices to spoiler log 2022-04-03 19:38:39 -04:00
Hussein Farran
cffa772801 Fix unit test and generation failures. Whoops. 2022-04-03 19:38:39 -04:00
Fabian Dill
66bd793306 HK: add item name groups 2022-04-03 19:38:39 -04:00
Hussein Farran
0eb37883ca Add docstrings to hollow knight YAML options. 2022-04-03 19:38:39 -04:00
Hussein Farran
356384ab05 Add Hollow Knight setup guide, game info, and to README 2022-04-03 19:38:39 -04:00
Fabian Dill
8c2c6877b6 HK: sort shop contents by cost 2022-04-03 19:38:39 -04:00
Fabian Dill
d1d40d8a60 HK: ignore relics logic
HK: write sets ordered, to reduce history changes
2022-04-03 19:38:39 -04:00
Fabian Dill
b026a0a372 HK: write charm costs to spoiler 2022-04-03 19:38:39 -04:00
Fabian Dill
73bcd0058a HK: force disabled options to actually be disabled 2022-04-03 19:38:39 -04:00
Fabian Dill
0cf396e5d6 HK: account for "Start" location in another place 2022-04-03 19:38:39 -04:00
Fabian Dill
1bc09d4292 make black sliver happy 2022-04-03 19:38:39 -04:00
Fabian Dill
97d0c51db1 HK: allow webgen 2022-04-03 19:38:39 -04:00
Fabian Dill
ed1c11267c Options: loudly crash if random text is not recognized, instead of de… (#401)
* Options: loudly crash if random text is not recognized, instead of defaulting to full "random"

* Update Options.py

Co-authored-by: Hussein Farran <hmfarran@gmail.com>

Co-authored-by: Hussein Farran <hmfarran@gmail.com>
2022-04-03 19:37:57 -04:00
Fabian Dill
a3e1ac896f Generate: don't fail on marked utf-8 files (#399)
utf-8-sig will fallback to non-sig automatically
2022-04-03 15:55:46 -04:00
Zach Parks
37d9eb2752 Added filesafe player name function and updated generator functions in all worlds to use filesafe player name during output
Thanks Windows for your bad filesystem.
2022-04-03 20:45:44 +02:00
CaitSith2
05e267a0bd Prevent use of old collection clients without boss collection blocklist. (#406) 2022-04-03 14:45:06 -04:00
Fabian Dill
d1f0a29a02 OoT: fix patching deltas when run from another folder 2022-04-03 20:44:27 +02:00
Fabian Dill
fb2e780c56 LttP/SMZ3: some more file ending fixes (#393) 2022-04-03 13:42:18 -04:00
Fabian Dill
ba3257f850 ItemLinks: prevent attempts at cross-game (#402) 2022-04-03 13:09:05 -04:00
Fabian Dill
215d5e9adf AutoWorld: ensure WebWorld is instantiated, preventing an easy mistake. (#404) 2022-04-03 13:08:50 -04:00
black-sliver
5392b32d5c SoE: WebWorld theme and fix long standing bug (#397) 2022-04-03 04:48:43 +02:00
alwaysintreble
4dd0a75914 multiworld tracker: properly fix item link breaking tracker 2022-04-03 02:03:48 +02:00
CaitSith2
a2212002ae Link to the Past Block collection of bosses. (#395) 2022-04-03 01:39:28 +02:00
lordlou
91ccee3513 [SM] remote item back compat fix (#400) 2022-04-03 01:36:31 +02:00
black-sliver
2a593d5d0a CI: add windows build action
set setuptools to 60.x until the issue is resolved
change retention to 7 days
2022-04-02 04:49:42 +02:00
black-sliver
a93b3d79aa Minecraft fixes (#388) 2022-04-02 04:49:27 +02:00
black-sliver
938ab32cda CI: bigger unittest matrix 2022-04-02 00:17:53 +02:00
Jarno Westhof
6f5ab05345 [Docs] Added WebWorld Theme (#387) 2022-04-01 22:39:39 +02:00
Chris Wilson
95f8647f09 Added 50 items to ArchipIDLE (#385) 2022-04-01 10:04:42 -04:00
jonloveslegos
06c8caa3cc Fixed checksfinder client failing when getting an item before sending one, and fixed checksfinder client not appearing in the installer (#383) 2022-04-01 07:55:06 +02:00
Fabian Dill
d206a562df rename ChecksFinder folder (#380) 2022-04-01 01:17:46 -04:00
Fabian Dill
a0a290e481 Setup: fix OoT conditions for file page (#381) 2022-04-01 01:17:26 -04:00
Fabian Dill
266ff0c520 Setup: fix apparently forgotten file endings (#382)
I feel like I did this... mh.
2022-04-01 01:16:54 -04:00
Fabian Dill
931bf7da16 SMZ3: fix loading TextScript on systems that don't default to something utf-8 compatible (#384) 2022-04-01 01:14:20 -04:00
black-sliver
fe4a26d034 CI: add Generate.py tests
* allows ModuleUpdate to be run outside of local_dir
* adds windows-latest to the unittest matrix
2022-04-01 06:16:13 +02:00
Zach Parks
dca70a99ad Webhost - Update copyright year 2022-04-01 04:46:02 +02:00
57 changed files with 600 additions and 157 deletions

View File

@@ -5,9 +5,41 @@ name: Build
on: workflow_dispatch
jobs:
# build-release-windows: # LF volunteer; RCs will still be built and signed by hand
# build-release-macos: # LF volunteer
build-win-py38: # RCs will still be built and signed by hand
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- name: Install python
uses: actions/setup-python@v3
with:
python-version: '3.8'
- name: Download run-time dependencies
run: |
Invoke-WebRequest -Uri https://github.com/alttpo/sni/releases/download/v0.0.79/sni-v0.0.79-windows-amd64.zip -OutFile sni.zip
Expand-Archive -Path sni.zip -DestinationPath SNI -Force
Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/6.4/win-x64.zip -OutFile enemizer.zip
Expand-Archive -Path enemizer.zip -DestinationPath EnemizerCLI -Force
- name: Build
run: |
python -m pip install --upgrade pip setuptools==60.10.0 # 61 does not work with the current layout
pip install -r requirements.txt
python setup.py build --yes
$NAME="$(ls build)".Split('.',2)[1]
$ZIP_NAME="Archipelago_$NAME.7z"
echo "ZIP_NAME=$ZIP_NAME" >> $Env:GITHUB_ENV
New-Item -Path dist -ItemType Directory -Force
cd build
Rename-Item exe.$NAME Archipelago
7z a -mx=9 -mhe=on -ms "../dist/$ZIP_NAME" Archipelago
- name: Store 7z
uses: actions/upload-artifact@v2
with:
name: ${{ env.ZIP_NAME }}
path: dist/${{ env.ZIP_NAME }}
retention-days: 7 # keep for 7 days, should be enough
build-ubuntu1804:
runs-on: ubuntu-18.04
steps:
@@ -39,7 +71,8 @@ jobs:
7za x -oEnemizerCLI/ ubuntu.16.04-x64.7z
- name: Build
run: |
"${{ env.PYTHON }}" -m pip install --upgrade pip setuptools virtualenv PyGObject # pygobject should probably move to requirements
# pygobject is an optional dependency for kivy that's not in requirements
"${{ env.PYTHON }}" -m pip install --upgrade pip virtualenv PyGObject setuptools==60.10.0 # setuptools same as windows
"${{ env.PYTHON }}" -m venv venv
source venv/bin/activate
pip install -r requirements.txt
@@ -56,8 +89,10 @@ jobs:
with:
name: ${{ env.APPIMAGE_NAME }}
path: dist/${{ env.APPIMAGE_NAME }}
retention-days: 7
- name: Store .tar.gz
uses: actions/upload-artifact@v2
with:
name: ${{ env.TAR_NAME }}
path: dist/${{ env.TAR_NAME }}
retention-days: 7

View File

@@ -7,17 +7,22 @@ on: [push, pull_request]
jobs:
build:
runs-on: ${{ matrix.os }}
name: Test Python ${{ matrix.python.version }} ${{ matrix.os }}
runs-on: ubuntu-latest
name: Test Python ${{ matrix.python.version }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
python:
- {version: '3.8'}
- {version: '3.9'}
#- {version: '3.10'}
- {version: '3.10'}
include:
- python: {version: '3.8'} # win7 compat
os: windows-latest
- python: {version: '3.10'} # current
os: windows-latest
steps:
- uses: actions/checkout@v2

View File

@@ -212,6 +212,8 @@ class MultiWorld():
for player in self.player_ids:
for item_link in self.item_links[player].value:
if item_link["name"] in item_links:
if item_link["game"] != self.game[player]:
raise Exception(f"Cannot ItemLink across games. Link: {item_link['name']}")
item_links[item_link["name"]]["players"][player] = item_link["replacement_item"]
item_links[item_link["name"]]["item_pool"] &= set(item_link["item_pool"])
else:
@@ -267,6 +269,9 @@ class MultiWorld():
def get_player_name(self, player: int) -> str:
return self.player_name[player]
def get_file_safe_player_name(self, player: int) -> str:
return ''.join(c for c in self.get_player_name(player) if c not in '<>:"/\\|?*')
def initialize_regions(self, regions=None):
for region in regions if regions else self.regions:
region.world = self

View File

@@ -476,6 +476,8 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
raise Exception('Connection refused by the multiworld host, no reason provided')
elif cmd == 'Connected':
if not os.path.exists(os.path.expandvars(r"%localappdata%/ChecksFinder")):
os.mkdir(os.path.expandvars(r"%localappdata%/ChecksFinder"))
ctx.team = args["team"]
ctx.slot = args["slot"]
ctx.consume_players_package(args["players"])

View File

@@ -217,10 +217,10 @@ def main(args=None, callback=ERmain):
def read_weights_yaml(path):
try:
if urllib.parse.urlparse(path).scheme in ('https', 'file'):
yaml = str(urllib.request.urlopen(path).read(), "utf-8")
yaml = str(urllib.request.urlopen(path).read(), "utf-8-sig")
else:
with open(path, 'rb') as f:
yaml = str(f.read(), "utf-8")
yaml = str(f.read(), "utf-8-sig")
except Exception as e:
raise Exception(f"Failed to read weights ({path})") from e

View File

@@ -58,9 +58,9 @@ def open_patch():
suffixes += c.file_identifier.suffixes if c.type == Type.CLIENT and \
isinstance(c.file_identifier, SuffixIdentifier) else []
filename = tkinter.filedialog.askopenfilename(filetypes=(('Patches', ' '.join(suffixes)),))
file, component = identify(filename)
file, _, component = identify(filename)
if file and component:
subprocess.Popen([*get_exe(component), file])
launch([*get_exe(component), file], component.cli)
def browse_files():
@@ -142,7 +142,7 @@ components: Iterable[Component] = (
# Factorio
Component('Factorio Client', 'FactorioClient'),
# Minecraft
Component('Minecraft Client', 'MinecraftClient', icon='mcicon',
Component('Minecraft Client', 'MinecraftClient', icon='mcicon', cli=True,
file_identifier=SuffixIdentifier('.apmc')),
# Ocarina of Time
Component('OoT Client', 'OoTClient',
@@ -165,11 +165,11 @@ icon_paths = {
def identify(path: Union[None, str]):
if path is None:
return None, None
return None, None, None
for component in components:
if component.handles_file(path):
return path, component.script_name
return (None, None) if '/' in path or '\\' in path else (None, path)
return path, component.script_name, component
return (None, None, None) if '/' in path or '\\' in path else (None, path, None)
def get_exe(component: Union[str, Component]) -> Optional[Sequence[str]]:
@@ -284,7 +284,7 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None):
args = {}
if "Patch|Game|Component" in args:
file, component = identify(args["Patch|Game|Component"])
file, component, _ = identify(args["Patch|Game|Component"])
if file:
args['file'] = file
if component:

View File

@@ -130,7 +130,7 @@ def adjust(args):
logger = logging.getLogger('Adjuster')
logger.info('Patching ROM.')
vanillaRom = args.baserom
if os.path.splitext(args.rom)[-1].lower() == '.apbp':
if os.path.splitext(args.rom)[-1].lower() in {'.apbp', '.aplttp'}:
import Patch
meta, args.rom = Patch.create_rom_file(args.rom)

View File

@@ -2,6 +2,7 @@ import argparse
import os, sys
import re
import atexit
import shutil
from subprocess import Popen
from shutil import copyfile
from time import strftime
@@ -16,6 +17,7 @@ atexit.register(input, "Press enter to exit.")
# 1 or more digits followed by m or g, then optional b
max_heap_re = re.compile(r"^\d+[mMgG][bB]?$")
forge_version = "1.17.1-37.1.1"
is_windows = sys.platform in ("win32", "cygwin", "msys")
def prompt_yes_no(prompt):
@@ -158,9 +160,15 @@ def find_jdk_dir() -> str:
# get the java exe location
def find_jdk() -> str:
jdk = find_jdk_dir()
jdk_exe = os.path.join(jdk, "bin", "java.exe")
if os.path.isfile(jdk_exe):
if is_windows:
jdk = find_jdk_dir()
jdk_exe = os.path.join(jdk, "bin", "java.exe")
if os.path.isfile(jdk_exe):
return jdk_exe
else:
jdk_exe = shutil.which(options["minecraft_options"].get("java", "java"))
if not jdk_exe:
raise Exception("Could not find Java. Is Java installed on the system?")
return jdk_exe
@@ -203,7 +211,7 @@ def install_forge(directory: str):
f.write(resp.content)
print(f"Installing Forge...")
argstring = ' '.join([jdk, "-jar", "\"" + forge_install_jar+ "\"", "--installServer", "\"" + directory + "\""])
install_process = Popen(argstring)
install_process = Popen(argstring, shell=not is_windows)
install_process.wait()
os.remove(forge_install_jar)
@@ -220,7 +228,8 @@ def run_forge_server(forge_dir: str, heap_arg):
heap_arg = heap_arg[:-1]
heap_arg = "-Xmx" + heap_arg
args_file = os.path.join(forge_dir, "libraries", "net", "minecraftforge", "forge", forge_version, "win_args.txt")
os_args = "win_args.txt" if is_windows else "unix_args.txt"
args_file = os.path.join(forge_dir, "libraries", "net", "minecraftforge", "forge", forge_version, os_args)
win_args = []
with open(args_file) as argfile:
for line in argfile:
@@ -229,7 +238,7 @@ def run_forge_server(forge_dir: str, heap_arg):
argstring = ' '.join([java_exe, heap_arg] + win_args + ["-nogui"])
logging.info(f"Running Forge server: {argstring}")
os.chdir(forge_dir)
return Popen(argstring)
return Popen(argstring, shell=not is_windows)
if __name__ == '__main__':
@@ -252,15 +261,21 @@ if __name__ == '__main__':
max_heap = options["minecraft_options"]["max_heap_size"]
if args.install:
print("Installing Java and Minecraft Forge")
download_java()
if is_windows:
print("Installing Java and Minecraft Forge")
download_java()
else:
print("Installing Minecraft Forge")
install_forge(forge_dir)
sys.exit(0)
if apmc_file is not None and not os.path.isfile(apmc_file):
raise FileNotFoundError(f"Path {apmc_file} does not exist or could not be accessed.")
if not os.path.isdir(forge_dir):
raise NotADirectoryError(f"Path {forge_dir} does not exist or could not be accessed.")
if prompt_yes_no("Did not find forge directory. Download and install forge now?"):
install_forge(forge_dir)
if not os.path.isdir(forge_dir):
raise NotADirectoryError(f"Path {forge_dir} does not exist or could not be accessed.")
if not max_heap_re.match(max_heap):
raise Exception(f"Max heap size {max_heap} in incorrect format. Use a number followed by M or G, e.g. 512M or 2G.")

View File

@@ -3,7 +3,8 @@ import sys
import subprocess
import pkg_resources
requirements_files = {'requirements.txt'}
local_dir = os.path.dirname(__file__)
requirements_files = {os.path.join(local_dir, 'requirements.txt')}
if sys.version_info < (3, 8, 6):
raise RuntimeError("Incompatible Python Version. 3.8.7+ is supported.")
@@ -11,7 +12,7 @@ if sys.version_info < (3, 8, 6):
update_ran = getattr(sys, "frozen", False) # don't run update if environment is frozen/compiled
if not update_ran:
for entry in os.scandir("worlds"):
for entry in os.scandir(os.path.join(local_dir, "worlds")):
if entry.is_dir():
req_file = os.path.join(entry.path, "requirements.txt")
if os.path.exists(req_file):

View File

@@ -226,7 +226,7 @@ async def patch_and_run_game(apz5_file):
decomp_path = base_name + '-decomp.z64'
comp_path = base_name + '.z64'
# Load vanilla ROM, patch file, compress ROM
rom = Rom(Utils.get_options()["oot_options"]["rom_file"])
rom = Rom(Utils.local_path(Utils.get_options()["oot_options"]["rom_file"]))
apply_patch_file(rom, apz5_file)
rom.write_to_file(decomp_path)
os.chdir(data_path("Compress"))

View File

@@ -256,8 +256,10 @@ class Range(Option[int], int):
return cls(int(round(random.triangular(random_range[0], random_range[1], random_range[1]))))
else:
return cls(int(round(random.randint(random_range[0], random_range[1]))))
else:
elif text == "random":
return cls(random.randint(cls.range_start, cls.range_end))
else:
raise Exception(f"random text \"{text}\" did not resolve to a recognized pattern. Acceptable values are: random, random-high, random-middle, random-low, random-range-low-<min>-<max>, random-range-middle-<min>-<max>, random-range-high-<min>-<max>, or random-range-<min>-<max>.")
return cls(int(text))
@classmethod

View File

@@ -21,6 +21,8 @@ Currently, the following games are supported:
* Meritous
* Super Metroid/Link to the Past combo randomizer (SMZ3)
* ChecksFinder
* ArchipIDLE
* Hollow Knight
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled

View File

@@ -504,6 +504,17 @@ location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
'Ganons Tower - Pre-Moldorm Chest': (0x3d, 0x40),
'Ganons Tower - Validation Chest': (0x4d, 0x10)}
boss_locations = {Regions.lookup_name_to_id[name] for name in {'Eastern Palace - Boss',
'Desert Palace - Boss',
'Tower of Hera - Boss',
'Palace of Darkness - Boss',
'Swamp Palace - Boss',
'Skull Woods - Boss',
"Thieves' Town - Boss",
'Ice Palace - Boss',
'Misery Mire - Boss',
'Turtle Rock - Boss'}}
location_table_uw_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_uw.items()}
location_table_npc = {'Mushroom': 0x1000,
@@ -890,7 +901,8 @@ async def track_locations(ctx: Context, roomid, roomdata):
uw_begin = min(uw_begin, roomid)
uw_end = max(uw_end, roomid + 1)
if location_id in ctx.checked_locations and location_id not in ctx.locations_checked and \
location_id in ctx.locations_info and ctx.locations_info[location_id].player != ctx.slot:
location_id in ctx.locations_info and ctx.locations_info[location_id].player != ctx.slot and \
location_id not in boss_locations:
uw_begin = min(uw_begin, roomid)
uw_end = max(uw_end, roomid + 1)
uw_checked[location_id] = (roomid, mask)
@@ -988,13 +1000,18 @@ async def game_watcher(ctx: Context):
if not ctx.rom:
ctx.finished_game = False
ctx.death_link_allow_survive = False
game_name = await snes_read(ctx, SM_ROMNAME_START, 2)
game_name = await snes_read(ctx, SM_ROMNAME_START, 5)
if game_name is None:
continue
elif game_name[:2] == b"SM":
ctx.game = GAME_SM
item_handling = await snes_read(ctx, SM_REMOTE_ITEM_FLAG_ADDR, 1)
ctx.items_handling = 0b001 if item_handling is None else item_handling[0]
# versions lower than 0.3.0 dont have item handling flag nor remote item support
romVersion = int(game_name[2:5].decode('UTF-8'))
if romVersion < 30:
ctx.items_handling = 0b001 # full local
else:
item_handling = await snes_read(ctx, SM_REMOTE_ITEM_FLAG_ADDR, 1)
ctx.items_handling = 0b001 if item_handling is None else item_handling[0]
else:
game_name = await snes_read(ctx, SMZ3_ROMNAME_START, 3)
if game_name == b"ZSM":
@@ -1148,7 +1165,7 @@ async def game_watcher(ctx: Context):
if itemOutPtr < len(ctx.items_received):
item = ctx.items_received[itemOutPtr]
itemId = item.item - items_start_id
locationId = (item.location - locations_start_id) if item.location >= 0 else 0x00
locationId = (item.location - locations_start_id) if item.location >= 0 and bool(ctx.items_handling & 0b010) else 0x00
playerID = item.player if item.player <= SM_ROM_PLAYER_LIMIT else 0
snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + itemOutPtr * 4, bytes(

View File

@@ -25,7 +25,7 @@ class Version(typing.NamedTuple):
build: int
__version__ = "0.3.0"
__version__ = "0.3.1"
version_tuple = tuplize_version(__version__)
from yaml import load, dump, SafeLoader

View File

@@ -0,0 +1,22 @@
# Hollow Knight
## Where is the settings page?
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
Randomization swaps around the locations of items. The items being swapped around are chosen within your YAML.
Shop costs are presently always randomized.
## What Hollow Knight items can appear in other players' worlds?
This is dependent entirely upon your YAML settings. Some examples include: charms, grubs, lifeblood cocoons, geo, etc.
## What does another world's item look like in Hollow Knight?
When the Hollow Knight player picks up an item from a location and it is an item for another game it will appear in that
player's recent items display as an item being sent to another player. If the item is for another Hollow Knight player
then the sprite will be that of the item's original sprite. If the item belongs to a player that is not playing Hollow
Knight then the sprite will be the Archipelago logo.

View File

@@ -0,0 +1,30 @@
# Hollow Knight for Archipelago Setup Guide
## Required Software
* Download and unzip the Scarab Mod Manager from the [Scarab GitHub Releases page](https://github.com/fifty-six/Scarab/releases).
* A legal copy of Hollow Knight, not purchased or played through XBox Game Pass.
* Unfortunately, the Game Pass version is not currently compatible with mods.
## Installing the Archipelago Mod using Scarab
1. Launch Scarab and ensure it locates your Hollow Knight installation directory.
2. Click the "Install" checkbox near the "Archipelago" mod entry.
3. Launch the game, you're all set!
## Configuring your YAML File
### What is a YAML and why do I need one?
You can see the [basic multiworld setup guide](/tutorial/Archipelago/setup/en) here on the Archipelago website to learn
about why Archipelago uses YAML files and what they're for.
### Where do I get a YAML?
You can use the [game settings page for Hollow Knight](/games/Hollow%20Knight/player-settings) here on the Archipelago
website to generate a YAML using a graphical interface.
### Joining an Archipelago Game in Hollow Knight
1. Start the game after installing all necessary mods.
2. Create a **new save game.**
3. Select the **Archipelago** game mode from the mode selection screen.
4. Enter the correct settings for your Archipelago server.
5. Hit **Start** to begin the game. The game will stall for a few seconds while it does all item placements.
6. The game will immediately drop you into the randomized game.
* If you are waiting for a countdown then wait for it to lapse before hitting Start.
* Or hit Start then pause the game once you're in it.

View File

@@ -555,5 +555,24 @@
]
}
]
},
{
"gameTitle": "Hollow Knight",
"tutorials": [
{
"name": "Mod Setup and Use Guide",
"description": "A guide to playing Hollow Knight with Archipelago.",
"files": [
{
"language": "English",
"filename": "Hollow Knight/setup_en.md",
"link": "Hollow Knight/setup/en",
"authors": [
"ijwu"
]
}
]
}
]
}
]

View File

@@ -44,11 +44,3 @@
#user-content table.dataTable{
width: unset;
}
table.dataTable thead th{
padding: 0 20px 0 0;
}
table.dataTable tbody td{
padding: 6px 20px 0 0;
}

View File

@@ -1,6 +1,6 @@
{% block footer %}
<footer id="island-footer">
<div id="copyright-notice">Copyright 2021 Archipelago</div>
<div id="copyright-notice">Copyright 2022 Archipelago</div>
<div id="links">
<a href="https://github.com/ArchipelagoMW/Archipelago">Source Code</a>
-

View File

@@ -4,12 +4,13 @@
{{ super() }}
<title>User Content</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/userContent.css") }}" />
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/userContent.js") }}"></script>
{% endblock %}
{% block body %}
{% include 'header/oceanHeader.html' %}
<div id="user-content-wrapper">
<div id="user-content-wrapper" class="markdown">
<div id="user-content" class="grass-island">
<h1>User Content</h1>
Below is a list of all the content you have generated on this site. Rooms and seeds are listed separately.
@@ -68,5 +69,4 @@
{% endif %}
</div>
</div>
{% include 'islandFooter.html' %}
{% endblock %}

View File

@@ -324,10 +324,7 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int, want
# If the player does not have the item, do nothing
for location in locations_checked:
if location in player_locations:
if len(player_locations[location]) == 3:
item, recipient, flags = player_locations[location]
else: # TODO: remove around version 0.2.5
item, recipient = player_locations[location]
item, recipient, flags = player_locations[location]
if recipient == tracked_player: # a check done for the tracked player
attribute_item_solo(inventory, item)
if ms_player == tracked_player: # a check done by the tracked player
@@ -392,10 +389,7 @@ def __renderAlttpTracker(multisave: Dict[str, Any], room: Room, locations: Dict[
player_small_key_locations = set()
for loc_data in locations.values():
for values in loc_data.values():
if len(values) == 3:
item_id, item_player, flags = values
else: # TODO: remove around version 0.2.5
item_id, item_player = values
item_id, item_player, flags = values
if item_player == player:
if item_id in ids_big_key:
player_big_key_locations.add(ids_big_key[item_id])
@@ -967,12 +961,10 @@ def getTracker(tracker: UUID):
if location not in player_locations or location not in player_location_to_area[player]:
continue
if len(player_locations[location]) == 3:
item, recipient, flags = player_locations[location]
else: # TODO: remove around version 0.2.5
item, recipient = player_locations[location]
item, recipient, flags = player_locations[location]
attribute_item(inventory, team, recipient, item)
if recipient in names:
attribute_item(inventory, team, recipient, item)
checks_done[team][player][player_location_to_area[player][location]] += 1
checks_done[team][player]["Total"] += 1
@@ -986,10 +978,7 @@ def getTracker(tracker: UUID):
player_small_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1) if playernumber not in groups}
for loc_data in locations.values():
for values in loc_data.values():
if len(values) == 3:
item_id, item_player, flags = values
else: # TODO: remove around version 0.2.5
item_id, item_player = values
item_id, item_player, flags = values
if item_id in ids_big_key:
player_big_key_locations[item_player].add(ids_big_key[item_id])

View File

@@ -57,8 +57,10 @@ game.
A `WebWorld` class contains specific attributes and methods that can be modified
for your world specifically on the webhost. At the moment this comprises of `settings_page`
which can be changed to a link instead of an AP generated settings page; such is the case
for Final Fantasy.
which can be changed to a link instead of an AP generated settings page, and a `theme` to be used for your game specific AP pages. Available themes:
| dirt | grass (default) | grassFlowers | ice | jungle | ocean | partyTime |
|---|---|---|---|---|---|---|
| <img src="img/theme_dirt.JPG" width="100"> | <img src="img/theme_grass.JPG" width="100"> | <img src="img/theme_grassFlowers.JPG" width="100"> | <img src="img/theme_ice.JPG" width="100"> | <img src="img/theme_jungle.JPG" width="100"> | <img src="img/theme_ocean.JPG" width="100"> | <img src="img/theme_partyTime.JPG" width="100"> |
### MultiWorld Object

BIN
docs/img/theme_dirt.JPG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

BIN
docs/img/theme_grass.JPG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

BIN
docs/img/theme_ice.JPG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

BIN
docs/img/theme_jungle.JPG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

BIN
docs/img/theme_ocean.JPG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

View File

@@ -66,6 +66,7 @@ Name: "client/factorio"; Description: "Factorio"; Types: full playing
Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278
Name: "client/oot"; Description: "Ocarina of Time"; Types: full playing
Name: "client/ff1"; Description: "Final Fantasy 1"; Types: full playing
Name: "client/cf"; Description: "ChecksFinder"; Types: full playing
Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing
[Dirs]
@@ -90,6 +91,7 @@ Source: "{#source_path}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags
Source: "{#source_path}\ArchipelagoOoTClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot
Source: "{#source_path}\ArchipelagoOoTAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot
Source: "{#source_path}\ArchipelagoFF1Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ff1
Source: "{#source_path}\ArchipelagoChecksFinderClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/cf
Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
[Icons]
@@ -101,6 +103,7 @@ Name: "{group}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactor
Name: "{group}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Components: client/minecraft
Name: "{group}\{#MyAppName} Ocarina of Time Client"; Filename: "{app}\ArchipelagoOoTClient.exe"; Components: client/oot
Name: "{group}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\ArchipelagoFF1Client.exe"; Components: client/ff1
Name: "{group}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Components: client/cf
Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon
Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon; Components: server
@@ -109,6 +112,7 @@ Name: "{commondesktop}\{#MyAppName} Factorio Client"; Filename: "{app}\Archipela
Name: "{commondesktop}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Tasks: desktopicon; Components: client/minecraft
Name: "{commondesktop}\{#MyAppName} Ocarina of Time Client"; Filename: "{app}\ArchipelagoOoTClient.exe"; Tasks: desktopicon; Components: client/oot
Name: "{commondesktop}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\ArchipelagoFF1Client.exe"; Tasks: desktopicon; Components: client/ff1
Name: "{commondesktop}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Tasks: desktopicon; Components: client/cf
[Run]
@@ -124,16 +128,21 @@ Type: files; Name: "{app}\ArchipelagoLttPClient.exe"
[Registry]
Root: HKCR; Subkey: ".apbp"; ValueData: "{#MyAppName}patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: ".aplttp"; ValueData: "{#MyAppName}patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}patch"; ValueData: "Archipelago Binary Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: ".apm3"; ValueData: "{#MyAppName}smpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: ".apsm"; ValueData: "{#MyAppName}smpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}smpatch"; ValueData: "Archipelago Super Metroid Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}smpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}smpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: ".apsmz3"; ValueData: "{#MyAppName}smz3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}smz3patch"; ValueData: "Archipelago SMZ3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}smz3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}smz3patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1""";
Root: HKCR; Subkey: ".apsoe"; ValueData: "{#MyAppName}soepatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}soepatch"; ValueData: "Archipelago Secret of Evermore Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}soepatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
@@ -377,5 +386,5 @@ begin
if (assigned(SoEROMFilePage)) and (PageID = SoEROMFilePage.ID) then
Result := not (WizardIsComponentSelected('generator/soe'));
if (assigned(OoTROMFilePage)) and (PageID = OoTROMFilePage.ID) then
Result := not (WizardIsComponentSelected('generator/oot'));
Result := not (WizardIsComponentSelected('generator/oot') or WizardIsComponentSelected('client/oot'));
end;

View File

@@ -0,0 +1,66 @@
# Tests for Generate.py (ArchipelagoGenerate.exe)
import unittest
import sys
from pathlib import Path
from tempfile import TemporaryDirectory
import os.path
import os
import ModuleUpdate
ModuleUpdate.update_ran = True # don't upgrade
import Generate
import Utils
class TestGenerateMain(unittest.TestCase):
"""This tests Generate.py (ArchipelagoGenerate.exe) main"""
generate_dir = Path(Generate.__file__).parent
run_dir = generate_dir / 'test' # reproducible cwd that's neither __file__ nor Generate.__file__
abs_input_dir = Path(__file__).parent / 'data' / 'OnePlayer'
rel_input_dir = abs_input_dir.relative_to(run_dir) # directly supplied relative paths are relative to cwd
yaml_input_dir = abs_input_dir.relative_to(generate_dir) # yaml paths are relative to user_path
def assertOutput(self, output_dir: str):
output_path = Path(output_dir)
output_files = list(output_path.glob('*.zip'))
if len(output_files) == 1:
return True
self.fail(f"Expected {output_dir} to contain one zip, but has {len(output_files)}: "
f"{list(output_path.glob('*'))}")
def setUp(self):
Utils.local_path.cached_path = str(self.generate_dir)
os.chdir(self.run_dir)
self.output_tempdir = TemporaryDirectory(prefix='AP_out_')
def test_generate_absolute(self):
sys.argv = [sys.argv[0], '--seed', '0',
'--player_files_path', str(self.abs_input_dir),
'--outputpath', self.output_tempdir.name]
print(f'Testing Generate.py {sys.argv} in {os.getcwd()}')
Generate.main()
self.assertOutput(self.output_tempdir.name)
def test_generate_relative(self):
sys.argv = [sys.argv[0], '--seed', '0',
'--player_files_path', str(self.rel_input_dir),
'--outputpath', self.output_tempdir.name]
print(f'Testing Generate.py {sys.argv} in {os.getcwd()}')
Generate.main()
self.assertOutput(self.output_tempdir.name)
def test_generate_yaml(self):
# override host.yaml
defaults = Utils.get_options()["generator"]
defaults["player_files_path"] = str(self.yaml_input_dir)
defaults["players"] = 0
sys.argv = [sys.argv[0], '--seed', '0',
'--outputpath', self.output_tempdir.name]
print(f'Testing Generate.py {sys.argv} in {os.getcwd()}, player_files_path={self.yaml_input_dir}')
Generate.main()
self.assertOutput(self.output_tempdir.name)

View File

View File

@@ -0,0 +1,9 @@
description: Almost blank test yaml
name: Player{NUMBER}
game:
Timespinner: 1 # what else
requires:
version: 0.2.6
Timespinner: {}

View File

@@ -11,6 +11,8 @@ class AutoWorldRegister(type):
world_types: Dict[str, World] = {}
def __new__(cls, name: str, bases, dct: Dict[str, Any]):
if "web" in dct:
assert isinstance(dct["web"], WebWorld), "WebWorld has to be instantiated."
# filter out any events
dct["item_name_to_id"] = {name: id for name, id in dct["item_name_to_id"].items() if id}
dct["location_name_to_id"] = {name: id for name, id in dct["location_name_to_id"].items() if id}

View File

@@ -298,7 +298,7 @@ class ALTTPWorld(World):
deathlink=world.death_link[player])
outfilepname = f'_P{player}'
outfilepname += f"_{world.player_name[player].replace(' ', '_')}" \
outfilepname += f"_{world.get_file_safe_player_name(player).replace(' ', '_')}" \
if world.player_name[player] != 'Player%d' % player else ''
rompath = os.path.join(output_directory, f'AP_{world.seed_name}{outfilepname}.sfc')
@@ -324,7 +324,7 @@ class ALTTPWorld(World):
multidata["connect_names"][new_name] = multidata["connect_names"][self.world.player_name[self.player]]
def get_required_client_version(self) -> tuple:
return max((0, 2, 6), super(ALTTPWorld, self).get_required_client_version())
return max((0, 3, 1), super(ALTTPWorld, self).get_required_client_version())
def create_item(self, name: str) -> Item:
return ALttPItem(name, self.player, **as_dict_item_table[name])

View File

@@ -11,7 +11,7 @@ item_table = (
'Towel',
'Scarf',
'2012 Magic the Gathering Core Set Starter Box',
'Pokemon Booster Pack',
'Poke\'mon Booster Pack',
'USB Speakers',
'Plastic Spork',
'Cheeseburger',
@@ -22,7 +22,7 @@ item_table = (
'One-Up Mushroom',
'Nokia N-GAGE',
'2-Liter of Sprite',
'Free trial of the critically acclaimed MMORPG Final Fantasy XIV, including the entirety of A Realm Reborn and the award winning Heavenasward expansion up to level 60 with no restrictions on playtime!',
'Free trial of the critically acclaimed MMORPG Final Fantasy XIV, including the entirety of A Realm Reborn and the award winning Heavensward expansion up to level 60 with no restrictions on playtime!',
'Can of Compressed Air',
'Striped Kitten',
'USB Power Adapter',
@@ -249,4 +249,54 @@ item_table = (
'Fire Extinguisher',
'Beeping Smoke Alarm',
'Greasy Spatula',
'Progressive Auto Insurance',
'Mace Windu\'s Purple Lightsaber',
'An Old Fixer-Upper',
'Gamer Chair',
'Comfortable Reclining Chair',
'Shirt Covered in Dog Hair',
'Angry Praying Mantis',
'Card Games on Motorcycles',
'Trucker Hat',
'The DK Rap',
'Three Great Balls',
'Some Very Sus Behavior',
'Glass of Orange Juice',
'Turkey Bacon',
'Bald Barbie Doll',
'Developer Commentary',
'Subscription to Nintendo Power Magazine',
'DeLorean Time Machine',
'Unkillable Cockroach',
'Dungeons & Dragons Rulebook',
'Boxed Copy of Quest 64',
'James Bond\'s Gadget Wristwatch',
'Tube of Go-Gurt',
'Digital Watch',
'Laser Pointer',
'The Secret Cow Level',
'AOL Free Trial CD-ROM',
'E.T. for Atari 2600',
'Season 2 of Knight Rider',
'Spam E-Mails',
'Half-Life 3 Release Date',
'Source Code of Jurassic Park',
'Moldy Cheese',
'Comic Book Collection',
'Hardcover Copy of Scott Pilgrim VS the World',
'Old Gym Shorts',
'Very Cool Sunglasses',
'Your High School Yearbook Picture',
'Written Invitation to Prom',
'The Star Wars Holiday Special',
'Oil Change Coupon',
'Finger Guns',
'Box of Tabletop Games',
'Sock Puppets',
'The Dog of Wisdom',
'Surprised Chipmunk',
'Stonks',
'A Shrubbery',
'Roomba with a Knife',
'Wet Cat',
)

View File

@@ -14,7 +14,7 @@ class ArchipIDLEWorld(World):
"""
game = "ArchipIDLE"
topology_present = False
data_version = 2
data_version = 3
web = ArchipIDLEWebWorld()
item_name_to_id = {}

View File

@@ -91,7 +91,7 @@ def generate_mod(world, output_directory: str):
for location in multiworld.get_filled_locations(player):
if location.address:
locations.append((location.name, location.item.name, location.item.player, location.item.advancement))
mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.player_name[player]}"
mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.get_file_safe_player_name(player)}"
tech_cost_scale = {0: 0.1,
1: 0.25,
2: 0.5,

File diff suppressed because one or more lines are too long

View File

@@ -251,7 +251,15 @@ for item_data in extra_item_data:
for effect in effects:
effect_names.add(effect["Term"])
effects = {effect["Term"]: effect["Value"] for effect in effects if
effect["Term"] != item_data["Name"] and effect["Term"] != "GEO"}
effect["Term"] != item_data["Name"] and effect["Term"] not in {"GEO",
"HALLOWNESTSEALS",
"WANDERERSJOURNALS",
'HALLOWNESTSEALS',
"KINGSIDOLS",
'ARCANEEGGS',
'MAPS'
}}
if effects:
item_effects[item_data["Name"]] = effects
@@ -363,7 +371,12 @@ warning = "# This module is written by Extractor.py, do not edit manually!.\n\n"
with open(os.path.join(os.path.dirname(__file__), "ExtractedData.py"), "wt") as py:
py.write(warning)
for name in names:
py.write(f"{name} = {globals()[name]}\n")
var = globals()[name]
if type(var) == set:
# sort so a regen doesn't cause a file change every time
var = sorted(var)
var = "{"+str(var)[1:-1]+"}"
py.write(f"{name} = {var}\n")
template_env: jinja2.Environment = \

View File

@@ -18,3 +18,7 @@ lookup_id_to_name: Dict[int, str] = {data.id: item_name for item_name, data in i
lookup_type_to_names: Dict[str, Set[str]] = {}
for item, item_data in item_table.items():
lookup_type_to_names.setdefault(item_data.type, set()).add(item)
item_name_groups = {group: lookup_type_to_names[group] for group in ("Skill", "Charm", "Mask", "Vessel",
"Relic", "Root", "Map", "Stag", "Cocoon",
"Soul", "DreamWarrior", "DreamBoss")}

View File

@@ -18,9 +18,79 @@ class Disabled(Toggle):
locations = {"option_" + start: i for i, start in enumerate(starts)}
# This way the dynamic start names are picked up by the MetaClass Choice belongs to
StartLocation = type("StartLocation", (Choice,), {"auto_display_name": False, **locations})
StartLocation = type("StartLocation", (Choice,), {"__module__": __name__, "auto_display_name": False, **locations,
"__doc__": "Choose your start location. "
"This is currently only locked to King's Pass."})
del (locations)
option_docstrings = {
"RandomizeDreamers": "Allow for Dreamers to be randomized into the item pool and opens their locations for "
"randomization.",
"RandomizeSkills": "Allow for Skills, such as Mantis Claw or Shade Soul, to be randomized into the item pool. "
"Also opens their locations for receiving randomized items.",
"RandomizeCharms": "Allow for Charms to be randomized into the item pool and open their locations for "
"randomization. Includes Charms sold in shops.",
"RandomizeKeys": "Allow for Keys to be randomized into the item pool. Includes those sold in shops.",
"RandomizeMaskShards": "Allow for Mask Shard to be randomized into the item pool and open their locations for"
" randomization.",
"RandomizeVesselFragments": "Allow for Vessel Fragments to be randomized into the item pool and open their "
"locations for randomization.",
"RandomizeCharmNotches": "Allow for Charm Notches to be randomized into the item pool. "
"Includes those sold by Salubra.",
"RandomizePaleOre": "Randomize Pale Ores into the item pool and open their locations for randomization.",
"RandomizeGeoChests": "Allow for Geo Chests to contain randomized items, "
"as well as their Geo reward being randomized into the item pool.",
"RandomizeJunkPitChests": "Randomize the contents of junk pit chests into the item pool and open their locations "
"for randomization.",
"RandomizeRancidEggs": "Randomize Rancid Eggs into the item pool and open their locations for randomization",
"RandomizeRelics": "Randomize Relics (King's Idol, et al.) into the item pool and open their locations for"
" randomization.",
"RandomizeWhisperingRoots": "Randomize the essence rewards from Whispering Roots into the item pool. Whispering "
"Roots will now grant a randomized item when completed. This can be previewed by "
"standing on the root.",
"RandomizeBossEssence": "Randomize boss essence drops, such as those for defeating Warrior Dreams, into the item "
"pool and open their locations for randomization.",
"RandomizeGrubs": "Randomize Grubs into the item pool and open their locations for randomization.",
"RandomizeMaps": "Randomize Maps into the item pool. This causes Cornifer to give you a message allowing you to see"
" and buy an item that is randomized into that location as well.",
"RandomizeStags": "Randomize Stag Stations unlocks into the item pool as well as placing randomized items "
"on the stag station bell/toll.",
"RandomizeLifebloodCocoons": "Randomize Lifeblood Cocoon grants into the item pool and open their locations"
" for randomization.",
"RandomizeGrimmkinFlames": "Randomize Grimmkin Flames into the item pool and open their locations for "
"randomization.",
"RandomizeJournalEntries": "Randomize the Hunter's Journal as well as the findable journal entries into the item "
"pool, and open their locations for randomization. Does not include journal entries "
"gained by killing enemies.",
"RandomizeGeoRocks": "Randomize Geo Rock rewards into the item pool and open their locations for randomization.",
"RandomizeBossGeo": "Randomize boss Geo drops into the item pool and open those locations for randomization.",
"RandomizeSoulTotems": "Randomize Soul Refill items into the item pool and open the Soul Totem locations for"
" randomization.",
"RandomizeLoreTablets": "Randomize Lore items into the itempool, one per Lore Tablet, and place randomized item "
"grants on the tablets themselves. You must still read the tablet to get the item.",
"PreciseMovement": "Places skips into logic which require extremely precise player movement, possibly without "
"movement skills such as dash or hook.",
"ProficientCombat": "Places skips into logic which require proficient combat, possibly with limited items.",
"BackgroundObjectPogos": "Places skips into logic for locations which are reachable via pogoing off of "
"background objects.",
"EnemyPogos": "Places skips into logic for locations which are reachable via pogos off of enemies.",
"ObscureSkips": "Places skips into logic which are considered obscure enough that a beginner is not expected "
"to know them.",
"ShadeSkips": "Places shade skips into logic which utilize the player's shade for pogoing or damage boosting.",
"InfectionSkips": "Places skips into logic which are only possible after the crossroads become infected.",
"FireballSkips": "Places skips into logic which require the use of spells to reset fall speed while in mid-air.",
"SpikeTunnels": "Places skips into logic which require the navigation of narrow tunnels filled with spikes.",
"AcidSkips": "Places skips into logic which require crossing a pool of acid without Isma's Tear, or water if swim "
"is disabled.",
"DamageBoosts": "Places skips into logic which require you to take damage from an enemy or hazard to progress.",
"DangerousSkips": "Places skips into logic which contain a high risk of taking damage.",
"DarkRooms": "Places skips into logic which require navigating dark rooms without the use of the Lumafly Lantern.",
"ComplexSkips": "Places skips into logic which require intense setup or are obscure even beyond advanced skip "
"standards.",
"DifficultSkips": "Places skips into logic which are considered more difficult than typical.",
"RemoveSpellUpgrades": "Removes the second level of all spells from the item pool."
}
default_on = {
"RandomizeDreamers",
"RandomizeSkills",
@@ -43,7 +113,9 @@ disabled = {
hollow_knight_randomize_options: typing.Dict[str, type(Option)] = {}
for option_name, option_data in pool_options.items():
extra_data = {"items": option_data[0], "locations": option_data[1]}
extra_data = {"__module__": __name__, "items": option_data[0], "locations": option_data[1]}
if option_name in option_docstrings:
extra_data["__doc__"] = option_docstrings[option_name]
if option_name in disabled:
extra_data["__doc__"] = "Disabled Option. Not implemented."
option = type(option_name, (Disabled,), extra_data)
@@ -51,15 +123,26 @@ for option_name, option_data in pool_options.items():
option = type(option_name, (DefaultOnToggle,), extra_data)
else:
option = type(option_name, (Toggle,), extra_data)
hollow_knight_randomize_options[option_name] = option
globals()[option.__name__] = option
hollow_knight_randomize_options[option.__name__] = option
hollow_knight_logic_options: typing.Dict[str, type(Option)] = {}
for option_name in logic_options.values():
if option_name in hollow_knight_randomize_options:
continue
extra_data = {}
if option_name in option_docstrings:
extra_data["__doc__"] = option_docstrings[option_name]
option = type(option_name, (Toggle,), extra_data)
if option_name in disabled:
extra_data["__doc__"] = "Disabled Option. Not implemented."
option = type(option_name, (Disabled,), extra_data)
hollow_knight_logic_options: typing.Dict[str, type(Option)] = {
option_name: Disabled if option_name in disabled else Toggle for option_name in logic_options.values() if
option_name not in hollow_knight_randomize_options}
hollow_knight_logic_options[option_name] = option
class MinimumGrubPrice(Range):
"""The minimum grub price in the range of prices that an item should cost from Grubfather."""
display_name = "Minimum Grub Price"
range_start = 1
range_end = 46
@@ -67,11 +150,13 @@ class MinimumGrubPrice(Range):
class MaximumGrubPrice(MinimumGrubPrice):
"""The maximum grub price in the range of prices that an item should cost from Grubfather."""
display_name = "Maximum Grub Price"
default = 23
class MinimumEssencePrice(Range):
"""The minimum essence price in the range of prices that an item should cost from Seer."""
display_name = "Minimum Essence Price"
range_start = 1
range_end = 2800
@@ -79,11 +164,14 @@ class MinimumEssencePrice(Range):
class MaximumEssencePrice(MinimumEssencePrice):
"""The maximum essence price in the range of prices that an item should cost from Seer."""
display_name = "Maximum Essence Price"
default = 1400
class MinimumEggPrice(Range):
"""The minimum rancid egg price in the range of prices that an item should cost from Ijii.
Only takes effect if the EggSlotShops option is greater than 0."""
display_name = "Minimum Egg Price"
range_start = 1
range_end = 21
@@ -91,12 +179,15 @@ class MinimumEggPrice(Range):
class MaximumEggPrice(MinimumEggPrice):
"""The maximum rancid egg price in the range of prices that an item should cost from Ijii.
Only takes effect if the EggSlotShops option is greater than 0."""
display_name = "Maximum Egg Price"
default = 10
class MinimumCharmPrice(Range):
"""For Salubra's Charm-count based locations."""
"""The minimum charm price in the range of prices that an item should cost for Salubra's shop item which also
carry a charm cost."""
display_name = "Minimum Charm Requirement"
range_start = 1
range_end = 40
@@ -104,13 +195,16 @@ class MinimumCharmPrice(Range):
class MaximumCharmPrice(MinimumCharmPrice):
"""The maximum charm price in the range of prices that an item should cost for Salubra's shop item which also
carry a charm cost."""
default = 20
class RandomCharmCosts(Range):
"""Total Cost of all Charms together. Set to -1 for vanilla costs. Vanilla sums to 90."""
"""Total Notch Cost of all Charms together. Set to -1 for vanilla costs. Vanilla sums to 90.
This value is distributed among all charms in a random fashion."""
display_name = "Random Charm Costs"
display_name = "Randomize Charm Notch Costs"
range_start = -1
range_end = 240
default = -1
@@ -141,15 +235,15 @@ class EggShopSlots(Range):
hollow_knight_options: typing.Dict[str, type(Option)] = {
**hollow_knight_randomize_options,
**hollow_knight_logic_options,
"start_location": StartLocation,
"minimum_grub_price": MinimumGrubPrice,
"maximum_grub_price": MaximumGrubPrice,
"minimum_essence_price": MinimumEssencePrice,
"maximum_essence_price": MaximumEssencePrice,
"minimum_egg_price": MinimumEggPrice,
"maximum_egg_price": MaximumEggPrice,
"minimum_charm_price": MinimumCharmPrice,
"maximum_charm_price": MaximumCharmPrice,
"random_charm_costs": RandomCharmCosts,
"egg_shop_slots": EggShopSlots,
StartLocation.__name__: StartLocation,
MinimumGrubPrice.__name__: MinimumGrubPrice,
MaximumGrubPrice.__name__: MaximumGrubPrice,
MinimumEssencePrice.__name__: MinimumEssencePrice,
MaximumEssencePrice.__name__: MaximumEssencePrice,
MinimumEggPrice.__name__: MinimumEggPrice,
MaximumEggPrice.__name__: MaximumEggPrice,
MinimumCharmPrice.__name__: MinimumCharmPrice,
MaximumCharmPrice.__name__: MaximumCharmPrice,
RandomCharmCosts.__name__: RandomCharmCosts,
EggShopSlots.__name__: EggShopSlots,
}

View File

@@ -3,10 +3,10 @@
from ..generic.Rules import set_rule, add_rule
units = {
"egg": "RANCIDEGGS",
"grub": "GRUBS",
"essence": "ESSENCE",
"charm": "CHARMS",
"Egg": "RANCIDEGGS",
"Grub": "GRUBS",
"Essence": "ESSENCE",
"Charm": "CHARMS",
}

View File

@@ -6,10 +6,10 @@ from collections import Counter
logger = logging.getLogger("Hollow Knight")
from .Items import item_table, lookup_type_to_names
from .Items import item_table, lookup_type_to_names, item_name_groups
from .Regions import create_regions
from .Rules import set_rules
from .Options import hollow_knight_options, hollow_knight_randomize_options
from .Options import hollow_knight_options, hollow_knight_randomize_options, disabled
from .ExtractedData import locations, starts, multi_locations, location_to_region_lookup, \
event_names, item_effects, connectors, one_ways
@@ -77,16 +77,26 @@ white_palace_locations = {
class HKWorld(World):
"""Beneath the fading town of Dirtmouth sleeps a vast, ancient kingdom. Many are drawn beneath the surface,
searching for riches, or glory, or answers to old secrets.
As the enigmatic Knight, youll traverse the depths, unravel its mysteries and conquer its evils.
""" # from https://www.hollowknight.com
game: str = "Hollow Knight"
options = hollow_knight_options
item_name_to_id = {name: data.id for name, data in item_table.items()}
location_name_to_id = {location_name: location_id for location_id, location_name in
enumerate(locations, start=0x1000000)}
item_name_groups = item_name_groups
hidden = True
ranges: typing.Dict[str, typing.Tuple[int, int]]
shops = {"Egg_Shop": "egg", "Grubfather": "grub", "Seer": "essence", "Salubra_(Requires_Charms)": "charm"}
shops: typing.Dict[str, str] = {
"Egg_Shop": "Egg",
"Grubfather": "Grub",
"Seer": "Essence",
"Salubra_(Requires_Charms)": "Charm"
}
charm_costs: typing.List[int]
data_version = 2
@@ -99,17 +109,19 @@ class HKWorld(World):
def generate_early(self):
world = self.world
self.charm_costs = world.random_charm_costs[self.player].get_costs(world.random)
self.charm_costs = world.RandomCharmCosts[self.player].get_costs(world.random)
world.exclude_locations[self.player].value.update(white_palace_locations)
world.local_items[self.player].value.add("Mimic_Grub")
for vendor, unit in self.shops.items():
mini = getattr(world, f"minimum_{unit}_price")[self.player]
maxi = getattr(world, f"maximum_{unit}_price")[self.player]
mini = getattr(world, f"Minimum{unit}Price")[self.player]
maxi = getattr(world, f"Maximum{unit}Price")[self.player]
# if minimum > maximum, set minimum to maximum
mini.value = min(mini.value, maxi.value)
self.ranges[unit] = mini.value, maxi.value
world.push_precollected(HKItem(starts[world.start_location[self.player].current_key],
world.push_precollected(HKItem(starts[world.StartLocation[self.player].current_key],
True, None, "Event", self.player))
for option_name in disabled:
getattr(world, option_name)[self.player].value = 0
def create_regions(self):
menu_region: Region = create_region(self.world, self.player, 'Menu')
@@ -145,11 +157,14 @@ class HKWorld(World):
for item_name, location_name in zip(option.items, option.locations):
if item_name in geo_replace:
item_name = "Geo_Rock-Default"
item = self.create_item(item_name)
if location_name in white_palace_locations:
self.create_location(location_name).place_locked_item(self.create_item(item_name))
self.create_location(location_name).place_locked_item(item)
elif location_name == "Start":
self.world.push_precollected(item)
else:
self.create_location(location_name)
pool.append(self.create_item(item_name))
pool.append(item)
else:
for item_name, location_name in zip(option.items, option.locations):
item = self.create_item(item_name)
@@ -157,7 +172,7 @@ class HKWorld(World):
self.world.push_precollected(item)
else:
self.create_location(location_name).place_locked_item(item)
for i in range(self.world.egg_shop_slots[self.player].value):
for i in range(self.world.EggShopSlots[self.player].value):
self.create_location("Egg_Shop")
pool.append(self.create_item("Geo_Rock-Default"))
if not self.allow_white_palace:
@@ -166,6 +181,17 @@ class HKWorld(World):
loc.item.advancement = False
self.world.itempool += pool
for shopname in self.shops:
prices: typing.List[int] = []
locations: typing.List[HKLocation] = []
for x in range(1, self.created_multi_locations[shopname]+1):
loc = self.world.get_location(self.get_multi_location_name(shopname, x), self.player)
locations.append(loc)
prices.append(loc.cost)
prices.sort()
for loc, price in zip(locations, prices):
loc.cost = price
def set_rules(self):
world = self.world
player = self.player
@@ -207,7 +233,7 @@ class HKWorld(World):
cost = 0
if name in multi_locations:
self.created_multi_locations[name] += 1
name += f"_{self.created_multi_locations[name]}"
name = self.get_multi_location_name(name, self.created_multi_locations[name])
region = self.world.get_region("Menu", self.player)
loc = HKLocation(self.player, name, self.location_name_to_id[name], region)
@@ -235,6 +261,33 @@ class HKWorld(World):
return change
@classmethod
def stage_write_spoiler(cls, world: MultiWorld, spoiler_handle):
hk_players = world.get_game_players(cls.game)
spoiler_handle.write('\n\nCharm Notches:')
for player in hk_players:
name = world.get_player_name(player)
spoiler_handle.write(f'\n{name}\n')
hk_world: HKWorld = world.worlds[player]
for charm_number, cost in enumerate(hk_world.charm_costs, start=1):
spoiler_handle.write(f"\n{charm_number}: {cost}")
spoiler_handle.write('\n\nShop Prices:')
for player in hk_players:
name = world.get_player_name(player)
spoiler_handle.write(f'\n{name}\n')
hk_world: HKWorld = world.worlds[player]
for shop_name, unit_name in cls.shops.items():
for x in range(1, hk_world.created_multi_locations[shop_name]+1):
loc = world.get_location(hk_world.get_multi_location_name(shop_name, x), player)
spoiler_handle.write(f"\n{loc}: {loc.item} costing {loc.cost} {unit_name}")
def get_multi_location_name(self, base: str, i: typing.Optional[int]) -> str:
if i is None:
i = self.created_multi_locations[base]
assert 0 < i < 18, "limited number of multi location IDs reserved."
return f"{base}_{i}"
def create_region(world: MultiWorld, player: int, name: str, location_names=None, exits=None) -> Region:
ret = Region(name, RegionType.Generic, name, player)
@@ -277,8 +330,8 @@ class HKLogicMixin(LogicMixin):
def _kh_option(self, player: int, option_name: str) -> int:
if option_name == "RandomizeCharmNotches":
return self.world.random_charm_costs[player] != -1
return self.world.RandomCharmCosts[player] != -1
return getattr(self.world, option_name)[player].value
def _kh_start(self, player, start_location: str) -> bool:
return self.world.start_location[player] == start_location
return self.world.StartLocation[player] == start_location

View File

@@ -1,10 +1,10 @@
from ..generic.Rules import set_rule, add_rule
units = {
"egg": "RANCIDEGGS",
"grub": "GRUBS",
"essence": "ESSENCE",
"charm": "CHARMS",
"Egg": "RANCIDEGGS",
"Grub": "GRUBS",
"Essence": "ESSENCE",
"Charm": "CHARMS",
}

View File

@@ -112,7 +112,7 @@ class MinecraftWorld(World):
def generate_output(self, output_directory: str):
data = self._get_mc_data()
filename = f"AP_{self.world.seed_name}_P{self.player}_{self.world.get_player_name(self.player)}.apmc"
filename = f"AP_{self.world.seed_name}_P{self.player}_{self.world.get_file_safe_player_name(self.player)}.apmc"
with open(os.path.join(output_directory, filename), 'wb') as f:
f.write(b64encode(bytes(json.dumps(data), 'utf-8')))

View File

@@ -773,7 +773,7 @@ class OOTWorld(World):
# Seed hint RNG, used for ganon text lines also
self.hint_rng = self.world.slot_seeds[self.player]
outfile_name = f"AP_{self.world.seed_name}_P{self.player}_{self.world.get_player_name(self.player)}"
outfile_name = f"AP_{self.world.seed_name}_P{self.player}_{self.world.get_file_safe_player_name(self.player)}"
rom = Rom(file=get_options()['oot_options']['rom_file'])
if self.hints != 'none':
buildWorldGossipHints(self)

View File

@@ -404,7 +404,7 @@ class SMWorld(World):
def generate_output(self, output_directory: str):
outfilebase = 'AP_' + self.world.seed_name
outfilepname = f'_P{self.player}'
outfilepname += f"_{self.world.player_name[self.player].replace(' ', '_')}"
outfilepname += f"_{self.world.get_file_safe_player_name(self.player).replace(' ', '_')}"
outputFilename = os.path.join(output_directory, f'{outfilebase}{outfilepname}.sfc')
try:

View File

@@ -107,6 +107,6 @@ class SM64World(World):
}
}
}
filename = f"AP_{self.world.seed_name}_P{self.player}_{self.world.get_player_name(self.player)}.apsm64ex"
filename = f"AP_{self.world.seed_name}_P{self.player}_{self.world.get_file_safe_player_name(self.player)}.apsm64ex"
with open(os.path.join(output_directory, filename), 'w') as f:
json.dump(data, f)

View File

@@ -12,7 +12,7 @@ ROM_PLAYER_LIMIT = 256
class SMZ3DeltaPatch(APDeltaPatch):
hash = "3a177ba9879e3dd04fb623a219d175b2"
game = "SMZ3"
patch_file_ending = ".smz3"
patch_file_ending = ".apsmz3"
@classmethod
def get_source_data(cls) -> bytes:

View File

@@ -17,7 +17,7 @@ class Texts:
@staticmethod
def ParseTextScript(resource: str):
with open(resource, 'r') as file:
with open(resource, 'r', encoding="utf-8-sig") as file:
return [text.rstrip('\n') for text in file.read().replace("\r", "").split("---\n") if text]
scripts: Any = ParseYamlScripts.__func__(text_folder + "/Scripts/General.yaml")

View File

@@ -257,7 +257,7 @@ class SMZ3World(World):
outfilebase = 'AP_' + self.world.seed_name
outfilepname = f'_P{self.player}'
outfilepname += f"_{self.world.player_name[self.player].replace(' ', '_')}" \
outfilepname += f"_{self.world.get_file_safe_player_name(self.player).replace(' ', '_')}" \
filename = os.path.join(output_directory, f'{outfilebase}{outfilepname}.sfc')
with open(filename, "wb") as binary_file:

View File

@@ -1,4 +1,4 @@
from ..AutoWorld import World
from ..AutoWorld import World, WebWorld
from ..generic.Rules import set_rule
from BaseClasses import Region, Location, Entrance, Item, RegionType
from Utils import output_path
@@ -7,7 +7,6 @@ import os
import os.path
import threading
import itertools
import time
try:
import pyevermizer # from package
@@ -133,6 +132,10 @@ def _get_item_grouping() -> typing.Dict[str, typing.Set[str]]:
return groups
class SoEWebWorld(WebWorld):
theme = 'jungle'
class SoEWorld(World):
"""
Secret of Evermore is a SNES action RPG. You learn alchemy spells, fight bosses and gather rocket parts to visit a
@@ -140,14 +143,17 @@ class SoEWorld(World):
"""
game: str = "Secret of Evermore"
options = soe_options
topology_present: bool = False
remote_items: bool = False
topology_present = False
remote_items = False
data_version = 2
web = SoEWebWorld()
item_name_to_id, item_id_to_raw = _get_item_mapping()
location_name_to_id, location_id_to_raw = _get_location_mapping()
item_name_groups = _get_item_grouping()
trap_types = [name[12:] for name in options if name.startswith('trap_chance_')]
evermizer_seed: int
connect_name: str
@@ -189,9 +195,8 @@ class SoEWorld(World):
trap_count = self.world.trap_count[self.player].value
trap_chances = {}
trap_names = {}
fool = 1648731600 <= time.time() <= 1648900800
if trap_count > 0:
for trap_type in ("quake", "poison", "confound", "hud", "ohko"):
for trap_type in self.trap_types:
trap_option = getattr(self.world, f'trap_chance_{trap_type}')[self.player]
trap_chances[trap_type] = trap_option.value
trap_names[trap_type] = trap_option.item_name
@@ -200,11 +205,6 @@ class SoEWorld(World):
for trap_type in trap_chances:
trap_chances[trap_type] = 1
trap_chances_total = len(trap_chances)
elif fool:
trap_count = 1
trap_chances = {'quake': 1}
trap_names = {'quake': getattr(self.world, 'trap_chance_quake')[self.player].item_name}
trap_chances_total = 1
def create_trap() -> Item:
v = self.world.random.randrange(trap_chances_total)
@@ -271,7 +271,8 @@ class SoEWorld(World):
if self.world.death_link[self.player].value:
switches.append("--death-link")
rom_file = get_base_rom_path()
out_base = output_path(output_directory, f'AP_{self.world.seed_name}_P{self.player}_{player_name}')
out_base = output_path(output_directory, f'AP_{self.world.seed_name}_P{self.player}_'
f'{self.world.get_file_safe_player_name(self.player)}')
out_file = out_base + '.sfc'
placement_file = out_base + '.txt'
patch_file = out_base + '.apsoe'

View File

@@ -1,13 +1,14 @@
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.1/pyevermizer-0.41.1-cp38-cp38-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.8'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.1/pyevermizer-0.41.1-cp39-cp39-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.9'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.1/pyevermizer-0.41.1-cp310-cp310-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.10'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.1/pyevermizer-0.41.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.8'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.1/pyevermizer-0.41.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.9'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.1/pyevermizer-0.41.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.10'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.1/pyevermizer-0.41.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.8'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.1/pyevermizer-0.41.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.9'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.1/pyevermizer-0.41.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.10'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.1/pyevermizer-0.41.1-cp38-cp38-macosx_10_9_x86_64.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.8'
#https://github.com/black-sliver/pyevermizer/releases/download/v0.41.1/pyevermizer-0.41.1-cp39-cp39-macosx_10_9_x86_64.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.9'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.1/pyevermizer-0.41.1-cp39-cp39-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.9'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.1/pyevermizer-0.41.1-cp310-cp310-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.10'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp38-cp38-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.8'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp39-cp39-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.9'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp310-cp310-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.10'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.8'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.9'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.10'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.8'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.9'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.10'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp38-cp38-macosx_10_9_x86_64.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.8'
#https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp39-cp39-macosx_10_9_x86_64.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.9'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp39-cp39-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.9'
https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp310-cp310-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.10'
#https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2.tar.gz#egg=pyevermizer; python_version == '3.11'

View File

@@ -1,6 +1,6 @@
from typing import Dict, List, Set, Tuple, TextIO
from BaseClasses import Item, MultiWorld, Location
from ..AutoWorld import World
from ..AutoWorld import World, WebWorld
from .LogicMixin import TimespinnerLogic
from .Items import get_item_names_per_category, item_table, starter_melee_weapons, starter_spells, starter_progression_items, filler_items
from .Locations import get_locations, starter_progression_locations, EventId
@@ -8,6 +8,8 @@ from .Regions import create_regions
from .Options import is_option_enabled, get_option_value, timespinner_options
from .PyramidKeys import get_pyramid_keys_unlock
class TimespinnerWebWorld(WebWorld):
theme = "ice"
class TimespinnerWorld(World):
"""
@@ -20,6 +22,7 @@ class TimespinnerWorld(World):
topology_present = True
remote_items = False
data_version = 8
web = TimespinnerWebWorld()
item_name_to_id = {name: data.code for name, data in item_table.items()}
location_name_to_id = {location.name: location.code for location in get_locations(None, None)}

View File

@@ -79,6 +79,6 @@ class V6World(World):
}
}
}
filename = f"AP_{self.world.seed_name}_P{self.player}_{self.world.get_player_name(self.player)}.apv6"
filename = f"AP_{self.world.seed_name}_P{self.player}_{self.world.get_file_safe_player_name(self.player)}.apv6"
with open(os.path.join(output_directory, filename), 'w') as f:
json.dump(data, f)