Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07ff0f1026 | ||
|
|
a080288e3e | ||
|
|
71bd87f293 | ||
|
|
574e2abba8 | ||
|
|
cffa772801 | ||
|
|
66bd793306 | ||
|
|
0eb37883ca | ||
|
|
356384ab05 | ||
|
|
8c2c6877b6 | ||
|
|
d1d40d8a60 | ||
|
|
b026a0a372 | ||
|
|
73bcd0058a | ||
|
|
0cf396e5d6 | ||
|
|
1bc09d4292 | ||
|
|
97d0c51db1 | ||
|
|
ed1c11267c | ||
|
|
a3e1ac896f | ||
|
|
37d9eb2752 | ||
|
|
05e267a0bd | ||
|
|
d1f0a29a02 | ||
|
|
fb2e780c56 | ||
|
|
ba3257f850 | ||
|
|
215d5e9adf | ||
|
|
5392b32d5c | ||
|
|
4dd0a75914 | ||
|
|
a2212002ae | ||
|
|
91ccee3513 | ||
|
|
2a593d5d0a | ||
|
|
a93b3d79aa | ||
|
|
938ab32cda | ||
|
|
6f5ab05345 | ||
|
|
95f8647f09 | ||
|
|
06c8caa3cc | ||
|
|
d206a562df | ||
|
|
a0a290e481 | ||
|
|
266ff0c520 | ||
|
|
931bf7da16 | ||
|
|
fe4a26d034 | ||
|
|
dca70a99ad |
39
.github/workflows/build.yml
vendored
@@ -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
|
||||
|
||||
13
.github/workflows/unittests.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
14
Launcher.py
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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.")
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
27
SNIClient.py
@@ -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(
|
||||
|
||||
2
Utils.py
@@ -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
|
||||
|
||||
22
WebHostLib/static/assets/gameInfo/en_Hollow Knight.md
Normal 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.
|
||||
30
WebHostLib/static/assets/tutorial/Hollow Knight/setup_en.md
Normal 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.
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
-
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
After Width: | Height: | Size: 209 KiB |
BIN
docs/img/theme_grass.JPG
Normal file
|
After Width: | Height: | Size: 216 KiB |
BIN
docs/img/theme_grassFlowers.JPG
Normal file
|
After Width: | Height: | Size: 214 KiB |
BIN
docs/img/theme_ice.JPG
Normal file
|
After Width: | Height: | Size: 214 KiB |
BIN
docs/img/theme_jungle.JPG
Normal file
|
After Width: | Height: | Size: 257 KiB |
BIN
docs/img/theme_ocean.JPG
Normal file
|
After Width: | Height: | Size: 221 KiB |
BIN
docs/img/theme_partyTime.JPG
Normal file
|
After Width: | Height: | Size: 245 KiB |
@@ -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;
|
||||
66
test/programs/TestGenerate.py
Normal 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)
|
||||
0
test/programs/__init__.py
Normal file
9
test/programs/data/OnePlayer/test.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
description: Almost blank test yaml
|
||||
name: Player{NUMBER}
|
||||
|
||||
game:
|
||||
Timespinner: 1 # what else
|
||||
requires:
|
||||
version: 0.2.6
|
||||
Timespinner: {}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@ class ArchipIDLEWorld(World):
|
||||
"""
|
||||
game = "ArchipIDLE"
|
||||
topology_present = False
|
||||
data_version = 2
|
||||
data_version = 3
|
||||
web = ArchipIDLEWebWorld()
|
||||
|
||||
item_name_to_id = {}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = \
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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, you’ll 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
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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')))
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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)
|
||||
|
||||