Compare commits
74 Commits
style-lock
...
use_sphinx
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95779c76ed | ||
|
|
99d2caa57d | ||
|
|
ade82e3d60 | ||
|
|
7c04e7e06f | ||
|
|
baf51e5959 | ||
|
|
8aad75ed23 | ||
|
|
1792b66b3a | ||
|
|
5e8ac74b2a | ||
|
|
2acc129381 | ||
|
|
0cbb3c2839 | ||
|
|
539d2e80f1 | ||
|
|
f9e28004a0 | ||
|
|
b7cfcc9272 | ||
|
|
4b6d46fd74 | ||
|
|
b45d8bf221 | ||
|
|
f7d107fc0c | ||
|
|
b14d694e1e | ||
|
|
8d2333006a | ||
|
|
e413619c26 | ||
|
|
03f66a922d | ||
|
|
b115bdafe7 | ||
|
|
0444fdc379 | ||
|
|
c617bba959 | ||
|
|
8da1cfeeb7 | ||
|
|
fcfc2c2e10 | ||
|
|
a753905ee4 | ||
|
|
2a7babce68 | ||
|
|
60d1a27079 | ||
|
|
4a2a184db1 | ||
|
|
45fb735320 | ||
|
|
3eb9e7050f | ||
|
|
26aed9351e | ||
|
|
b1ffbc49c9 | ||
|
|
6d6111de2a | ||
|
|
cc8ce32c61 | ||
|
|
4c94bb0ad5 | ||
|
|
af19180ff0 | ||
|
|
a175aa93e7 | ||
|
|
a78863fde1 | ||
|
|
0d6cbd9093 | ||
|
|
1aaf89ff2c | ||
|
|
295ea97544 | ||
|
|
33103b209d | ||
|
|
fab12dca0b | ||
|
|
c390801c4c | ||
|
|
e548abd332 | ||
|
|
0a5b24be2b | ||
|
|
7f41cafffc | ||
|
|
d66f981be6 | ||
|
|
b66a265726 | ||
|
|
c695f91198 | ||
|
|
11cbc0b40b | ||
|
|
87d91aeef3 | ||
|
|
6a6dfcbaff | ||
|
|
9553627136 | ||
|
|
a4a8894d22 | ||
|
|
bf217dcf85 | ||
|
|
484ee9f065 | ||
|
|
bba82ccd6c | ||
|
|
fb122df5f5 | ||
|
|
be8c3131d8 | ||
|
|
9341332379 | ||
|
|
83bcb441bf | ||
|
|
b8b6b1c2da | ||
|
|
309651a644 | ||
|
|
dee8a2aaa9 | ||
|
|
edcfd66658 | ||
|
|
a074d16297 | ||
|
|
89ab4aff9c | ||
|
|
0ac67bfe76 | ||
|
|
b0119a6a80 | ||
|
|
e35d1f98eb | ||
|
|
727f86c1f1 | ||
|
|
f3e5acbbc4 |
35
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: Bug Report
|
||||
description: File a bug report.
|
||||
title: "Bug: "
|
||||
labels:
|
||||
- bug / fix
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report! If this bug occurred during local generation check your
|
||||
Archipelago install for a log (probably `C:\ProgramData\Archipelago\logs`)
|
||||
and upload it with this report, as well as all yaml files used.
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected-results
|
||||
attributes:
|
||||
label: What were the expected results?
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: version
|
||||
attributes:
|
||||
label: Software
|
||||
description: Where did this bug occur?
|
||||
options:
|
||||
- Website
|
||||
- Local generation
|
||||
- While playing
|
||||
validations:
|
||||
required: true
|
||||
17
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: Feature Request
|
||||
description: Request a feature!
|
||||
title: "Category: "
|
||||
labels:
|
||||
- enhancement
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please replace `Category` in the title with what this feature will be targeting, such as Core generation,
|
||||
website, documentation, or a game.
|
||||
Note: this is not for requesting new games to be added. If you would like to request a game, the best place to
|
||||
ask is about it is in the [discord](https://archipelago.gg/discord).
|
||||
- type: textarea
|
||||
id: feature
|
||||
attributes:
|
||||
label: What feature would you like to see?
|
||||
10
.github/ISSUE_TEMPLATE/task.yaml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
name: Task
|
||||
description: Submit a task to be done. If this is not targeting core, it should likely be elsewhere.
|
||||
title: "Core: "
|
||||
labels:
|
||||
- core
|
||||
- enhancement
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: What task needs to be completed?
|
||||
12
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
Please format your title with what portion of the project this pull request is
|
||||
targeting and what it's changing.
|
||||
|
||||
ex. "MyGame4: implement new game" or "Docs: add new guide for customizing MyGame3"
|
||||
|
||||
## What is this fixing or adding?
|
||||
|
||||
|
||||
## How was this tested?
|
||||
|
||||
|
||||
## If this makes graphical changes, please attach screenshots.
|
||||
17
.github/workflows/build.yml
vendored
@@ -4,6 +4,11 @@ name: Build
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
env:
|
||||
SNI_VERSION: v0.0.84
|
||||
ENEMIZER_VERSION: 7.1
|
||||
APPIMAGETOOL_VERSION: 13
|
||||
|
||||
jobs:
|
||||
# build-release-macos: # LF volunteer
|
||||
|
||||
@@ -17,9 +22,9 @@ jobs:
|
||||
python-version: '3.8'
|
||||
- name: Download run-time dependencies
|
||||
run: |
|
||||
Invoke-WebRequest -Uri https://github.com/alttpo/sni/releases/download/v0.0.82/sni-v0.0.82-windows-amd64.zip -OutFile sni.zip
|
||||
Invoke-WebRequest -Uri https://github.com/alttpo/sni/releases/download/${Env:SNI_VERSION}/sni-${Env:SNI_VERSION}-windows-amd64.zip -OutFile sni.zip
|
||||
Expand-Archive -Path sni.zip -DestinationPath SNI -Force
|
||||
Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/7.0.1/win-x64.zip -OutFile enemizer.zip
|
||||
Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip
|
||||
Expand-Archive -Path enemizer.zip -DestinationPath EnemizerCLI -Force
|
||||
- name: Build
|
||||
run: |
|
||||
@@ -43,6 +48,7 @@ jobs:
|
||||
build-ubuntu1804:
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
# - copy code below to release.yml -
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install base dependencies
|
||||
run: |
|
||||
@@ -56,18 +62,18 @@ jobs:
|
||||
- name: Install build-time dependencies
|
||||
run: |
|
||||
echo "PYTHON=python3.9" >> $GITHUB_ENV
|
||||
wget -nv https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage
|
||||
wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage
|
||||
chmod a+rx appimagetool-x86_64.AppImage
|
||||
./appimagetool-x86_64.AppImage --appimage-extract
|
||||
echo -e '#/bin/sh\n./squashfs-root/AppRun "$@"' > appimagetool
|
||||
chmod a+rx appimagetool
|
||||
- name: Download run-time dependencies
|
||||
run: |
|
||||
wget -nv https://github.com/alttpo/sni/releases/download/v0.0.82/sni-v0.0.82-manylinux2014-amd64.tar.xz
|
||||
wget -nv https://github.com/alttpo/sni/releases/download/$SNI_VERSION/sni-$SNI_VERSION-manylinux2014-amd64.tar.xz
|
||||
tar xf sni-*.tar.xz
|
||||
rm sni-*.tar.xz
|
||||
mv sni-* SNI
|
||||
wget -nv https://github.com/Ijwu/Enemizer/releases/download/7.0.1/ubuntu.16.04-x64.7z
|
||||
wget -nv https://github.com/Ijwu/Enemizer/releases/download/$ENEMIZER_VERSION/ubuntu.16.04-x64.7z
|
||||
7za x -oEnemizerCLI/ ubuntu.16.04-x64.7z
|
||||
- name: Build
|
||||
run: |
|
||||
@@ -84,6 +90,7 @@ jobs:
|
||||
(cd build && DIR_NAME="`ls | grep exe`" && mv "$DIR_NAME" Archipelago && tar -czvf ../dist/$TAR_NAME Archipelago && mv Archipelago "$DIR_NAME")
|
||||
echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV
|
||||
echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV
|
||||
# - copy code above to release.yml -
|
||||
- name: Store AppImage
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
|
||||
4
.github/workflows/lint.yml
vendored
@@ -18,8 +18,8 @@ jobs:
|
||||
python-version: 3.9
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install flake8 pytest
|
||||
python -m pip install --upgrade pip wheel
|
||||
pip install flake8 pytest pytest-subtests
|
||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
|
||||
14
.github/workflows/release.yml
vendored
@@ -7,6 +7,11 @@ on:
|
||||
tags:
|
||||
- '*.*.*'
|
||||
|
||||
env:
|
||||
SNI_VERSION: v0.0.84
|
||||
ENEMIZER_VERSION: 7.1
|
||||
APPIMAGETOOL_VERSION: 13
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -44,22 +49,23 @@ jobs:
|
||||
- name: Install build-time dependencies
|
||||
run: |
|
||||
echo "PYTHON=python3.9" >> $GITHUB_ENV
|
||||
wget -nv https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage
|
||||
wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage
|
||||
chmod a+rx appimagetool-x86_64.AppImage
|
||||
./appimagetool-x86_64.AppImage --appimage-extract
|
||||
echo -e '#/bin/sh\n./squashfs-root/AppRun "$@"' > appimagetool
|
||||
chmod a+rx appimagetool
|
||||
- name: Download run-time dependencies
|
||||
run: |
|
||||
wget -nv https://github.com/alttpo/sni/releases/download/v0.0.82/sni-v0.0.82-manylinux2014-amd64.tar.xz
|
||||
wget -nv https://github.com/alttpo/sni/releases/download/$SNI_VERSION/sni-$SNI_VERSION-manylinux2014-amd64.tar.xz
|
||||
tar xf sni-*.tar.xz
|
||||
rm sni-*.tar.xz
|
||||
mv sni-* SNI
|
||||
wget -nv https://github.com/Ijwu/Enemizer/releases/download/7.0.1/ubuntu.16.04-x64.7z
|
||||
wget -nv https://github.com/Ijwu/Enemizer/releases/download/$ENEMIZER_VERSION/ubuntu.16.04-x64.7z
|
||||
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
|
||||
"${{ env.PYTHON }}" -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
|
||||
4
.github/workflows/unittests.yml
vendored
@@ -32,8 +32,8 @@ jobs:
|
||||
python-version: ${{ matrix.python.version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install flake8 pytest
|
||||
python -m pip install --upgrade pip wheel
|
||||
pip install flake8 pytest pytest-subtests
|
||||
python ModuleUpdate.py --yes --force --append "WebHostLib/requirements.txt"
|
||||
- name: Unittests
|
||||
run: |
|
||||
|
||||
2
.gitignore
vendored
@@ -21,6 +21,7 @@
|
||||
*.archipelago
|
||||
*.apsave
|
||||
|
||||
docs/sphinx/_build/
|
||||
build
|
||||
bundle/components.wxs
|
||||
dist
|
||||
@@ -28,6 +29,7 @@ README.html
|
||||
.vs/
|
||||
EnemizerCLI/
|
||||
/Players/
|
||||
/SNI/
|
||||
/options.yaml
|
||||
/config.yaml
|
||||
/logs/
|
||||
|
||||
@@ -152,8 +152,9 @@ class CommonContext:
|
||||
# locations
|
||||
locations_checked: typing.Set[int] # local state
|
||||
locations_scouted: typing.Set[int]
|
||||
missing_locations: typing.Set[int]
|
||||
missing_locations: typing.Set[int] # server state
|
||||
checked_locations: typing.Set[int] # server state
|
||||
server_locations: typing.Set[int] # all locations the server knows of, missing_location | checked_locations
|
||||
locations_info: typing.Dict[int, NetworkItem]
|
||||
|
||||
# internals
|
||||
@@ -184,8 +185,9 @@ class CommonContext:
|
||||
self.locations_checked = set() # local state
|
||||
self.locations_scouted = set()
|
||||
self.items_received = []
|
||||
self.missing_locations = set()
|
||||
self.missing_locations = set() # server state
|
||||
self.checked_locations = set() # server state
|
||||
self.server_locations = set() # all locations the server knows of, missing_location | checked_locations
|
||||
self.locations_info = {}
|
||||
|
||||
self.input_queue = asyncio.Queue()
|
||||
@@ -345,6 +347,8 @@ class CommonContext:
|
||||
cache_package = Utils.persistent_load().get("datapackage", {}).get("games", {})
|
||||
needed_updates: typing.Set[str] = set()
|
||||
for game in relevant_games:
|
||||
if game not in remote_datepackage_versions:
|
||||
continue
|
||||
remote_version: int = remote_datepackage_versions[game]
|
||||
|
||||
if remote_version == 0: # custom datapackage for this game
|
||||
@@ -632,6 +636,7 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
||||
# when /missing is used for the client side view of what is missing.
|
||||
ctx.missing_locations = set(args["missing_locations"])
|
||||
ctx.checked_locations = set(args["checked_locations"])
|
||||
ctx.server_locations = ctx.missing_locations | ctx. checked_locations
|
||||
|
||||
elif cmd == 'ReceivedItems':
|
||||
start_index = args["index"]
|
||||
|
||||
11
Generate.py
@@ -63,7 +63,7 @@ class PlandoSettings(enum.IntFlag):
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.value:
|
||||
return ", ".join((flag.name for flag in PlandoSettings if self.value & flag.value))
|
||||
return ", ".join(flag.name for flag in PlandoSettings if self.value & flag.value)
|
||||
return "Off"
|
||||
|
||||
|
||||
@@ -84,11 +84,6 @@ def mystery_argparse():
|
||||
parser.add_argument('--seed', help='Define seed number to generate.', type=int)
|
||||
parser.add_argument('--multi', default=defaults["players"], type=lambda value: max(int(value), 1))
|
||||
parser.add_argument('--spoiler', type=int, default=defaults["spoiler"])
|
||||
parser.add_argument('--lttp_rom', default=options["lttp_options"]["rom_file"],
|
||||
help="Path to the 1.0 JP LttP Baserom.") # absolute, relative to cwd or relative to app path
|
||||
parser.add_argument('--sm_rom', default=options["sm_options"]["rom_file"],
|
||||
help="Path to the 1.0 JP SM Baserom.")
|
||||
parser.add_argument('--enemizercli', default=resolve_path(defaults["enemizer_path"], local_path))
|
||||
parser.add_argument('--outputpath', default=resolve_path(options["general_options"]["output_path"], user_path),
|
||||
help="Path to output folder. Absolute or relative to cwd.") # absolute or relative to cwd
|
||||
parser.add_argument('--race', action='store_true', default=defaults["race"])
|
||||
@@ -183,10 +178,6 @@ def main(args=None, callback=ERmain):
|
||||
|
||||
Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level)
|
||||
|
||||
erargs.lttp_rom = args.lttp_rom
|
||||
erargs.sm_rom = args.sm_rom
|
||||
erargs.enemizercli = args.enemizercli
|
||||
|
||||
settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \
|
||||
{fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.samesettings else None)
|
||||
for fname, yamls in weights_cache.items()}
|
||||
|
||||
20
Launcher.py
@@ -10,16 +10,21 @@ Scroll down to components= to add components to the launcher as well as setup.py
|
||||
|
||||
|
||||
import argparse
|
||||
from os.path import isfile
|
||||
import sys
|
||||
from typing import Iterable, Sequence, Callable, Union, Optional
|
||||
import subprocess
|
||||
import itertools
|
||||
from Utils import is_frozen, user_path, local_path, init_logging, open_filename, messagebox,\
|
||||
is_windows, is_macos, is_linux
|
||||
from shutil import which
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
from enum import Enum, auto
|
||||
from os.path import isfile
|
||||
from shutil import which
|
||||
from typing import Iterable, Sequence, Callable, Union, Optional
|
||||
|
||||
if __name__ == "__main__":
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.update()
|
||||
|
||||
from Utils import is_frozen, user_path, local_path, init_logging, open_filename, messagebox, \
|
||||
is_windows, is_macos, is_linux
|
||||
|
||||
|
||||
def open_host_yaml():
|
||||
@@ -65,6 +70,7 @@ def browse_files():
|
||||
webbrowser.open(file)
|
||||
|
||||
|
||||
# noinspection PyArgumentList
|
||||
class Type(Enum):
|
||||
TOOL = auto()
|
||||
FUNC = auto() # not a real component
|
||||
|
||||
@@ -83,9 +83,9 @@ def main():
|
||||
parser.add_argument('--ow_palettes', default='default',
|
||||
choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
|
||||
'sick'])
|
||||
parser.add_argument('--link_palettes', default='default',
|
||||
choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
|
||||
'sick'])
|
||||
# parser.add_argument('--link_palettes', default='default',
|
||||
# choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
|
||||
# 'sick'])
|
||||
parser.add_argument('--shield_palettes', default='default',
|
||||
choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
|
||||
'sick'])
|
||||
@@ -752,6 +752,7 @@ class SpriteSelector():
|
||||
self.window['pady'] = 5
|
||||
self.spritesPerRow = 32
|
||||
self.all_sprites = []
|
||||
self.invalid_sprites = []
|
||||
self.sprite_pool = spritePool
|
||||
|
||||
def open_custom_sprite_dir(_evt):
|
||||
@@ -833,6 +834,13 @@ class SpriteSelector():
|
||||
self.window.focus()
|
||||
tkinter_center_window(self.window)
|
||||
|
||||
if self.invalid_sprites:
|
||||
invalid = sorted(self.invalid_sprites)
|
||||
logging.warning(f"The following sprites are invalid: {', '.join(invalid)}")
|
||||
msg = f"{invalid[0]} "
|
||||
msg += f"and {len(invalid)-1} more are invalid" if len(invalid) > 1 else "is invalid"
|
||||
messagebox.showerror("Invalid sprites detected", msg, parent=self.window)
|
||||
|
||||
def remove_from_sprite_pool(self, button, spritename):
|
||||
self.callback(("remove", spritename))
|
||||
self.spritePoolButtons.buttons.remove(button)
|
||||
@@ -897,7 +905,13 @@ class SpriteSelector():
|
||||
sprites = []
|
||||
|
||||
for file in os.listdir(path):
|
||||
sprites.append((file, Sprite(os.path.join(path, file))))
|
||||
if file == '.gitignore':
|
||||
continue
|
||||
sprite = Sprite(os.path.join(path, file))
|
||||
if sprite.valid:
|
||||
sprites.append((file, sprite))
|
||||
else:
|
||||
self.invalid_sprites.append(file)
|
||||
|
||||
sprites.sort(key=lambda s: str.lower(s[1].name or "").strip())
|
||||
|
||||
|
||||
1
Main.py
@@ -70,7 +70,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
world.required_medallions = args.required_medallions.copy()
|
||||
world.game = args.game.copy()
|
||||
world.player_name = args.name.copy()
|
||||
world.enemizer = args.enemizercli
|
||||
world.sprite = args.sprite.copy()
|
||||
world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option.
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, Networ
|
||||
SlotType
|
||||
|
||||
min_client_version = Version(0, 1, 6)
|
||||
print_command_compatability_threshold = Version(0, 3, 5) # Remove backwards compatibility around 0.3.7
|
||||
colorama.init()
|
||||
|
||||
# functions callable on storable data on the server by clients
|
||||
@@ -291,20 +292,27 @@ class Context:
|
||||
|
||||
# text
|
||||
|
||||
def notify_all(self, text):
|
||||
def notify_all(self, text: str):
|
||||
logging.info("Notice (all): %s" % text)
|
||||
self.broadcast_all([{"cmd": "Print", "text": text}])
|
||||
broadcast_text_all(self, text)
|
||||
|
||||
def notify_client(self, client: Client, text: str):
|
||||
if not client.auth:
|
||||
return
|
||||
logging.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text))
|
||||
asyncio.create_task(self.send_msgs(client, [{"cmd": "Print", "text": text}]))
|
||||
if client.version >= print_command_compatability_threshold:
|
||||
asyncio.create_task(self.send_msgs(client, [{"cmd": "PrintJSON", "data": [{ "text": text }]}]))
|
||||
else:
|
||||
asyncio.create_task(self.send_msgs(client, [{"cmd": "Print", "text": text}]))
|
||||
|
||||
def notify_client_multiple(self, client: Client, texts: typing.List[str]):
|
||||
if not client.auth:
|
||||
return
|
||||
asyncio.create_task(self.send_msgs(client, [{"cmd": "Print", "text": text} for text in texts]))
|
||||
if client.version >= print_command_compatability_threshold:
|
||||
asyncio.create_task(self.send_msgs(client,
|
||||
[{"cmd": "PrintJSON", "data": [{ "text": text }]} for text in texts]))
|
||||
else:
|
||||
asyncio.create_task(self.send_msgs(client, [{"cmd": "Print", "text": text} for text in texts]))
|
||||
|
||||
# loading
|
||||
|
||||
@@ -585,6 +593,7 @@ class Context:
|
||||
forfeit_player(self, client.team, client.slot)
|
||||
elif self.forced_auto_forfeits[self.games[client.slot]]:
|
||||
forfeit_player(self, client.team, client.slot)
|
||||
self.save() # save goal completion flag
|
||||
|
||||
|
||||
def notify_hints(ctx: Context, team: int, hints: typing.List[NetUtils.Hint], only_new: bool = False):
|
||||
@@ -721,19 +730,33 @@ async def on_client_left(ctx: Context, client: Client):
|
||||
ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc)
|
||||
|
||||
|
||||
async def countdown(ctx: Context, timer):
|
||||
ctx.notify_all(f'[Server]: Starting countdown of {timer}s')
|
||||
async def countdown(ctx: Context, timer: int):
|
||||
broadcast_countdown(ctx, timer, f"[Server]: Starting countdown of {timer}s")
|
||||
if ctx.countdown_timer:
|
||||
ctx.countdown_timer = timer # timer is already running, set it to a different time
|
||||
else:
|
||||
ctx.countdown_timer = timer
|
||||
while ctx.countdown_timer > 0:
|
||||
ctx.notify_all(f'[Server]: {ctx.countdown_timer}')
|
||||
broadcast_countdown(ctx, ctx.countdown_timer, f"[Server]: {ctx.countdown_timer}")
|
||||
ctx.countdown_timer -= 1
|
||||
await asyncio.sleep(1)
|
||||
ctx.notify_all(f'[Server]: GO')
|
||||
broadcast_countdown(ctx, 0, f"[Server]: GO")
|
||||
ctx.countdown_timer = 0
|
||||
|
||||
def broadcast_text_all(ctx: Context, text: str, additional_arguments: dict = {}):
|
||||
old_clients, new_clients = [], []
|
||||
|
||||
for teams in ctx.clients.values():
|
||||
for clients in teams.values():
|
||||
for client in clients:
|
||||
new_clients.append(client) if client.version >= print_command_compatability_threshold \
|
||||
else old_clients.append(client)
|
||||
|
||||
ctx.broadcast(old_clients, [{"cmd": "Print", "text": text }])
|
||||
ctx.broadcast(new_clients, [{**{"cmd": "PrintJSON", "data": [{ "text": text }]}, **additional_arguments}])
|
||||
|
||||
def broadcast_countdown(ctx: Context, timer: int, message: str):
|
||||
broadcast_text_all(ctx, message, { "type": "Countdown", "countdown": timer })
|
||||
|
||||
def get_players_string(ctx: Context):
|
||||
auth_clients = {(c.team, c.slot) for c in ctx.endpoints if c.auth}
|
||||
|
||||
@@ -298,7 +298,7 @@ class Toggle(NumericOption):
|
||||
if type(data) == str:
|
||||
return cls.from_text(data)
|
||||
else:
|
||||
return cls(data)
|
||||
return cls(int(data))
|
||||
|
||||
@classmethod
|
||||
def get_option_name(cls, value):
|
||||
|
||||
3
Patch.py
@@ -17,7 +17,7 @@ ModuleUpdate.update()
|
||||
|
||||
import Utils
|
||||
|
||||
current_patch_version = 4
|
||||
current_patch_version = 5
|
||||
|
||||
|
||||
class AutoPatchRegister(type):
|
||||
@@ -128,6 +128,7 @@ class APDeltaPatch(APContainer, metaclass=AutoPatchRegister):
|
||||
manifest = super(APDeltaPatch, self).get_manifest()
|
||||
manifest["base_checksum"] = self.hash
|
||||
manifest["result_file_ending"] = self.result_file_ending
|
||||
manifest["patch_file_ending"] = self.patch_file_ending
|
||||
return manifest
|
||||
|
||||
@classmethod
|
||||
|
||||
22
README.md
@@ -61,26 +61,10 @@ This project makes use of multiple other projects. We wouldn't be here without t
|
||||
* [Ocarina of Time Randomizer](https://github.com/TestRunnerSRL/OoT-Randomizer)
|
||||
|
||||
## Contributing
|
||||
Contributions are welcome. We have a few asks of any new contributors.
|
||||
|
||||
* Ensure that all changes which affect logic are covered by unit tests.
|
||||
* Do not introduce any unit test failures/regressions.
|
||||
|
||||
Otherwise, we tend to judge code on a case to case basis. It is a generally good idea to stick to PEP-8 guidelines to ensure consistency with existing code. (And to make the linter happy.)
|
||||
|
||||
For adding a new game to Archipelago and other documentation on how Archipelago functions, please see [the docs folder](docs/) for the relevant information and feel free to ask any questions in the #archipelago-dev channel in our discord.
|
||||
For contribution guidelines, please see our [Contributing doc.](/docs/contributing.md)
|
||||
|
||||
## FAQ
|
||||
For frequently asked questions see the website's [FAQ Page](https://archipelago.gg/faq/en/)
|
||||
For Frequently asked questions, please see the website's [FAQ Page.](https://archipelago.gg/faq/en/)
|
||||
|
||||
## Code of Conduct
|
||||
We conduct ourselves openly and inclusively here. Please do not contribute to an environment which makes other people uncomfortable. This means that we expect all contributors or participants here to:
|
||||
|
||||
* Be welcoming and inclusive in tone and language.
|
||||
* Be respectful of others and their abilities.
|
||||
* Show empathy when speaking with others.
|
||||
* Be gracious and accept feedback and constructive criticism.
|
||||
|
||||
These guidelines apply to all channels of communication within this GitHub repository. Please be respectful in both public channels, such as issues, and private, such as private messaging or emails.
|
||||
|
||||
Any incidents of abuse may be reported directly to Ijwu at hmfarran@gmail.com.
|
||||
Please refer to our [code of conduct.](/docs/code_of_conduct.md)
|
||||
|
||||
56
SNIClient.py
@@ -149,8 +149,8 @@ class Context(CommonContext):
|
||||
def event_invalid_slot(self):
|
||||
if self.snes_socket is not None and not self.snes_socket.closed:
|
||||
asyncio.create_task(self.snes_socket.close())
|
||||
raise Exception('Invalid ROM detected, '
|
||||
'please verify that you have loaded the correct rom and reconnect your snes (/snes)')
|
||||
raise Exception("Invalid ROM detected, "
|
||||
"please verify that you have loaded the correct rom and reconnect your snes (/snes)")
|
||||
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
@@ -158,7 +158,7 @@ class Context(CommonContext):
|
||||
if self.rom is None:
|
||||
self.awaiting_rom = True
|
||||
snes_logger.info(
|
||||
'No ROM detected, awaiting snes connection to authenticate to the multiworld server (/snes)')
|
||||
"No ROM detected, awaiting snes connection to authenticate to the multiworld server (/snes)")
|
||||
return
|
||||
self.awaiting_rom = False
|
||||
self.auth = self.rom
|
||||
@@ -262,7 +262,7 @@ async def deathlink_kill_player(ctx: Context):
|
||||
|
||||
SNES_RECONNECT_DELAY = 5
|
||||
|
||||
# LttP
|
||||
# FXPAK Pro protocol memory mapping used by SNI
|
||||
ROM_START = 0x000000
|
||||
WRAM_START = 0xF50000
|
||||
WRAM_SIZE = 0x20000
|
||||
@@ -293,21 +293,24 @@ SHOP_LEN = (len(Shops.shop_table) * 3) + 5
|
||||
DEATH_LINK_ACTIVE_ADDR = ROMNAME_START + 0x15 # 1 byte
|
||||
|
||||
# SM
|
||||
SM_ROMNAME_START = 0x007FC0
|
||||
SM_ROMNAME_START = ROM_START + 0x007FC0
|
||||
|
||||
SM_INGAME_MODES = {0x07, 0x09, 0x0b}
|
||||
SM_ENDGAME_MODES = {0x26, 0x27}
|
||||
SM_DEATH_MODES = {0x15, 0x17, 0x18, 0x19, 0x1A}
|
||||
|
||||
SM_RECV_PROGRESS_ADDR = SRAM_START + 0x2000 # 2 bytes
|
||||
SM_RECV_ITEM_ADDR = SAVEDATA_START + 0x4D2 # 1 byte
|
||||
SM_RECV_ITEM_PLAYER_ADDR = SAVEDATA_START + 0x4D3 # 1 byte
|
||||
# RECV and SEND are from the gameplay's perspective: SNIClient writes to RECV queue and reads from SEND queue
|
||||
SM_RECV_QUEUE_START = SRAM_START + 0x2000
|
||||
SM_RECV_QUEUE_WCOUNT = SRAM_START + 0x2602
|
||||
SM_SEND_QUEUE_START = SRAM_START + 0x2700
|
||||
SM_SEND_QUEUE_RCOUNT = SRAM_START + 0x2680
|
||||
SM_SEND_QUEUE_WCOUNT = SRAM_START + 0x2682
|
||||
|
||||
SM_DEATH_LINK_ACTIVE_ADDR = ROM_START + 0x277f04 # 1 byte
|
||||
SM_REMOTE_ITEM_FLAG_ADDR = ROM_START + 0x277f06 # 1 byte
|
||||
|
||||
# SMZ3
|
||||
SMZ3_ROMNAME_START = 0x00FFC0
|
||||
SMZ3_ROMNAME_START = ROM_START + 0x00FFC0
|
||||
|
||||
SMZ3_INGAME_MODES = {0x07, 0x09, 0x0b}
|
||||
SMZ3_ENDGAME_MODES = {0x26, 0x27}
|
||||
@@ -1083,6 +1086,9 @@ async def game_watcher(ctx: Context):
|
||||
|
||||
if ctx.awaiting_rom:
|
||||
await ctx.server_auth(False)
|
||||
elif ctx.server is None:
|
||||
snes_logger.warning("ROM detected but no active multiworld server connection. " +
|
||||
"Connect using command: /connect server:port")
|
||||
|
||||
if ctx.auth and ctx.auth != ctx.rom:
|
||||
snes_logger.warning("ROM change detected, please reconnect to the multiworld server")
|
||||
@@ -1159,6 +1165,9 @@ async def game_watcher(ctx: Context):
|
||||
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": [scout_location]}])
|
||||
await track_locations(ctx, roomid, roomdata)
|
||||
elif ctx.game == GAME_SM:
|
||||
if ctx.server is None or ctx.slot is None:
|
||||
# not successfully connected to a multiworld server, cannot process the game sending items
|
||||
continue
|
||||
gamemode = await snes_read(ctx, WRAM_START + 0x0998, 1)
|
||||
if "DeathLink" in ctx.tags and gamemode and ctx.last_death_link + 1 < time.time():
|
||||
currently_dead = gamemode[0] in SM_DEATH_MODES
|
||||
@@ -1169,25 +1178,25 @@ async def game_watcher(ctx: Context):
|
||||
ctx.finished_game = True
|
||||
continue
|
||||
|
||||
data = await snes_read(ctx, SM_RECV_PROGRESS_ADDR + 0x680, 4)
|
||||
data = await snes_read(ctx, SM_SEND_QUEUE_RCOUNT, 4)
|
||||
if data is None:
|
||||
continue
|
||||
|
||||
recv_index = data[0] | (data[1] << 8)
|
||||
recv_item = data[2] | (data[3] << 8)
|
||||
recv_item = data[2] | (data[3] << 8) # this is actually SM_SEND_QUEUE_WCOUNT
|
||||
|
||||
while (recv_index < recv_item):
|
||||
itemAdress = recv_index * 8
|
||||
message = await snes_read(ctx, SM_RECV_PROGRESS_ADDR + 0x700 + itemAdress, 8)
|
||||
message = await snes_read(ctx, SM_SEND_QUEUE_START + itemAdress, 8)
|
||||
# worldId = message[0] | (message[1] << 8) # unused
|
||||
# itemId = message[2] | (message[3] << 8) # unused
|
||||
itemIndex = (message[4] | (message[5] << 8)) >> 3
|
||||
|
||||
recv_index += 1
|
||||
snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + 0x680,
|
||||
snes_buffered_write(ctx, SM_SEND_QUEUE_RCOUNT,
|
||||
bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
|
||||
|
||||
from worlds.sm.Locations import locations_start_id
|
||||
from worlds.sm import locations_start_id
|
||||
location_id = locations_start_id + itemIndex
|
||||
|
||||
ctx.locations_checked.add(location_id)
|
||||
@@ -1196,15 +1205,14 @@ async def game_watcher(ctx: Context):
|
||||
f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
|
||||
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}])
|
||||
|
||||
data = await snes_read(ctx, SM_RECV_PROGRESS_ADDR + 0x600, 4)
|
||||
data = await snes_read(ctx, SM_RECV_QUEUE_WCOUNT, 2)
|
||||
if data is None:
|
||||
continue
|
||||
|
||||
# recv_itemOutPtr = data[0] | (data[1] << 8) # unused
|
||||
itemOutPtr = data[2] | (data[3] << 8)
|
||||
itemOutPtr = data[0] | (data[1] << 8)
|
||||
|
||||
from worlds.sm.Items import items_start_id
|
||||
from worlds.sm.Locations import locations_start_id
|
||||
from worlds.sm import items_start_id
|
||||
from worlds.sm import locations_start_id
|
||||
if itemOutPtr < len(ctx.items_received):
|
||||
item = ctx.items_received[itemOutPtr]
|
||||
itemId = item.item - items_start_id
|
||||
@@ -1214,10 +1222,10 @@ async def game_watcher(ctx: Context):
|
||||
locationId = 0x00 #backward compat
|
||||
|
||||
playerID = item.player if item.player <= SM_ROM_PLAYER_LIMIT else 0
|
||||
snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + itemOutPtr * 4, bytes(
|
||||
snes_buffered_write(ctx, SM_RECV_QUEUE_START + itemOutPtr * 4, bytes(
|
||||
[playerID & 0xFF, (playerID >> 8) & 0xFF, itemId & 0xFF, locationId & 0xFF]))
|
||||
itemOutPtr += 1
|
||||
snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + 0x602,
|
||||
snes_buffered_write(ctx, SM_RECV_QUEUE_WCOUNT,
|
||||
bytes([itemOutPtr & 0xFF, (itemOutPtr >> 8) & 0xFF]))
|
||||
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
|
||||
color(ctx.item_names[item.item], 'red', 'bold'),
|
||||
@@ -1225,6 +1233,9 @@ async def game_watcher(ctx: Context):
|
||||
ctx.location_names[item.location], itemOutPtr, len(ctx.items_received)))
|
||||
await snes_flush_writes(ctx)
|
||||
elif ctx.game == GAME_SMZ3:
|
||||
if ctx.server is None or ctx.slot is None:
|
||||
# not successfully connected to a multiworld server, cannot process the game sending items
|
||||
continue
|
||||
currentGame = await snes_read(ctx, SRAM_START + 0x33FE, 2)
|
||||
if (currentGame is not None):
|
||||
if (currentGame[0] != 0):
|
||||
@@ -1260,7 +1271,8 @@ async def game_watcher(ctx: Context):
|
||||
snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x680, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
|
||||
|
||||
from worlds.smz3.TotalSMZ3.Location import locations_start_id
|
||||
location_id = locations_start_id + itemIndex
|
||||
from worlds.smz3 import convertLocSMZ3IDToAPID
|
||||
location_id = locations_start_id + convertLocSMZ3IDToAPID(itemIndex)
|
||||
|
||||
ctx.locations_checked.add(location_id)
|
||||
location = ctx.location_names[location_id]
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import multiprocessing
|
||||
import logging
|
||||
import asyncio
|
||||
import copy
|
||||
import ctypes
|
||||
import logging
|
||||
import multiprocessing
|
||||
import os.path
|
||||
import re
|
||||
import sys
|
||||
import typing
|
||||
import queue
|
||||
from pathlib import Path
|
||||
|
||||
import nest_asyncio
|
||||
import sc2
|
||||
|
||||
from sc2.main import run_game
|
||||
from sc2.data import Race
|
||||
from sc2.bot_ai import BotAI
|
||||
from sc2.data import Race
|
||||
from sc2.main import run_game
|
||||
from sc2.player import Bot
|
||||
|
||||
from worlds.sc2wol.Regions import MissionInfo
|
||||
from worlds.sc2wol.MissionTables import lookup_id_to_mission
|
||||
from MultiServer import mark_raw
|
||||
from Utils import init_logging, is_windows
|
||||
from worlds.sc2wol import SC2WoLWorld
|
||||
from worlds.sc2wol.Items import lookup_id_to_name, item_table
|
||||
from worlds.sc2wol.Locations import SC2WOL_LOC_ID_OFFSET
|
||||
from worlds.sc2wol import SC2WoLWorld
|
||||
|
||||
from pathlib import Path
|
||||
import re
|
||||
from MultiServer import mark_raw
|
||||
import ctypes
|
||||
import sys
|
||||
|
||||
from Utils import init_logging, is_windows
|
||||
from worlds.sc2wol.MissionTables import lookup_id_to_mission
|
||||
from worlds.sc2wol.Regions import MissionInfo
|
||||
|
||||
if __name__ == "__main__":
|
||||
init_logging("SC2Client", exception_logger="Client")
|
||||
@@ -35,10 +35,12 @@ sc2_logger = logging.getLogger("Starcraft2")
|
||||
|
||||
import colorama
|
||||
|
||||
from NetUtils import *
|
||||
from NetUtils import ClientStatus, RawJSONtoTextParser
|
||||
from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser
|
||||
|
||||
nest_asyncio.apply()
|
||||
max_bonus: int = 8
|
||||
victory_modulo: int = 100
|
||||
|
||||
|
||||
class StarcraftClientProcessor(ClientCommandProcessor):
|
||||
@@ -98,13 +100,13 @@ class StarcraftClientProcessor(ClientCommandProcessor):
|
||||
def _cmd_available(self) -> bool:
|
||||
"""Get what missions are currently available to play"""
|
||||
|
||||
request_available_missions(self.ctx.checked_locations, self.ctx.mission_req_table, self.ctx.ui)
|
||||
request_available_missions(self.ctx)
|
||||
return True
|
||||
|
||||
def _cmd_unfinished(self) -> bool:
|
||||
"""Get what missions are currently available to play and have not had all locations checked"""
|
||||
|
||||
request_unfinished_missions(self.ctx.checked_locations, self.ctx.mission_req_table, self.ctx.ui, self.ctx)
|
||||
request_unfinished_missions(self.ctx)
|
||||
return True
|
||||
|
||||
@mark_raw
|
||||
@@ -125,18 +127,19 @@ class SC2Context(CommonContext):
|
||||
items_handling = 0b111
|
||||
difficulty = -1
|
||||
all_in_choice = 0
|
||||
mission_req_table = None
|
||||
items_rec_to_announce = []
|
||||
rec_announce_pos = 0
|
||||
items_sent_to_announce = []
|
||||
sent_announce_pos = 0
|
||||
announcements = []
|
||||
announcement_pos = 0
|
||||
mission_req_table: typing.Dict[str, MissionInfo] = {}
|
||||
announcements = queue.Queue()
|
||||
sc2_run_task: typing.Optional[asyncio.Task] = None
|
||||
missions_unlocked = False
|
||||
missions_unlocked: bool = False # allow launching missions ignoring requirements
|
||||
current_tooltip = None
|
||||
last_loc_list = None
|
||||
difficulty_override = -1
|
||||
mission_id_to_location_ids: typing.Dict[int, typing.List[int]] = {}
|
||||
raw_text_parser: RawJSONtoTextParser
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SC2Context, self).__init__(*args, **kwargs)
|
||||
self.raw_text_parser = RawJSONtoTextParser(self)
|
||||
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
@@ -149,30 +152,32 @@ class SC2Context(CommonContext):
|
||||
self.difficulty = args["slot_data"]["game_difficulty"]
|
||||
self.all_in_choice = args["slot_data"]["all_in_map"]
|
||||
slot_req_table = args["slot_data"]["mission_req"]
|
||||
self.mission_req_table = {}
|
||||
# Compatibility for 0.3.2 server data.
|
||||
if "category" not in next(iter(slot_req_table)):
|
||||
for i, mission_data in enumerate(slot_req_table.values()):
|
||||
mission_data["category"] = wol_default_categories[i]
|
||||
for mission in slot_req_table:
|
||||
self.mission_req_table[mission] = MissionInfo(**slot_req_table[mission])
|
||||
self.mission_req_table = {
|
||||
mission: MissionInfo(**slot_req_table[mission]) for mission in slot_req_table
|
||||
}
|
||||
|
||||
self.build_location_to_mission_mapping()
|
||||
|
||||
# Look for and set SC2PATH.
|
||||
# check_game_install_path() returns True if and only if it finds + sets SC2PATH.
|
||||
if "SC2PATH" not in os.environ and check_game_install_path():
|
||||
check_mod_install()
|
||||
|
||||
if cmd in {"PrintJSON"}:
|
||||
if "receiving" in args:
|
||||
if self.slot_concerns_self(args["receiving"]):
|
||||
self.announcements.append(args["data"])
|
||||
return
|
||||
if "item" in args:
|
||||
if self.slot_concerns_self(args["item"].player):
|
||||
self.announcements.append(args["data"])
|
||||
def on_print_json(self, args: dict):
|
||||
if "receiving" in args and self.slot_concerns_self(args["receiving"]):
|
||||
relevant = True
|
||||
elif "item" in args and self.slot_concerns_self(args["item"].player):
|
||||
relevant = True
|
||||
else:
|
||||
relevant = False
|
||||
|
||||
if relevant:
|
||||
self.announcements.put(self.raw_text_parser(copy.deepcopy(args["data"])))
|
||||
|
||||
super(SC2Context, self).on_print_json(args)
|
||||
|
||||
def run_gui(self):
|
||||
from kvui import GameManager, HoverBehavior, ServerToolTip, fade_in_animation
|
||||
from kvui import GameManager, HoverBehavior, ServerToolTip
|
||||
from kivy.app import App
|
||||
from kivy.clock import Clock
|
||||
from kivy.uix.tabbedpanel import TabbedPanelItem
|
||||
@@ -190,6 +195,7 @@ class SC2Context(CommonContext):
|
||||
|
||||
class MissionButton(HoverableButton):
|
||||
tooltip_text = StringProperty("Test")
|
||||
ctx: SC2Context
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(HoverableButton, self).__init__(*args, **kwargs)
|
||||
@@ -210,10 +216,7 @@ class SC2Context(CommonContext):
|
||||
self.ctx.current_tooltip = self.layout
|
||||
|
||||
def on_leave(self):
|
||||
if self.ctx.current_tooltip:
|
||||
App.get_running_app().root.remove_widget(self.ctx.current_tooltip)
|
||||
|
||||
self.ctx.current_tooltip = None
|
||||
self.ctx.ui.clear_tooltip()
|
||||
|
||||
@property
|
||||
def ctx(self) -> CommonContext:
|
||||
@@ -235,13 +238,20 @@ class SC2Context(CommonContext):
|
||||
mission_panel = None
|
||||
last_checked_locations = {}
|
||||
mission_id_to_button = {}
|
||||
launching = False
|
||||
launching: typing.Union[bool, int] = False # if int -> mission ID
|
||||
refresh_from_launching = True
|
||||
first_check = True
|
||||
ctx: SC2Context
|
||||
|
||||
def __init__(self, ctx):
|
||||
super().__init__(ctx)
|
||||
|
||||
def clear_tooltip(self):
|
||||
if self.ctx.current_tooltip:
|
||||
App.get_running_app().root.remove_widget(self.ctx.current_tooltip)
|
||||
|
||||
self.ctx.current_tooltip = None
|
||||
|
||||
def build(self):
|
||||
container = super().build()
|
||||
|
||||
@@ -256,7 +266,7 @@ class SC2Context(CommonContext):
|
||||
|
||||
def build_mission_table(self, dt):
|
||||
if (not self.launching and (not self.last_checked_locations == self.ctx.checked_locations or
|
||||
not self.refresh_from_launching)) or self.first_check:
|
||||
not self.refresh_from_launching)) or self.first_check:
|
||||
self.refresh_from_launching = True
|
||||
|
||||
self.mission_panel.clear_widgets()
|
||||
@@ -267,12 +277,7 @@ class SC2Context(CommonContext):
|
||||
|
||||
self.mission_id_to_button = {}
|
||||
categories = {}
|
||||
available_missions = []
|
||||
unfinished_locations = initialize_blank_mission_dict(self.ctx.mission_req_table)
|
||||
unfinished_missions = calc_unfinished_missions(self.ctx.checked_locations,
|
||||
self.ctx.mission_req_table,
|
||||
self.ctx, available_missions=available_missions,
|
||||
unfinished_locations=unfinished_locations)
|
||||
available_missions, unfinished_missions = calc_unfinished_missions(self.ctx)
|
||||
|
||||
# separate missions into categories
|
||||
for mission in self.ctx.mission_req_table:
|
||||
@@ -283,7 +288,8 @@ class SC2Context(CommonContext):
|
||||
|
||||
for category in categories:
|
||||
category_panel = MissionCategory()
|
||||
category_panel.add_widget(Label(text=category, size_hint_y=None, height=50, outline_width=1))
|
||||
category_panel.add_widget(
|
||||
Label(text=category, size_hint_y=None, height=50, outline_width=1))
|
||||
|
||||
# Map is completed
|
||||
for mission in categories[category]:
|
||||
@@ -295,7 +301,9 @@ class SC2Context(CommonContext):
|
||||
text = f"[color=6495ED]{text}[/color]"
|
||||
|
||||
tooltip = f"Uncollected locations:\n"
|
||||
tooltip += "\n".join(location for location in unfinished_locations[mission])
|
||||
tooltip += "\n".join([self.ctx.location_names[loc] for loc in
|
||||
self.ctx.locations_for_mission(mission)
|
||||
if loc in self.ctx.missing_locations])
|
||||
elif mission in available_missions:
|
||||
text = f"[color=FFFFFF]{text}[/color]"
|
||||
# Map requirements not met
|
||||
@@ -303,7 +311,7 @@ class SC2Context(CommonContext):
|
||||
text = f"[color=a9a9a9]{text}[/color]"
|
||||
tooltip = f"Requires: "
|
||||
if len(self.ctx.mission_req_table[mission].required_world) > 0:
|
||||
tooltip += ", ".join(list(self.ctx.mission_req_table)[req_mission-1] for
|
||||
tooltip += ", ".join(list(self.ctx.mission_req_table)[req_mission - 1] for
|
||||
req_mission in
|
||||
self.ctx.mission_req_table[mission].required_world)
|
||||
|
||||
@@ -325,13 +333,16 @@ class SC2Context(CommonContext):
|
||||
self.refresh_from_launching = False
|
||||
|
||||
self.mission_panel.clear_widgets()
|
||||
self.mission_panel.add_widget(Label(text="Launching Mission"))
|
||||
self.mission_panel.add_widget(Label(text="Launching Mission: " +
|
||||
lookup_id_to_mission[self.launching]))
|
||||
if self.ctx.ui:
|
||||
self.ctx.ui.clear_tooltip()
|
||||
|
||||
def mission_callback(self, button):
|
||||
if not self.launching:
|
||||
self.ctx.play_mission(list(self.mission_id_to_button.keys())
|
||||
[list(self.mission_id_to_button.values()).index(button)])
|
||||
self.launching = True
|
||||
mission_id: int = next(k for k, v in self.mission_id_to_button.items() if v == button)
|
||||
self.ctx.play_mission(mission_id)
|
||||
self.launching = mission_id
|
||||
Clock.schedule_once(self.finish_launching, 10)
|
||||
|
||||
def finish_launching(self, dt):
|
||||
@@ -347,9 +358,9 @@ class SC2Context(CommonContext):
|
||||
if self.sc2_run_task:
|
||||
self.sc2_run_task.cancel()
|
||||
|
||||
def play_mission(self, mission_id):
|
||||
def play_mission(self, mission_id: int):
|
||||
if self.missions_unlocked or \
|
||||
is_mission_available(mission_id, self.checked_locations, self.mission_req_table):
|
||||
is_mission_available(self, mission_id):
|
||||
if self.sc2_run_task:
|
||||
if not self.sc2_run_task.done():
|
||||
sc2_logger.warning("Starcraft 2 Client is still running!")
|
||||
@@ -358,12 +369,29 @@ class SC2Context(CommonContext):
|
||||
sc2_logger.warning("Launching Mission without Archipelago authentication, "
|
||||
"checks will not be registered to server.")
|
||||
self.sc2_run_task = asyncio.create_task(starcraft_launch(self, mission_id),
|
||||
name="Starcraft 2 Launch")
|
||||
name="Starcraft 2 Launch")
|
||||
else:
|
||||
sc2_logger.info(
|
||||
f"{lookup_id_to_mission[mission_id]} is not currently unlocked. "
|
||||
f"Use /unfinished or /available to see what is available.")
|
||||
|
||||
def build_location_to_mission_mapping(self):
|
||||
mission_id_to_location_ids: typing.Dict[int, typing.Set[int]] = {
|
||||
mission_info.id: set() for mission_info in self.mission_req_table.values()
|
||||
}
|
||||
|
||||
for loc in self.server_locations:
|
||||
mission_id, objective = divmod(loc - SC2WOL_LOC_ID_OFFSET, victory_modulo)
|
||||
mission_id_to_location_ids[mission_id].add(objective)
|
||||
self.mission_id_to_location_ids = {mission_id: sorted(objectives) for mission_id, objectives in
|
||||
mission_id_to_location_ids.items()}
|
||||
|
||||
def locations_for_mission(self, mission: str):
|
||||
mission_id: int = self.mission_req_table[mission].id
|
||||
objectives = self.mission_id_to_location_ids[self.mission_req_table[mission].id]
|
||||
for objective in objectives:
|
||||
yield SC2WOL_LOC_ID_OFFSET + mission_id * 100 + objective
|
||||
|
||||
|
||||
async def main():
|
||||
multiprocessing.freeze_support()
|
||||
@@ -459,11 +487,7 @@ def calc_difficulty(difficulty):
|
||||
return 'X'
|
||||
|
||||
|
||||
async def starcraft_launch(ctx: SC2Context, mission_id):
|
||||
ctx.rec_announce_pos = len(ctx.items_rec_to_announce)
|
||||
ctx.sent_announce_pos = len(ctx.items_sent_to_announce)
|
||||
ctx.announcements_pos = len(ctx.announcements)
|
||||
|
||||
async def starcraft_launch(ctx: SC2Context, mission_id: int):
|
||||
sc2_logger.info(f"Launching {lookup_id_to_mission[mission_id]}. If game does not launch check log file for errors.")
|
||||
|
||||
with DllDirectory(None):
|
||||
@@ -472,32 +496,29 @@ async def starcraft_launch(ctx: SC2Context, mission_id):
|
||||
|
||||
|
||||
class ArchipelagoBot(sc2.bot_ai.BotAI):
|
||||
game_running = False
|
||||
mission_completed = False
|
||||
first_bonus = False
|
||||
second_bonus = False
|
||||
third_bonus = False
|
||||
fourth_bonus = False
|
||||
fifth_bonus = False
|
||||
sixth_bonus = False
|
||||
seventh_bonus = False
|
||||
eight_bonus = False
|
||||
ctx: SC2Context = None
|
||||
mission_id = 0
|
||||
game_running: bool = False
|
||||
mission_completed: bool = False
|
||||
boni: typing.List[bool]
|
||||
setup_done: bool
|
||||
ctx: SC2Context
|
||||
mission_id: int
|
||||
|
||||
can_read_game = False
|
||||
|
||||
last_received_update = 0
|
||||
last_received_update: int = 0
|
||||
|
||||
def __init__(self, ctx: SC2Context, mission_id):
|
||||
self.setup_done = False
|
||||
self.ctx = ctx
|
||||
self.mission_id = mission_id
|
||||
self.boni = [False for _ in range(max_bonus)]
|
||||
|
||||
super(ArchipelagoBot, self).__init__()
|
||||
|
||||
async def on_step(self, iteration: int):
|
||||
game_state = 0
|
||||
if iteration == 0:
|
||||
if not self.setup_done:
|
||||
self.setup_done = True
|
||||
start_items = calculate_items(self.ctx.items_received)
|
||||
if self.ctx.difficulty_override >= 0:
|
||||
difficulty = calc_difficulty(self.ctx.difficulty_override)
|
||||
@@ -511,36 +532,10 @@ class ArchipelagoBot(sc2.bot_ai.BotAI):
|
||||
self.last_received_update = len(self.ctx.items_received)
|
||||
|
||||
else:
|
||||
if self.ctx.announcement_pos < len(self.ctx.announcements):
|
||||
index = 0
|
||||
message = ""
|
||||
while index < len(self.ctx.announcements[self.ctx.announcement_pos]):
|
||||
message += self.ctx.announcements[self.ctx.announcement_pos][index]["text"]
|
||||
index += 1
|
||||
|
||||
index = 0
|
||||
start_rem_pos = -1
|
||||
# Remove unneeded [Color] tags
|
||||
while index < len(message):
|
||||
if message[index] == '[':
|
||||
start_rem_pos = index
|
||||
index += 1
|
||||
elif message[index] == ']' and start_rem_pos > -1:
|
||||
temp_msg = ""
|
||||
|
||||
if start_rem_pos > 0:
|
||||
temp_msg = message[:start_rem_pos]
|
||||
if index < len(message) - 1:
|
||||
temp_msg += message[index + 1:]
|
||||
|
||||
message = temp_msg
|
||||
index += start_rem_pos - index
|
||||
start_rem_pos = -1
|
||||
else:
|
||||
index += 1
|
||||
|
||||
if not self.ctx.announcements.empty():
|
||||
message = self.ctx.announcements.get(timeout=1)
|
||||
await self.chat_send("SendMessage " + message)
|
||||
self.ctx.announcement_pos += 1
|
||||
self.ctx.announcements.task_done()
|
||||
|
||||
# Archipelago reads the health
|
||||
for unit in self.all_own_units():
|
||||
@@ -568,169 +563,97 @@ class ArchipelagoBot(sc2.bot_ai.BotAI):
|
||||
if game_state & (1 << 1) and not self.mission_completed:
|
||||
if self.mission_id != 29:
|
||||
print("Mission Completed")
|
||||
await self.ctx.send_msgs([
|
||||
{"cmd": 'LocationChecks', "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id]}])
|
||||
await self.ctx.send_msgs(
|
||||
[{"cmd": 'LocationChecks',
|
||||
"locations": [SC2WOL_LOC_ID_OFFSET + victory_modulo * self.mission_id]}])
|
||||
self.mission_completed = True
|
||||
else:
|
||||
print("Game Complete")
|
||||
await self.ctx.send_msgs([{"cmd": 'StatusUpdate', "status": ClientStatus.CLIENT_GOAL}])
|
||||
self.mission_completed = True
|
||||
|
||||
if game_state & (1 << 2) and not self.first_bonus:
|
||||
print("1st Bonus Collected")
|
||||
await self.ctx.send_msgs(
|
||||
[{"cmd": 'LocationChecks',
|
||||
"locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 1]}])
|
||||
self.first_bonus = True
|
||||
|
||||
if not self.second_bonus and game_state & (1 << 3):
|
||||
print("2nd Bonus Collected")
|
||||
await self.ctx.send_msgs(
|
||||
[{"cmd": 'LocationChecks',
|
||||
"locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 2]}])
|
||||
self.second_bonus = True
|
||||
|
||||
if not self.third_bonus and game_state & (1 << 4):
|
||||
print("3rd Bonus Collected")
|
||||
await self.ctx.send_msgs(
|
||||
[{"cmd": 'LocationChecks',
|
||||
"locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 3]}])
|
||||
self.third_bonus = True
|
||||
|
||||
if not self.fourth_bonus and game_state & (1 << 5):
|
||||
print("4th Bonus Collected")
|
||||
await self.ctx.send_msgs(
|
||||
[{"cmd": 'LocationChecks',
|
||||
"locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 4]}])
|
||||
self.fourth_bonus = True
|
||||
|
||||
if not self.fifth_bonus and game_state & (1 << 6):
|
||||
print("5th Bonus Collected")
|
||||
await self.ctx.send_msgs(
|
||||
[{"cmd": 'LocationChecks',
|
||||
"locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 5]}])
|
||||
self.fifth_bonus = True
|
||||
|
||||
if not self.sixth_bonus and game_state & (1 << 7):
|
||||
print("6th Bonus Collected")
|
||||
await self.ctx.send_msgs(
|
||||
[{"cmd": 'LocationChecks',
|
||||
"locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 6]}])
|
||||
self.sixth_bonus = True
|
||||
|
||||
if not self.seventh_bonus and game_state & (1 << 8):
|
||||
print("6th Bonus Collected")
|
||||
await self.ctx.send_msgs(
|
||||
[{"cmd": 'LocationChecks',
|
||||
"locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 7]}])
|
||||
self.seventh_bonus = True
|
||||
|
||||
if not self.eight_bonus and game_state & (1 << 9):
|
||||
print("6th Bonus Collected")
|
||||
await self.ctx.send_msgs(
|
||||
[{"cmd": 'LocationChecks',
|
||||
"locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 8]}])
|
||||
self.eight_bonus = True
|
||||
for x, completed in enumerate(self.boni):
|
||||
if not completed and game_state & (1 << (x + 2)):
|
||||
await self.ctx.send_msgs(
|
||||
[{"cmd": 'LocationChecks',
|
||||
"locations": [SC2WOL_LOC_ID_OFFSET + victory_modulo * self.mission_id + x + 1]}])
|
||||
self.boni[x] = True
|
||||
|
||||
else:
|
||||
await self.chat_send("LostConnection - Lost connection to game.")
|
||||
|
||||
|
||||
def calc_objectives_completed(mission, missions_info, locations_done, unfinished_locations, ctx):
|
||||
objectives_complete = 0
|
||||
|
||||
if missions_info[mission].extra_locations > 0:
|
||||
for i in range(missions_info[mission].extra_locations):
|
||||
if (missions_info[mission].id * 100 + SC2WOL_LOC_ID_OFFSET + i) in locations_done:
|
||||
objectives_complete += 1
|
||||
else:
|
||||
unfinished_locations[mission].append(ctx.location_names[
|
||||
missions_info[mission].id * 100 + SC2WOL_LOC_ID_OFFSET + i])
|
||||
|
||||
return objectives_complete
|
||||
|
||||
else:
|
||||
return -1
|
||||
|
||||
|
||||
def request_unfinished_missions(locations_done, location_table, ui, ctx):
|
||||
if location_table:
|
||||
def request_unfinished_missions(ctx: SC2Context):
|
||||
if ctx.mission_req_table:
|
||||
message = "Unfinished Missions: "
|
||||
unlocks = initialize_blank_mission_dict(location_table)
|
||||
unfinished_locations = initialize_blank_mission_dict(location_table)
|
||||
unlocks = initialize_blank_mission_dict(ctx.mission_req_table)
|
||||
unfinished_locations = initialize_blank_mission_dict(ctx.mission_req_table)
|
||||
|
||||
unfinished_missions = calc_unfinished_missions(locations_done, location_table, ctx, unlocks=unlocks,
|
||||
unfinished_locations=unfinished_locations)
|
||||
_, unfinished_missions = calc_unfinished_missions(ctx, unlocks=unlocks)
|
||||
|
||||
message += ", ".join(f"{mark_up_mission_name(mission, location_table, ui,unlocks)}[{location_table[mission].id}] " +
|
||||
message += ", ".join(f"{mark_up_mission_name(ctx, mission, unlocks)}[{ctx.mission_req_table[mission].id}] " +
|
||||
mark_up_objectives(
|
||||
f"[{unfinished_missions[mission]}/{location_table[mission].extra_locations}]",
|
||||
f"[{len(unfinished_missions[mission])}/"
|
||||
f"{sum(1 for _ in ctx.locations_for_mission(mission))}]",
|
||||
ctx, unfinished_locations, mission)
|
||||
for mission in unfinished_missions)
|
||||
|
||||
if ui:
|
||||
ui.log_panels['All'].on_message_markup(message)
|
||||
ui.log_panels['Starcraft2'].on_message_markup(message)
|
||||
if ctx.ui:
|
||||
ctx.ui.log_panels['All'].on_message_markup(message)
|
||||
ctx.ui.log_panels['Starcraft2'].on_message_markup(message)
|
||||
else:
|
||||
sc2_logger.info(message)
|
||||
else:
|
||||
sc2_logger.warning("No mission table found, you are likely not connected to a server.")
|
||||
|
||||
|
||||
def calc_unfinished_missions(locations_done, locations, ctx, unlocks=None, unfinished_locations=None,
|
||||
available_missions=[]):
|
||||
def calc_unfinished_missions(ctx: SC2Context, unlocks=None):
|
||||
unfinished_missions = []
|
||||
locations_completed = []
|
||||
|
||||
if not unlocks:
|
||||
unlocks = initialize_blank_mission_dict(locations)
|
||||
unlocks = initialize_blank_mission_dict(ctx.mission_req_table)
|
||||
|
||||
if not unfinished_locations:
|
||||
unfinished_locations = initialize_blank_mission_dict(locations)
|
||||
|
||||
if len(available_missions) > 0:
|
||||
available_missions = []
|
||||
|
||||
available_missions.extend(calc_available_missions(locations_done, locations, unlocks))
|
||||
available_missions = calc_available_missions(ctx, unlocks)
|
||||
|
||||
for name in available_missions:
|
||||
if not locations[name].extra_locations == -1:
|
||||
objectives_completed = calc_objectives_completed(name, locations, locations_done, unfinished_locations, ctx)
|
||||
|
||||
if objectives_completed < locations[name].extra_locations:
|
||||
objectives = set(ctx.locations_for_mission(name))
|
||||
if objectives:
|
||||
objectives_completed = ctx.checked_locations & objectives
|
||||
if len(objectives_completed) < len(objectives):
|
||||
unfinished_missions.append(name)
|
||||
locations_completed.append(objectives_completed)
|
||||
|
||||
else:
|
||||
else: # infer that this is the final mission as it has no objectives
|
||||
unfinished_missions.append(name)
|
||||
locations_completed.append(-1)
|
||||
|
||||
return {unfinished_missions[i]: locations_completed[i] for i in range(len(unfinished_missions))}
|
||||
return available_missions, dict(zip(unfinished_missions, locations_completed))
|
||||
|
||||
|
||||
def is_mission_available(mission_id_to_check, locations_done, locations):
|
||||
unfinished_missions = calc_available_missions(locations_done, locations)
|
||||
def is_mission_available(ctx: SC2Context, mission_id_to_check):
|
||||
unfinished_missions = calc_available_missions(ctx)
|
||||
|
||||
return any(mission_id_to_check == locations[mission].id for mission in unfinished_missions)
|
||||
return any(mission_id_to_check == ctx.mission_req_table[mission].id for mission in unfinished_missions)
|
||||
|
||||
|
||||
def mark_up_mission_name(mission, location_table, ui, unlock_table):
|
||||
def mark_up_mission_name(ctx: SC2Context, mission, unlock_table):
|
||||
"""Checks if the mission is required for game completion and adds '*' to the name to mark that."""
|
||||
|
||||
if location_table[mission].completion_critical:
|
||||
if ui:
|
||||
if ctx.mission_req_table[mission].completion_critical:
|
||||
if ctx.ui:
|
||||
message = "[color=AF99EF]" + mission + "[/color]"
|
||||
else:
|
||||
message = "*" + mission + "*"
|
||||
else:
|
||||
message = mission
|
||||
|
||||
if ui:
|
||||
if ctx.ui:
|
||||
unlocks = unlock_table[mission]
|
||||
|
||||
if len(unlocks) > 0:
|
||||
pre_message = f"[ref={list(location_table).index(mission)}|Unlocks: "
|
||||
pre_message += ", ".join(f"{unlock}({location_table[unlock].id})" for unlock in unlocks)
|
||||
pre_message = f"[ref={list(ctx.mission_req_table).index(mission)}|Unlocks: "
|
||||
pre_message += ", ".join(f"{unlock}({ctx.mission_req_table[unlock].id})" for unlock in unlocks)
|
||||
pre_message += f"]"
|
||||
message = pre_message + message + "[/ref]"
|
||||
|
||||
@@ -743,7 +666,7 @@ def mark_up_objectives(message, ctx, unfinished_locations, mission):
|
||||
if ctx.ui:
|
||||
locations = unfinished_locations[mission]
|
||||
|
||||
pre_message = f"[ref={list(ctx.mission_req_table).index(mission)+30}|"
|
||||
pre_message = f"[ref={list(ctx.mission_req_table).index(mission) + 30}|"
|
||||
pre_message += "<br>".join(location for location in locations)
|
||||
pre_message += f"]"
|
||||
formatted_message = pre_message + message + "[/ref]"
|
||||
@@ -751,90 +674,91 @@ def mark_up_objectives(message, ctx, unfinished_locations, mission):
|
||||
return formatted_message
|
||||
|
||||
|
||||
def request_available_missions(locations_done, location_table, ui):
|
||||
if location_table:
|
||||
def request_available_missions(ctx: SC2Context):
|
||||
if ctx.mission_req_table:
|
||||
message = "Available Missions: "
|
||||
|
||||
# Initialize mission unlock table
|
||||
unlocks = initialize_blank_mission_dict(location_table)
|
||||
unlocks = initialize_blank_mission_dict(ctx.mission_req_table)
|
||||
|
||||
missions = calc_available_missions(locations_done, location_table, unlocks)
|
||||
missions = calc_available_missions(ctx, unlocks)
|
||||
message += \
|
||||
", ".join(f"{mark_up_mission_name(mission, location_table, ui, unlocks)}[{location_table[mission].id}]"
|
||||
", ".join(f"{mark_up_mission_name(ctx, mission, unlocks)}"
|
||||
f"[{ctx.mission_req_table[mission].id}]"
|
||||
for mission in missions)
|
||||
|
||||
if ui:
|
||||
ui.log_panels['All'].on_message_markup(message)
|
||||
ui.log_panels['Starcraft2'].on_message_markup(message)
|
||||
if ctx.ui:
|
||||
ctx.ui.log_panels['All'].on_message_markup(message)
|
||||
ctx.ui.log_panels['Starcraft2'].on_message_markup(message)
|
||||
else:
|
||||
sc2_logger.info(message)
|
||||
else:
|
||||
sc2_logger.warning("No mission table found, you are likely not connected to a server.")
|
||||
|
||||
|
||||
def calc_available_missions(locations_done, locations, unlocks=None):
|
||||
def calc_available_missions(ctx: SC2Context, unlocks=None):
|
||||
available_missions = []
|
||||
missions_complete = 0
|
||||
|
||||
# Get number of missions completed
|
||||
for loc in locations_done:
|
||||
if loc % 100 == 0:
|
||||
for loc in ctx.checked_locations:
|
||||
if loc % victory_modulo == 0:
|
||||
missions_complete += 1
|
||||
|
||||
for name in locations:
|
||||
for name in ctx.mission_req_table:
|
||||
# Go through the required missions for each mission and fill up unlock table used later for hover-over tooltips
|
||||
if unlocks:
|
||||
for unlock in locations[name].required_world:
|
||||
unlocks[list(locations)[unlock-1]].append(name)
|
||||
for unlock in ctx.mission_req_table[name].required_world:
|
||||
unlocks[list(ctx.mission_req_table)[unlock - 1]].append(name)
|
||||
|
||||
if mission_reqs_completed(name, missions_complete, locations_done, locations):
|
||||
if mission_reqs_completed(ctx, name, missions_complete):
|
||||
available_missions.append(name)
|
||||
|
||||
return available_missions
|
||||
|
||||
|
||||
def mission_reqs_completed(location_to_check, missions_complete, locations_done, locations):
|
||||
def mission_reqs_completed(ctx: SC2Context, mission_name: str, missions_complete):
|
||||
"""Returns a bool signifying if the mission has all requirements complete and can be done
|
||||
|
||||
Keyword arguments:
|
||||
Arguments:
|
||||
ctx -- instance of SC2Context
|
||||
locations_to_check -- the mission string name to check
|
||||
missions_complete -- an int of how many missions have been completed
|
||||
locations_done -- a list of the location ids that have been complete
|
||||
locations -- a dict of MissionInfo for mission requirements for this world"""
|
||||
if len(locations[location_to_check].required_world) >= 1:
|
||||
"""
|
||||
if len(ctx.mission_req_table[mission_name].required_world) >= 1:
|
||||
# A check for when the requirements are being or'd
|
||||
or_success = False
|
||||
|
||||
# Loop through required missions
|
||||
for req_mission in locations[location_to_check].required_world:
|
||||
for req_mission in ctx.mission_req_table[mission_name].required_world:
|
||||
req_success = True
|
||||
|
||||
# Check if required mission has been completed
|
||||
if not (locations[list(locations)[req_mission-1]].id * 100 + SC2WOL_LOC_ID_OFFSET) in locations_done:
|
||||
if not locations[location_to_check].or_requirements:
|
||||
if not (ctx.mission_req_table[list(ctx.mission_req_table)[req_mission - 1]].id *
|
||||
victory_modulo + SC2WOL_LOC_ID_OFFSET) in ctx.checked_locations:
|
||||
if not ctx.mission_req_table[mission_name].or_requirements:
|
||||
return False
|
||||
else:
|
||||
req_success = False
|
||||
|
||||
# Recursively check required mission to see if it's requirements are met, in case !collect has been done
|
||||
if not mission_reqs_completed(list(locations)[req_mission-1], missions_complete, locations_done,
|
||||
locations):
|
||||
if not locations[location_to_check].or_requirements:
|
||||
if not mission_reqs_completed(ctx, list(ctx.mission_req_table)[req_mission - 1], missions_complete):
|
||||
if not ctx.mission_req_table[mission_name].or_requirements:
|
||||
return False
|
||||
else:
|
||||
req_success = False
|
||||
|
||||
# If requirement check succeeded mark or as satisfied
|
||||
if locations[location_to_check].or_requirements and req_success:
|
||||
if ctx.mission_req_table[mission_name].or_requirements and req_success:
|
||||
or_success = True
|
||||
|
||||
if locations[location_to_check].or_requirements:
|
||||
if ctx.mission_req_table[mission_name].or_requirements:
|
||||
# Return false if or requirements not met
|
||||
if not or_success:
|
||||
return False
|
||||
|
||||
# Check number of missions
|
||||
if missions_complete >= locations[location_to_check].number:
|
||||
if missions_complete >= ctx.mission_req_table[mission_name].number:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@@ -929,7 +853,7 @@ class DllDirectory:
|
||||
self.set(self._old)
|
||||
|
||||
@staticmethod
|
||||
def get() -> str:
|
||||
def get() -> typing.Optional[str]:
|
||||
if sys.platform == "win32":
|
||||
n = ctypes.windll.kernel32.GetDllDirectoryW(0, None)
|
||||
buf = ctypes.create_unicode_buffer(n)
|
||||
|
||||
6
Utils.py
@@ -35,7 +35,7 @@ class Version(typing.NamedTuple):
|
||||
build: int
|
||||
|
||||
|
||||
__version__ = "0.3.4"
|
||||
__version__ = "0.3.5"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
|
||||
is_linux = sys.platform.startswith("linux")
|
||||
@@ -619,7 +619,7 @@ def title_sorted(data: typing.Sequence, key=None, ignore: typing.Set = frozenset
|
||||
def sorter(element: str) -> str:
|
||||
parts = element.split(maxsplit=1)
|
||||
if parts[0].lower() in ignore:
|
||||
return parts[1]
|
||||
return parts[1].lower()
|
||||
else:
|
||||
return element
|
||||
return element.lower()
|
||||
return sorted(data, key=lambda i: sorter(key(i)) if key else sorter(i))
|
||||
|
||||
@@ -12,7 +12,7 @@ ModuleUpdate.update()
|
||||
# in case app gets imported by something like gunicorn
|
||||
import Utils
|
||||
|
||||
Utils.local_path.cached_path = os.path.dirname(__file__)
|
||||
Utils.local_path.cached_path = os.path.dirname(__file__) or "." # py3.8 is not abs. remove "." when dropping 3.8
|
||||
|
||||
from WebHostLib import register, app as raw_app
|
||||
from waitress import serve
|
||||
@@ -104,7 +104,7 @@ def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]]
|
||||
for games in data:
|
||||
if 'Archipelago' in games['gameTitle']:
|
||||
generic_data = data.pop(data.index(games))
|
||||
sorted_data = [generic_data] + Utils.title_sorted(data, key=lambda entry: entry["gameTitle"].lower())
|
||||
sorted_data = [generic_data] + Utils.title_sorted(data, key=lambda entry: entry["gameTitle"])
|
||||
json.dump(sorted_data, json_target, indent=2, ensure_ascii=False)
|
||||
return sorted_data
|
||||
|
||||
|
||||
46
WebHostLib/README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# WebHost
|
||||
|
||||
## Contribution Guidelines
|
||||
**Thank you for your interest in contributing to the Archipelago website!**
|
||||
Much of the content on the website is generated automatically, but there are some things
|
||||
that need a personal touch. For those things, we rely on contributions from both the core
|
||||
team and the community. The current primary maintainer of the website is Farrak Kilhn.
|
||||
He may be found on Discord as `Farrak Kilhn#0418`, or on GitHub as `LegendaryLinux`.
|
||||
|
||||
### Small Changes
|
||||
Little changes like adding a button or a couple new select elements are perfectly fine.
|
||||
Tweaks to style specific to a PR's content are also probably not a problem. For example, if
|
||||
you build a new page which needs two side by side tables, and you need to write a CSS file
|
||||
specific to your page, that is perfectly reasonable.
|
||||
|
||||
### Content Additions
|
||||
Once you develop a new feature or add new content the website, make a pull request. It will
|
||||
be reviewed by the community and there will probably be some discussion around it. Depending
|
||||
on the size of the feature, and if new styles are required, there may be an additional step
|
||||
before the PR is accepted wherein Farrak works with the designer to implement styles.
|
||||
|
||||
### Restrictions on Style Changes
|
||||
A professional designer is paid to develop the styles and assets for the Archipelago website.
|
||||
In an effort to maintain a consistent look and feel, pull requests which *exclusively*
|
||||
change site styles are rejected. Please note this applies to code which changes the overall
|
||||
look and feel of the site, not to small tweaks to CSS for your custom page. The intention
|
||||
behind these restrictions is to maintain a curated feel for the design of the site. If
|
||||
any PR affects the overall feel of the site but includes additive changes, there will
|
||||
likely be a conversation about how to implement those changes without compromising the
|
||||
curated site style. It is therefore worth noting there are a couple files which, if
|
||||
changed in your pull request, will cause it to draw additional scrutiny.
|
||||
|
||||
These closely guarded files are:
|
||||
- `globalStyles.css`
|
||||
- `islandFooter.css`
|
||||
- `landing.css`
|
||||
- `markdown.css`
|
||||
- `tooltip.css`
|
||||
|
||||
### Site Themes
|
||||
There are several themes available for game pages. It is possible to request a new theme in
|
||||
the `#art-and-design` channel on Discord. Because themes are created by the designer, they
|
||||
are not free, and take some time to create. Farrak works closely with the designer to implement
|
||||
these themes, and pays for the assets out of pocket. Therefore, only a couple themes per year
|
||||
are added. If a proposed theme seems like a cool idea and the community likes it, there is a
|
||||
good chance it will become a reality.
|
||||
@@ -103,7 +103,7 @@ class WebHostContext(Context):
|
||||
room.multisave = pickle.dumps(self.get_save())
|
||||
# saving only occurs on activity, so we can "abuse" this information to mark this as last_activity
|
||||
if not exit_save: # we don't want to count a shutdown as activity, which would restart the server again
|
||||
room.last_activity = datetime.utcnow()
|
||||
room.last_activity = datetime.datetime.utcnow()
|
||||
return True
|
||||
|
||||
def get_save(self) -> dict:
|
||||
|
||||
@@ -32,9 +32,12 @@ def download_patch(room_id, patch_id):
|
||||
new_zip.writestr("archipelago.json", json.dumps(manifest))
|
||||
else:
|
||||
new_zip.writestr(file.filename, zf.read(file), file.compress_type, 9)
|
||||
|
||||
if "patch_file_ending" in manifest:
|
||||
patch_file_ending = manifest["patch_file_ending"]
|
||||
else:
|
||||
patch_file_ending = AutoPatchRegister.patch_types[patch.game].patch_file_ending
|
||||
fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](room_id)}" \
|
||||
f"{AutoPatchRegister.patch_types[patch.game].patch_file_ending}"
|
||||
f"{patch_file_ending}"
|
||||
new_file.seek(0)
|
||||
return send_file(new_file, as_attachment=True, download_name=fname)
|
||||
else:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
import os
|
||||
from Utils import __version__
|
||||
from Utils import __version__, local_path
|
||||
from jinja2 import Template
|
||||
import yaml
|
||||
import json
|
||||
@@ -9,14 +9,13 @@ import typing
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
import Options
|
||||
|
||||
target_folder = os.path.join("WebHostLib", "static", "generated")
|
||||
|
||||
handled_in_js = {"start_inventory", "local_items", "non_local_items", "start_hints", "start_location_hints",
|
||||
"exclude_locations"}
|
||||
|
||||
|
||||
def create():
|
||||
os.makedirs(os.path.join(target_folder, 'configs'), exist_ok=True)
|
||||
target_folder = local_path("WebHostLib", "static", "generated")
|
||||
os.makedirs(os.path.join(target_folder, "configs"), exist_ok=True)
|
||||
|
||||
def dictify_range(option: typing.Union[Options.Range, Options.SpecialRange]):
|
||||
data = {}
|
||||
@@ -49,6 +48,11 @@ def create():
|
||||
return list(default_value)
|
||||
return default_value
|
||||
|
||||
def get_html_doc(option_type: type(Options.Option)) -> str:
|
||||
if not option_type.__doc__:
|
||||
return "Please document me!"
|
||||
return "\n".join(line.strip() for line in option_type.__doc__.split("\n")).strip()
|
||||
|
||||
weighted_settings = {
|
||||
"baseOptions": {
|
||||
"description": "Generated by https://archipelago.gg/",
|
||||
@@ -61,12 +65,16 @@ def create():
|
||||
for game_name, world in AutoWorldRegister.world_types.items():
|
||||
|
||||
all_options = {**Options.per_game_common_options, **world.option_definitions}
|
||||
res = Template(open(os.path.join("WebHostLib", "templates", "options.yaml")).read()).render(
|
||||
with open(local_path("WebHostLib", "templates", "options.yaml")) as f:
|
||||
file_data = f.read()
|
||||
res = Template(file_data).render(
|
||||
options=all_options,
|
||||
__version__=__version__, game=game_name, yaml_dump=yaml.dump,
|
||||
dictify_range=dictify_range, default_converter=default_converter,
|
||||
)
|
||||
|
||||
del file_data
|
||||
|
||||
with open(os.path.join(target_folder, 'configs', game_name + ".yaml"), "w") as f:
|
||||
f.write(res)
|
||||
|
||||
@@ -88,7 +96,7 @@ def create():
|
||||
game_options[option_name] = this_option = {
|
||||
"type": "select",
|
||||
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
|
||||
"description": option.__doc__ if option.__doc__ else "Please document me!",
|
||||
"description": get_html_doc(option),
|
||||
"defaultValue": None,
|
||||
"options": []
|
||||
}
|
||||
@@ -114,7 +122,7 @@ def create():
|
||||
game_options[option_name] = {
|
||||
"type": "range",
|
||||
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
|
||||
"description": option.__doc__ if option.__doc__ else "Please document me!",
|
||||
"description": get_html_doc(option),
|
||||
"defaultValue": option.default if hasattr(
|
||||
option, "default") and option.default != "random" else option.range_start,
|
||||
"min": option.range_start,
|
||||
@@ -131,14 +139,14 @@ def create():
|
||||
game_options[option_name] = {
|
||||
"type": "items-list",
|
||||
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
|
||||
"description": option.__doc__ if option.__doc__ else "Please document me!",
|
||||
"description": get_html_doc(option),
|
||||
}
|
||||
|
||||
elif getattr(option, "verify_location_name", False):
|
||||
game_options[option_name] = {
|
||||
"type": "locations-list",
|
||||
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
|
||||
"description": option.__doc__ if option.__doc__ else "Please document me!",
|
||||
"description": get_html_doc(option),
|
||||
}
|
||||
|
||||
elif issubclass(option, Options.OptionList) or issubclass(option, Options.OptionSet):
|
||||
@@ -146,7 +154,7 @@ def create():
|
||||
game_options[option_name] = {
|
||||
"type": "custom-list",
|
||||
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
|
||||
"description": option.__doc__ if option.__doc__ else "Please document me!",
|
||||
"description": get_html_doc(option),
|
||||
"options": list(option.valid_keys),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
flask>=2.1.3
|
||||
flask>=2.2.2
|
||||
pony>=0.7.16
|
||||
waitress>=2.1.1
|
||||
waitress>=2.1.2
|
||||
Flask-Caching>=2.0.1
|
||||
Flask-Compress>=1.12
|
||||
Flask-Limiter>=2.5.0
|
||||
Flask-Limiter>=2.6.2
|
||||
bokeh>=2.4.3
|
||||
|
||||
@@ -102,9 +102,15 @@ const buildOptionsTable = (settings, romOpts = false) => {
|
||||
// td Left
|
||||
const tdl = document.createElement('td');
|
||||
const label = document.createElement('label');
|
||||
label.textContent = `${settings[setting].displayName}: `;
|
||||
label.setAttribute('for', setting);
|
||||
label.setAttribute('data-tooltip', settings[setting].description);
|
||||
label.innerText = `${settings[setting].displayName}:`;
|
||||
|
||||
const questionSpan = document.createElement('span');
|
||||
questionSpan.classList.add('interactive');
|
||||
questionSpan.setAttribute('data-tooltip', settings[setting].description);
|
||||
questionSpan.innerText = '(?)';
|
||||
|
||||
label.appendChild(questionSpan);
|
||||
tdl.appendChild(label);
|
||||
tr.appendChild(tdl);
|
||||
|
||||
|
||||
@@ -56,7 +56,3 @@
|
||||
#file-input{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.interactive{
|
||||
color: #ffef00;
|
||||
}
|
||||
|
||||
@@ -105,3 +105,7 @@ h5, h6{
|
||||
margin-bottom: 20px;
|
||||
background-color: #ffff00;
|
||||
}
|
||||
|
||||
.interactive{
|
||||
color: #ffef00;
|
||||
}
|
||||
@@ -14,7 +14,6 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
|
||||
/* Base styles for the element that has a tooltip */
|
||||
[data-tooltip], .tooltip {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Base styles for the entire tooltip */
|
||||
@@ -55,14 +54,15 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
|
||||
|
||||
/** Content styles */
|
||||
.tooltip:after, [data-tooltip]:after {
|
||||
width: 260px;
|
||||
z-index: 10000;
|
||||
padding: 8px;
|
||||
width: 160px;
|
||||
border-radius: 4px;
|
||||
background-color: #000;
|
||||
background-color: hsla(0, 0%, 20%, 0.9);
|
||||
color: #fff;
|
||||
content: attr(data-tooltip);
|
||||
white-space: pre-wrap;
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
@@ -41,12 +41,11 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="forfeit_mode">Forfeit Permission:</label>
|
||||
<span
|
||||
class="interactive"
|
||||
data-tooltip="A forfeit releases all remaining items from the locations
|
||||
in your world.">(?)
|
||||
</span>
|
||||
<label for="forfeit_mode">Forfeit Permission:
|
||||
<span class="interactive" data-tooltip="A forfeit releases all remaining items from the locations in your world.">
|
||||
(?)
|
||||
</span>
|
||||
</label>
|
||||
</td>
|
||||
<td>
|
||||
<select name="forfeit_mode" id="forfeit_mode">
|
||||
@@ -63,12 +62,11 @@
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<label for="collect_mode">Collect Permission:</label>
|
||||
<span
|
||||
class="interactive"
|
||||
data-tooltip="A collect releases all of your remaining items to you
|
||||
from across the multiworld.">(?)
|
||||
</span>
|
||||
<label for="collect_mode">Collect Permission:
|
||||
<span class="interactive" data-tooltip="A collect releases all of your remaining items to you from across the multiworld.">
|
||||
(?)
|
||||
</span>
|
||||
</label>
|
||||
</td>
|
||||
<td>
|
||||
<select name="collect_mode" id="collect_mode">
|
||||
@@ -85,12 +83,11 @@
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<label for="remaining_mode">Remaining Permission:</label>
|
||||
<span
|
||||
class="interactive"
|
||||
data-tooltip="Remaining lists all items still in your world by name only."
|
||||
>(?)
|
||||
</span>
|
||||
<label for="remaining_mode">Remaining Permission:
|
||||
<span class="interactive" data-tooltip="Remaining lists all items still in your world by name only.">
|
||||
(?)
|
||||
</span>
|
||||
</label>
|
||||
</td>
|
||||
<td>
|
||||
<select name="remaining_mode" id="remaining_mode">
|
||||
@@ -106,11 +103,11 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="item_cheat">Item Cheat:</label>
|
||||
<span
|
||||
class="interactive"
|
||||
data-tooltip="Allows players to use the !getitem command.">(?)
|
||||
</span>
|
||||
<label for="item_cheat">Item Cheat:
|
||||
<span class="interactive" data-tooltip="Allows players to use the !getitem command.">
|
||||
(?)
|
||||
</span>
|
||||
</label>
|
||||
</td>
|
||||
<td>
|
||||
<select name="item_cheat" id="item_cheat">
|
||||
@@ -131,12 +128,11 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="hint_cost"> Hint Cost:</label>
|
||||
<span
|
||||
class="interactive"
|
||||
data-tooltip="After gathering this many checks, players can !hint <itemname>
|
||||
to get the location of that hint item.">(?)
|
||||
</span>
|
||||
<label for="hint_cost"> Hint Cost:
|
||||
<span class="interactive" data-tooltip="After gathering this many checks, players can !hint <itemname> to get the location of that hint item.">
|
||||
(?)
|
||||
</span>
|
||||
</label>
|
||||
</td>
|
||||
<td>
|
||||
<select name="hint_cost" id="hint_cost">
|
||||
@@ -150,11 +146,11 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="server_password">Server Password:</label>
|
||||
<span
|
||||
class="interactive"
|
||||
data-tooltip="Allows for issuing of server console commands from any text client or in-game client using the !admin command.">(?)
|
||||
</span>
|
||||
<label for="server_password">Server Password:
|
||||
<span class="interactive" data-tooltip="Allows for issuing of server console commands from any text client or in-game client using the !admin command.">
|
||||
(?)
|
||||
</span>
|
||||
</label>
|
||||
</td>
|
||||
<td>
|
||||
<input id="server_password" name="server_password">
|
||||
@@ -162,23 +158,22 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="plando_options">Plando Options:</label>
|
||||
<span
|
||||
class="interactive"
|
||||
data-tooltip="Allows players to plan some of the randomization. See the 'Archipelago Plando Guide' in 'Setup Guides' for more information.">(?)
|
||||
Plando Options:
|
||||
<span class="interactive" data-tooltip="Allows players to plan some of the randomization. See the 'Archipelago Plando Guide' in 'Setup Guides' for more information.">
|
||||
(?)
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" name="plando_bosses" value="bosses" checked>
|
||||
<input type="checkbox" id="plando_bosses" name="plando_bosses" value="bosses" checked>
|
||||
<label for="plando_bosses">Bosses</label><br>
|
||||
|
||||
<input type="checkbox" name="plando_items" value="items" checked>
|
||||
<input type="checkbox" id="plando_items" name="plando_items" value="items" checked>
|
||||
<label for="plando_items">Items</label><br>
|
||||
|
||||
<input type="checkbox" name="plando_connections" value="connections" checked>
|
||||
<input type="checkbox" id="plando_connections" name="plando_connections" value="connections" checked>
|
||||
<label for="plando_connections">Connections</label><br>
|
||||
|
||||
<input type="checkbox" name="plando_texts" value="texts" checked>
|
||||
<input type="checkbox" id="plando_texts" name="plando_texts" value="texts" checked>
|
||||
<label for="plando_texts">Text</label>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
-
|
||||
<a href="https://github.com/ArchipelagoMW/Archipelago">Source Code</a>
|
||||
-
|
||||
<a href="https://github.com/ArchipelagoMW/Archipelago/wiki">Wiki</a>
|
||||
-
|
||||
<a href="https://github.com/ArchipelagoMW/Archipelago/graphs/contributors">Contributors</a>
|
||||
-
|
||||
<a href="https://github.com/ArchipelagoMW/Archipelago/issues">Bug Report</a>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
<title>Player Settings</title>
|
||||
<title>Supported Games</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/supportedGames.css") }}" />
|
||||
{% endblock %}
|
||||
|
||||
@@ -97,6 +97,11 @@ local extensionConsumableLookup = {
|
||||
[443] = 0x3F
|
||||
}
|
||||
|
||||
local noOverworldItemsLookup = {
|
||||
[499] = 0x2B,
|
||||
[500] = 0x12,
|
||||
}
|
||||
|
||||
local itemMessages = {}
|
||||
local consumableStacks = nil
|
||||
local prevstate = ""
|
||||
@@ -341,7 +346,7 @@ function processBlock(block)
|
||||
-- This is a key item
|
||||
memoryLocation = memoryLocation - 0x0E0
|
||||
wU8(memoryLocation, 0x01)
|
||||
elseif v >= 0x1E0 then
|
||||
elseif v >= 0x1E0 and v <= 0x1F2 then
|
||||
-- This is a movement item
|
||||
-- Minus Offset (0x100) - movement offset (0xE0)
|
||||
memoryLocation = memoryLocation - 0x1E0
|
||||
@@ -351,7 +356,10 @@ function processBlock(block)
|
||||
else
|
||||
wU8(memoryLocation, 0x01)
|
||||
end
|
||||
|
||||
elseif v >= 0x1F3 and v <= 0x1F4 then
|
||||
-- NoOverworld special items
|
||||
memoryLocation = noOverworldItemsLookup[v]
|
||||
wU8(memoryLocation, 0x01)
|
||||
elseif v >= 0x16C and v <= 0x1AF then
|
||||
-- This is a gold item
|
||||
amountToAdd = goldLookup[v]
|
||||
|
||||
@@ -1,343 +0,0 @@
|
||||
|
||||
|
||||
# How do I add a game to Archipelago?
|
||||
This guide is going to try and be a broad summary of how you can do just that.
|
||||
There are two key steps to incorporating a game into Archipelago:
|
||||
- Game Modification
|
||||
- Archipelago Server Integration
|
||||
|
||||
Refer to the following documents as well:
|
||||
- [network protocol.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/network%20protocol.md) for network communication between client and server.
|
||||
- [world api.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/world%20api.md) for documentation on server side code and creating a world package.
|
||||
|
||||
|
||||
# Game Modification
|
||||
One half of the work required to integrate a game into Archipelago is the development of the game client. This is
|
||||
typically done through a modding API or other modification process, described further down.
|
||||
|
||||
As an example, modifications to a game typically include (more on this later):
|
||||
- Hooking into when a 'location check' is completed.
|
||||
- Networking with the Archipelago server.
|
||||
- Optionally, UI or HUD updates to show status of the multiworld session or Archipelago server connection.
|
||||
|
||||
In order to determine how to modify a game, refer to the following sections.
|
||||
|
||||
## Engine Identification
|
||||
This is a good way to make the modding process much easier. Being able to identify what engine a game was made in is critical. The first step is to look at a game's files. Let's go over what some game files might look like. It’s important that you be able to see file extensions, so be sure to enable that feature in your file viewer of choice.
|
||||
Examples are provided below.
|
||||
|
||||
### Creepy Castle
|
||||

|
||||
|
||||
This is the delightful title Creepy Castle, which is a fantastic game that I highly recommend. It’s also your worst-case
|
||||
scenario as a modder. All that’s present here is an executable file and some meta-information that Steam uses. You have
|
||||
basically nothing here to work with. If you want to change this game, the only option you have is to do some pretty nasty
|
||||
disassembly and reverse engineering work, which is outside the scope of this tutorial. Let’s look at some other examples
|
||||
of game releases.
|
||||
|
||||
### Heavy Bullets
|
||||

|
||||
|
||||
Here’s the release files for another game, Heavy Bullets. We see a .exe file, like expected, and a few more files.
|
||||
“hello.txt” is a text file, which we can quickly skim in any text editor. Many games have them in some form, usually
|
||||
with a name like README.txt, and they may contain information about a game, such as a EULA, terms of service, licensing
|
||||
information, credits, and general info about the game. You usually won’t find anything too helpful here, but it never
|
||||
hurts to check. In this case, it contains some credits and a changelog for the game, so nothing too important.
|
||||
“steam_api.dll” is a file you can safely ignore, it’s just some code used to interface with Steam.
|
||||
The directory “HEAVY_BULLETS_Data”, however, has some good news.
|
||||
|
||||

|
||||
|
||||
Jackpot! It might not be obvious what you’re looking at here, but I can instantly tell from this folder’s contents that
|
||||
what we have is a game made in the Unity Engine. If you look in the sub-folders, you’ll seem some .dll files which affirm
|
||||
our suspicions. Telltale signs for this are directories titled “Managed” and “Mono”, as well as the numbered, extension-less
|
||||
level files and the sharedassets files. We’ll tell you a bit about why seeing a Unity game is such good news later,
|
||||
but for now, this is what one looks like. Also keep your eyes out for an executable with a name like UnityCrashHandler,
|
||||
that’s another dead giveaway.
|
||||
|
||||
### Stardew Valley
|
||||

|
||||
|
||||
This is the game contents of Stardew Valley. A lot more to look at here, but some key takeaways.
|
||||
Notice the .dll files which include “CSharp” in their name. This tells us that the game was made in C#, which is good news.
|
||||
More on that later.
|
||||
|
||||
### Gato Roboto
|
||||

|
||||
|
||||
Our last example is the game Gato Roboto. This game is made in GameMaker, which is another green flag to look out for.
|
||||
The giveaway is the file titled "data.win". This immediately tips us off that this game was made in GameMaker.
|
||||
|
||||
This isn't all you'll ever see looking at game files, but it's a good place to start.
|
||||
As a general rule, the more files a game has out in plain sight, the more you'll be able to change.
|
||||
This especially applies in the case of code or script files - always keep a lookout for anything you can use to your
|
||||
advantage!
|
||||
|
||||
## Open or Leaked Source Games
|
||||
As a side note, many games have either been made open source, or have had source files leaked at some point.
|
||||
This can be a boon to any would-be modder, for obvious reasons.
|
||||
Always be sure to check - a quick internet search for "(Game) Source Code" might not give results often, but when it
|
||||
does you're going to have a much better time.
|
||||
|
||||
Be sure never to distribute source code for games that you decompile or find if you do not have express permission to do
|
||||
so, or to redistribute any materials obtained through similar methods, as this is illegal and unethical.
|
||||
|
||||
## Modifying Release Versions of Games
|
||||
However, for now we'll assume you haven't been so lucky, and have to work with only what’s sitting in your install directory.
|
||||
Some developers are kind enough to deliberately leave you ways to alter their games, like modding tools,
|
||||
but these are often not geared to the kind of work you'll be doing and may not help much.
|
||||
|
||||
As a general rule, any modding tool that lets you write actual code is something worth using.
|
||||
|
||||
### Research
|
||||
The first step is to research your game. Even if you've been dealt the worst hand in terms of engine modification,
|
||||
it's possible other motivated parties have concocted useful tools for your game already.
|
||||
Always be sure to search the Internet for the efforts of other modders.
|
||||
|
||||
### Analysis Tools
|
||||
Depending on the game’s underlying engine, there may be some tools you can use either in lieu of or in addition to existing game tools.
|
||||
|
||||
#### [dnSpy](https://github.com/dnSpy/dnSpy/releases)
|
||||
The first tool in your toolbox is dnSpy.
|
||||
dnSpy is useful for opening and modifying code files, like .exe and .dll files, that were made in C#.
|
||||
This won't work for executable files made by other means, and obfuscated code (code which was deliberately made
|
||||
difficult to reverse engineer) will thwart it, but 9 times out of 10 this is exactly what you need.
|
||||
You'll want to avoid opening common library files in dnSpy, as these are unlikely to contain the data you're looking to
|
||||
modify.
|
||||
|
||||
For Unity games, the file you’ll want to open will be the file (Data Folder)/Managed/Assembly-CSharp.dll, as pictured below:
|
||||
|
||||

|
||||
|
||||
This file will contain the data of the actual game.
|
||||
For other C# games, the file you want is usually just the executable itself.
|
||||
|
||||
With dnSpy, you can view the game’s C# code, but the tool isn’t perfect.
|
||||
Although the names of classes, methods, variables, and more will be preserved, code structures may not remain entirely intact. This is because compilers will often subtly rewrite code to be more optimal, so that it works the same as the original code but uses fewer resources. Compiled C# files also lose comments and other documentation.
|
||||
|
||||
#### [UndertaleModTool](https://github.com/krzys-h/UndertaleModTool/releases)
|
||||
This is currently the best tool for modifying games made in GameMaker, and supports games made in both GMS 1 and 2.
|
||||
It allows you to modify code in GML, if the game wasn't made with the wrong compiler (usually something you don't have
|
||||
to worry about).
|
||||
|
||||
You'll want to open the data.win file, as this is where all the goods are kept.
|
||||
Like dnSpy, you won’t be able to see comments.
|
||||
In addition, you will be able to see and modify many hidden fields on items that GameMaker itself will often hide from
|
||||
creators.
|
||||
|
||||
Fonts in particular are notoriously complex, and to add new sprites you may need to modify existing sprite sheets.
|
||||
|
||||
#### [CheatEngine](https://cheatengine.org/)
|
||||
CheatEngine is a tool with a very long and storied history.
|
||||
Be warned that because it performs live modifications to the memory of other processes, it will likely be flagged as
|
||||
malware (because this behavior is most commonly found in malware and rarely used by other programs).
|
||||
If you use CheatEngine, you need to have a deep understanding of how computers work at the nuts and bolts level,
|
||||
including binary data formats, addressing, and assembly language programming.
|
||||
|
||||
The tool itself is highly complex and even I have not yet charted its expanses.
|
||||
However, it can also be a very powerful tool in the right hands, allowing you to query and modify gamestate without ever
|
||||
modifying the actual game itself.
|
||||
In theory it is compatible with any piece of software you can run on your computer, but there is no "easy way" to do
|
||||
anything with it.
|
||||
|
||||
### What Modifications You Should Make to the Game
|
||||
We talked about this briefly in [Game Modification](#game-modification) section.
|
||||
The next step is to know what you need to make the game do now that you can modify it. Here are your key goals:
|
||||
- Modify the game so that checks are shuffled
|
||||
- Know when the player has completed a check, and react accordingly
|
||||
- Listen for messages from the Archipelago server
|
||||
- Modify the game to display messages from the Archipelago server
|
||||
- Add interface for connecting to the Archipelago server with passwords and sessions
|
||||
- Add commands for manually rewarding, re-syncing, forfeiting, and other actions
|
||||
|
||||
To elaborate, you need to be able to inform the server whenever you check locations, print out messages that you receive
|
||||
from the server in-game so players can read them, award items when the server tells you to, sync and re-sync when necessary,
|
||||
avoid double-awarding items while still maintaining game file integrity, and allow players to manually enter commands in
|
||||
case the client or server make mistakes.
|
||||
|
||||
Refer to the [Network Protocol documentation](./network%20protocol.md) for how to communicate with Archipelago's servers.
|
||||
|
||||
## But my Game is a console game. Can I still add it?
|
||||
That depends – what console?
|
||||
|
||||
### My Game is a recent game for the PS4/Xbox-One/Nintendo Switch/etc
|
||||
Most games for recent generations of console platforms are inaccessible to the typical modder. It is generally advised
|
||||
that you do not attempt to work with these games as they are difficult to modify and are protected by their copyright
|
||||
holders. Most modern AAA game studios will provide a modding interface or otherwise deny modifications for their console games.
|
||||
|
||||
### My Game isn’t that old, it’s for the Wii/PS2/360/etc
|
||||
This is very complex, but doable.
|
||||
If you don't have good knowledge of stuff like Assembly programming, this is not where you want to learn it.
|
||||
There exist many disassembly and debugging tools, but more recent content may have lackluster support.
|
||||
|
||||
### My Game is a classic for the SNES/Sega Genesis/etc
|
||||
That’s a lot more feasible.
|
||||
There are many good tools available for understanding and modifying games on these older consoles, and the emulation
|
||||
community will have figured out the bulk of the console’s secrets.
|
||||
Look for debugging tools, but be ready to learn assembly.
|
||||
Old consoles usually have their own unique dialects of ASM you’ll need to get used to.
|
||||
|
||||
Also make sure there’s a good way to interface with a running emulator, since that’s the only way you can connect these
|
||||
older consoles to the Internet.
|
||||
There are also hardware mods and flash carts, which can do the same things an emulator would when connected to a computer,
|
||||
but these will require the same sort of interface software to be written in order to work properly - from your perspective
|
||||
the two won't really look any different.
|
||||
|
||||
### My Game is an exclusive for the Super Baby Magic Dream Boy. It’s this console from the Soviet Union that-
|
||||
Unless you have a circuit schematic for the Super Baby Magic Dream Boy sitting on your desk, no.
|
||||
Obscurity is your enemy – there will likely be little to no emulator or modding information, and you’d essentially be
|
||||
working from scratch.
|
||||
|
||||
## How to Distribute Game Modifications
|
||||
**NEVER EVER distribute anyone else's copyrighted work UNLESS THEY EXPLICITLY GIVE YOU PERMISSION TO DO SO!!!**
|
||||
|
||||
This is a good way to get any project you're working on sued out from under you.
|
||||
The right way to distribute modified versions of a game's binaries, assuming that the licensing terms do not allow you
|
||||
to copy them wholesale, is as patches.
|
||||
|
||||
There are many patch formats, which I'll cover in brief. The common theme is that you can’t distribute anything that
|
||||
wasn't made by you. Patches are files that describe how your modified file differs from the original one, thus avoiding
|
||||
the issue of distributing someone else’s original work.
|
||||
|
||||
Users who have a copy of the game just need to apply the patch, and those who don’t are unable to play.
|
||||
|
||||
### Patches
|
||||
|
||||
#### IPS
|
||||
IPS patches are a simple list of chunks to replace in the original to generate the output. It is not possible to encode
|
||||
moving of a chunk, so they may inadvertently contain copyrighted material and should be avoided unless you know it's
|
||||
fine.
|
||||
|
||||
#### UPS, BPS, VCDIFF (xdelta), bsdiff
|
||||
Other patch formats generate the difference between two streams (delta patches) with varying complexity. This way it is
|
||||
possible to insert bytes or move chunks without including any original data. Bsdiff is highly optimized and includes
|
||||
compression, so this format is used by APBP.
|
||||
|
||||
Only a bsdiff module is integrated into AP. If the final patch requires or is based on any other patch, convert them to
|
||||
bsdiff or APBP before adding it to the AP source code as "basepatch.bsdiff4" or "basepatch.apbp".
|
||||
|
||||
#### APBP Archipelago Binary Patch
|
||||
Starting with version 4 of the APBP format, this is a ZIP file containing metadata in `archipelago.json` and additional
|
||||
files required by the game / patching process. For ROM-based games the ZIP will include a `delta.bsdiff4` which is the
|
||||
bsdiff between the original and the randomized ROM.
|
||||
|
||||
To make using APBP easy, they can be generated by inheriting from `Patch.APDeltaPatch`.
|
||||
|
||||
### Mod files
|
||||
Games which support modding will usually just let you drag and drop the mod’s files into a folder somewhere.
|
||||
Mod files come in many forms, but the rules about not distributing other people's content remain the same.
|
||||
They can either be generic and modify the game using a seed or `slot_data` from the AP websocket, or they can be
|
||||
generated per seed.
|
||||
|
||||
If the mod is generated by AP and is installed from a ZIP file, it may be possible to include APBP metadata for easy
|
||||
integration into the Webhost by inheriting from `Patch.APContainer`.
|
||||
|
||||
|
||||
## Archipelago Integration
|
||||
Integrating a randomizer into Archipelago involves a few steps.
|
||||
There are several things that may need to be done, but the most important is to create an implementation of the
|
||||
`World` class specific to your game. This implementation should exist as a Python module within the `worlds` folder
|
||||
in the Archipelago file structure.
|
||||
|
||||
This encompasses most of the data for your game – the items available, what checks you have, the logic for reaching those
|
||||
checks, what options to offer for the player’s yaml file, and the code to initialize all this data.
|
||||
|
||||
Here’s an example of what your world module can look like:
|
||||
|
||||

|
||||
|
||||
The minimum requirements for a new archipelago world are the package itself (the world folder containing a file named `__init__.py`),
|
||||
which must define a `World` class object for the game with a game name, create an equal number of items and locations with rules,
|
||||
a win condition, and at least one `Region` object.
|
||||
|
||||
Let's give a quick breakdown of what the contents for these files look like.
|
||||
This is just one example of an Archipelago world - the way things are done below is not an immutable property of Archipelago.
|
||||
|
||||
### Items.py
|
||||
This file is used to define the items which exist in a given game.
|
||||
|
||||

|
||||
|
||||
Some important things to note here. The center of our Items.py file is the item_table, which individually lists every
|
||||
item in the game and associates them with an ItemData.
|
||||
|
||||
This file is rather skeletal - most of the actual data has been stripped out for simplicity.
|
||||
Each ItemData gives a numeric ID to associate with the item and a boolean telling us whether the item might allow the
|
||||
player to do more than they would have been able to before.
|
||||
|
||||
Next there's the item_frequencies. This simply tells Archipelago how many times each item appears in the pool.
|
||||
Items that appear exactly once need not be listed - Archipelago will interpret absence from this dictionary as meaning
|
||||
that the item appears once.
|
||||
|
||||
Lastly, note the `lookup_id_to_name` dictionary, which is typically imported and used in your Archipelago `World`
|
||||
implementation. This is how Archipelago is told about the items in your world.
|
||||
|
||||
### Locations.py
|
||||
This file lists all locations in the game.
|
||||
|
||||

|
||||
|
||||
First is the achievement_table. It lists each location, the region that it can be found in (more on regions later),
|
||||
and a numeric ID to associate with each location.
|
||||
|
||||
The exclusion table is a series of dictionaries which are used to exclude certain checks from the pool of progression
|
||||
locations based on user settings, and the events table associates certain specific checks with specific items.
|
||||
|
||||
`lookup_id_to_name` is also present for locations, though this is a separate dictionary, to be clear.
|
||||
|
||||
### Options.py
|
||||
This file details options to be searched for in a player's YAML settings file.
|
||||
|
||||

|
||||
|
||||
There are several types of option Archipelago has support for.
|
||||
In our case, we have three separate choices a player can toggle, either On or Off.
|
||||
You can also have players choose between a number of predefined values, or have them provide a numeric value within a
|
||||
specified range.
|
||||
|
||||
### Regions.py
|
||||
This file contains data which defines the world's topology.
|
||||
In other words, it details how different regions of the game connect to each other.
|
||||
|
||||

|
||||
|
||||
`terraria_regions` contains a list of tuples.
|
||||
The first element of the tuple is the name of the region, and the second is a list of connections that lead out of the region.
|
||||
|
||||
`mandatory_connections` describe where the connection leads.
|
||||
|
||||
Above this data is a function called `link_terraria_structures` which uses our defined regions and connections to create
|
||||
something more usable for Archipelago, but this has been left out for clarity.
|
||||
|
||||
### Rules.py
|
||||
This is the file that details rules for what players can and cannot logically be required to do, based on items and settings.
|
||||
|
||||

|
||||
|
||||
This is the most complicated part of the job, and is one part of Archipelago that is likely to see some changes in the future.
|
||||
The first class, called `TerrariaLogic`, is an extension of the `LogicMixin` class.
|
||||
This is where you would want to define methods for evaluating certain conditions, which would then return a boolean to
|
||||
indicate whether conditions have been met. Your rule definitions should start with some sort of identifier to delineate it
|
||||
from other games, as all rules are mixed together due to `LogicMixin`. In our case, `_terraria_rule` would be a better name.
|
||||
|
||||
The method below, `set_rules()`, is where you would assign these functions as "rules", using lambdas to associate these
|
||||
functions or combinations of them (or any other code that evaluates to a boolean, in my case just the placeholder `True`)
|
||||
to certain tasks, like checking locations or using entrances.
|
||||
|
||||
### \_\_init\_\_.py
|
||||
This is the file that actually extends the `World` class, and is where you expose functionality and data to Archipelago.
|
||||
|
||||

|
||||
|
||||
This is the most important file for the implementation, and technically the only one you need, but it's best to keep this
|
||||
file as short as possible and use other script files to do most of the heavy lifting.
|
||||
If you've done things well, this will just be where you assign everything you set up in the other files to their associated
|
||||
fields in the class being extended.
|
||||
|
||||
This is also a good place to put game-specific quirky behavior that needs to be managed, as it tends to make things a bit
|
||||
cluttered if you put these things elsewhere.
|
||||
|
||||
The various methods and attributes are documented in `/worlds/AutoWorld.py[World]` and
|
||||
[world api.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/world%20api.md),
|
||||
though it is also recommended to look at existing implementations to see how all this works first-hand.
|
||||
Once you get all that, all that remains to do is test the game and publish your work.
|
||||
11
docs/code_of_conduct.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Code of Conduct
|
||||
We conduct ourselves openly and inclusively here. Please do not contribute to an environment which makes other people uncomfortable. This means that we expect all contributors or participants here to:
|
||||
|
||||
* Be welcoming and inclusive in tone and language.
|
||||
* Be respectful of others and their abilities.
|
||||
* Show empathy when speaking with others.
|
||||
* Be gracious and accept feedback and constructive criticism.
|
||||
|
||||
These guidelines apply to all channels of communication within this GitHub repository. Please be respectful in both public channels, such as issues, and private ones, such as private messaging or emails.
|
||||
|
||||
Any incidents of abuse may be reported directly to ijwu at hmfarran@gmail.com.
|
||||
12
docs/contributing.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Contributing
|
||||
Contributions are welcome. We have a few requests of any new contributors.
|
||||
|
||||
* Ensure that all changes which affect logic are covered by unit tests.
|
||||
* Do not introduce any unit test failures/regressions.
|
||||
* Follow styling as designated in our [styling documentation](/docs/style.md).
|
||||
|
||||
Otherwise, we tend to judge code on a case to case basis.
|
||||
|
||||
For adding a new game to Archipelago and other documentation on how Archipelago functions, please see
|
||||
[the docs folder](docs/) for the relevant information and feel free to ask any questions in the #archipelago-dev
|
||||
channel in our [Discord](https://archipelago.gg/discord).
|
||||
@@ -56,3 +56,8 @@ SNI is required to use SNIClient. If not integrated into the project, it has to
|
||||
You can get the latest SNI release at [SNI Github releases](https://github.com/alttpo/sni/releases).
|
||||
It should be dropped as "SNI" into the root folder of the project. Alternatively, you can point the sni setting in
|
||||
host.yaml at your SNI folder.
|
||||
|
||||
|
||||
## Running tests
|
||||
|
||||
Run `pip install pytest pytest-subtests`, then use your IDE to run tests or run `pytest` from the source folder.
|
||||
|
||||
108
docs/sphinx/AddingArchipelagoIntegration.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# Archipelago Integration
|
||||
Integrating a randomizer into Archipelago involves a few steps.
|
||||
There are several things that may need to be done, but the most important is to create an implementation of the
|
||||
`World` class specific to your game. This implementation should exist as a Python module within the `worlds` folder
|
||||
in the Archipelago file structure.
|
||||
|
||||
This encompasses most of the data for your game – the items available, what checks you have, the logic for reaching those
|
||||
checks, what options to offer for the player’s yaml file, and the code to initialize all this data.
|
||||
|
||||
Here’s an example of what your world module can look like:
|
||||
|
||||

|
||||
|
||||
The minimum requirements for a new archipelago world are the package itself (the world folder containing a file named `__init__.py`),
|
||||
which must define a `World` class object for the game with a game name, create an equal number of items and locations with rules,
|
||||
a win condition, and at least one `Region` object.
|
||||
|
||||
Let's give a quick breakdown of what the contents for these files look like.
|
||||
This is just one example of an Archipelago world - the way things are done below is not an immutable property of Archipelago.
|
||||
|
||||
## Items.py
|
||||
This file is used to define the items which exist in a given game.
|
||||
|
||||

|
||||
|
||||
Some important things to note here. The center of our Items.py file is the item_table, which individually lists every
|
||||
item in the game and associates them with an ItemData.
|
||||
|
||||
This file is rather skeletal - most of the actual data has been stripped out for simplicity.
|
||||
Each ItemData gives a numeric ID to associate with the item and a boolean telling us whether the item might allow the
|
||||
player to do more than they would have been able to before.
|
||||
|
||||
Next there's the item_frequencies. This simply tells Archipelago how many times each item appears in the pool.
|
||||
Items that appear exactly once need not be listed - Archipelago will interpret absence from this dictionary as meaning
|
||||
that the item appears once.
|
||||
|
||||
Lastly, note the `lookup_id_to_name` dictionary, which is typically imported and used in your Archipelago `World`
|
||||
implementation. This is how Archipelago is told about the items in your world.
|
||||
|
||||
## Locations.py
|
||||
This file lists all locations in the game.
|
||||
|
||||

|
||||
|
||||
First is the achievement_table. It lists each location, the region that it can be found in (more on regions later),
|
||||
and a numeric ID to associate with each location.
|
||||
|
||||
The exclusion table is a series of dictionaries which are used to exclude certain checks from the pool of progression
|
||||
locations based on user settings, and the events table associates certain specific checks with specific items.
|
||||
|
||||
`lookup_id_to_name` is also present for locations, though this is a separate dictionary, to be clear.
|
||||
|
||||
## Options.py
|
||||
This file details options to be searched for in a player's YAML settings file.
|
||||
|
||||

|
||||
|
||||
There are several types of option Archipelago has support for.
|
||||
In our case, we have three separate choices a player can toggle, either On or Off.
|
||||
You can also have players choose between a number of predefined values, or have them provide a numeric value within a
|
||||
specified range.
|
||||
|
||||
## Regions.py
|
||||
This file contains data which defines the world's topology.
|
||||
In other words, it details how different regions of the game connect to each other.
|
||||
|
||||

|
||||
|
||||
`terraria_regions` contains a list of tuples.
|
||||
The first element of the tuple is the name of the region, and the second is a list of connections that lead out of the region.
|
||||
|
||||
`mandatory_connections` describe where the connection leads.
|
||||
|
||||
Above this data is a function called `link_terraria_structures` which uses our defined regions and connections to create
|
||||
something more usable for Archipelago, but this has been left out for clarity.
|
||||
|
||||
## Rules.py
|
||||
This is the file that details rules for what players can and cannot logically be required to do, based on items and settings.
|
||||
|
||||

|
||||
|
||||
This is the most complicated part of the job, and is one part of Archipelago that is likely to see some changes in the future.
|
||||
The first class, called `TerrariaLogic`, is an extension of the `LogicMixin` class.
|
||||
This is where you would want to define methods for evaluating certain conditions, which would then return a boolean to
|
||||
indicate whether conditions have been met. Your rule definitions should start with some sort of identifier to delineate it
|
||||
from other games, as all rules are mixed together due to `LogicMixin`. In our case, `_terraria_rule` would be a better name.
|
||||
|
||||
The method below, `set_rules()`, is where you would assign these functions as "rules", using lambdas to associate these
|
||||
functions or combinations of them (or any other code that evaluates to a boolean, in my case just the placeholder `True`)
|
||||
to certain tasks, like checking locations or using entrances.
|
||||
|
||||
## \_\_init\_\_.py
|
||||
This is the file that actually extends the `World` class, and is where you expose functionality and data to Archipelago.
|
||||
|
||||

|
||||
|
||||
This is the most important file for the implementation, and technically the only one you need, but it's best to keep this
|
||||
file as short as possible and use other script files to do most of the heavy lifting.
|
||||
If you've done things well, this will just be where you assign everything you set up in the other files to their associated
|
||||
fields in the class being extended.
|
||||
|
||||
This is also a good place to put game-specific quirky behavior that needs to be managed, as it tends to make things a bit
|
||||
cluttered if you put these things elsewhere.
|
||||
|
||||
The various methods and attributes are documented in `/worlds/AutoWorld.py[World]` and
|
||||
[world api.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/world%20api.md),
|
||||
though it is also recommended to look at existing implementations to see how all this works first-hand.
|
||||
Once you get all that, all that remains to do is test the game and publish your work.
|
||||
225
docs/sphinx/AddingGames.md
Normal file
@@ -0,0 +1,225 @@
|
||||
# Adding Games to Archipelago
|
||||
This guide is going to try and be a broad summary of what is required to add a game integration to Archipelago.
|
||||
|
||||
This guide is not an in-depth tutorial on video game modification nor is it a getting started guide to software or
|
||||
video game development. The intent is to provide information, tips, and tools, to assist a would-be modder in adding a
|
||||
game integration to Archipelago.
|
||||
|
||||
There are two key steps to incorporating a game into Archipelago:
|
||||
- Game Modification
|
||||
- Archipelago Server Integration
|
||||
|
||||
This document covers game modification. Information on creating the Archipelago server integration may be found in the
|
||||
[Adding Archipelago Integration](./AddingArchipelagoIntegration.md).
|
||||
|
||||
## Game Modification
|
||||
One half of the work required to integrate a game into Archipelago is the development of the game client. This is
|
||||
typically done through a modding API or other modification process, this is described further down.
|
||||
|
||||
As an example, modifications to a game typically include:
|
||||
- Hooking into when a "location check" is completed.
|
||||
- Networking with the Archipelago server.
|
||||
- Optionally, UI or HUD updates to show status of the multiworld session or Archipelago server connection.
|
||||
|
||||
### Engine Identification
|
||||
This is a good way to make the modding process much easier. Being able to identify what engine a game was made in is
|
||||
critical. The first step is to look at a game's files. Let's go over what some game files might look like. It’s
|
||||
important that you be able to see file extensions, so be sure to enable that feature in your file viewer of choice.
|
||||
|
||||
#### Examples
|
||||
##### Proprietary Game Engine
|
||||

|
||||
|
||||
This is the game _Creepy Castle_. It’s your worst-case scenario as a modder. All that’s present here is an executable
|
||||
file and some meta-information that Steam uses. You have basically nothing here to work with. If you want to change
|
||||
this game, the only option you have is to do some pretty nasty disassembly and reverse engineering work, which is
|
||||
outside the scope of this tutorial.
|
||||
|
||||
##### Unity Game Engine
|
||||

|
||||
|
||||
Here’s the release files for another game, _Heavy Bullets_. We see a .exe file, like expected, and a few more files.
|
||||
`hello.txt` is a text file, which we can quickly skim in any text editor. Many games have text files in their directories
|
||||
in some form, usually with a name like `README.txt`. They may contain information about a game, such as a EULA, terms
|
||||
of service, licensing information, credits, or other general info about the game. You typically won’t find anything too
|
||||
helpful here, but it never hurts to check.
|
||||
|
||||
In this case, it contains some credits and a changelog for the game, so nothing too important.
|
||||
`steam_api.dll` is a file you can safely ignore, it’s just some code used to interface with Steam.
|
||||
The directory `HEAVY_BULLETS_Data`, however, has some good news.
|
||||
|
||||

|
||||
|
||||
The contents of the `HEAVY_BULLETS_Data` directory follow the pattern typically used by the Unity game engine.
|
||||
If you look in the sub-folders, you’ll seem some .dll files which affirm our suspicions. Telltale signs for this are
|
||||
directories titled `Managed` and `Mono`, as well as the numbered, extension-less level files and the sharedassets files.
|
||||
Also keep your eyes out for an executable with a name like UnityCrashHandler, that’s another dead giveaway.
|
||||
|
||||
##### XNA/FNA
|
||||

|
||||
|
||||
This is the game contents of _Stardew Valley_.
|
||||
A lot more to look at here, but there are some key takeaways. Notice the .dll files which include “CSharp” in their
|
||||
name. Also notice the `Content`. These signs point to a game based on the .NET framework and many games following this
|
||||
style will use the XNA game framework as the base to build their game from.
|
||||
|
||||
##### Gato Roboto
|
||||

|
||||
|
||||
Our last example is the game _Gato Roboto_. Notice the file titled `data.win`. This immediately tips us off that this
|
||||
game was made in GameMaker.
|
||||
|
||||
### Open or Leaked Source Games
|
||||
As a side note, many games have either been made open source, or have had source files leaked at some point.
|
||||
This can be a boon to any would-be modder, for obvious reasons.
|
||||
Always be sure to check - a quick internet search for "(Game) Source Code" might not give results often, but when it
|
||||
does you're going to have a much better time.
|
||||
|
||||
Be sure **never** to distribute source code for games that you decompile or find if you do not have express permission
|
||||
from the author to do so, nor to redistribute any materials obtained through similar methods, as this is illegal and
|
||||
unethical.
|
||||
|
||||
### Modifying Release Versions of Games
|
||||
Some developers are kind enough to deliberately leave you ways to alter their games, like modding tools, but these are
|
||||
often not geared to the kind of work you'll be doing and may not help much. This is usually assessed on a case-by-case
|
||||
basis. Games with large modding communities typically grow around the tooling a developer provides or they grow around
|
||||
the fact that the game is easy to modify in the first place.
|
||||
|
||||
As a general rule, any modding tool that lets you write actual code is something worth using.
|
||||
|
||||
### Creating the Mod
|
||||
#### Research
|
||||
The first step is to research your game. Even if you've been dealt the worst hand in terms of engine modification,
|
||||
it's possible other motivated parties have concocted useful tools for your game already.
|
||||
Always be sure to search the Internet for the efforts of other modders.
|
||||
|
||||
#### Analysis Tools
|
||||
Depending on the game’s underlying engine, there may be some tools you can use either in lieu of or in addition to
|
||||
existing game tools.
|
||||
|
||||
##### ILSpy
|
||||
You can download ILSpy and see more info about it on the [ILSpy GitHub repository homepage](https://github.com/icsharpcode/ILSpy).
|
||||
|
||||
The first tool in your toolbox is ILSpy. ILSpy is a .NET decompiler. The purpose of this program is to take a compiled
|
||||
.NET assembly (.DLL or .EXE file) and turn it back into human-readable source code. A file is a .NET assembly when it
|
||||
was created through the compilation of any programming language targeting the .NET runtime. Usually, the programming
|
||||
language in question is C# (C Sharp).
|
||||
|
||||
Unity games are a combination of native code (compiled in a "native language" such as C++) and .NET code. Most game
|
||||
developers will write the bulk of their code as C# and this will be compiled by Unity into .NET assemblies. Those files
|
||||
may then be decompiled with ILSpy to allow you to see the original source code of the game.
|
||||
|
||||
For Unity games, the file you’ll typically want to open will be the file (Data Folder)/Managed/Assembly-CSharp.dll, as
|
||||
pictured below:
|
||||
|
||||

|
||||
|
||||
For other .NET based games, which are not made in Unity, the file you want is usually just the executable itself.
|
||||
|
||||
Although the names of classes, methods, variables, and more will be preserved, code structures may not remain entirely
|
||||
intact. This is because compilers will often subtly rewrite code to be more optimal, so that it works the same as the
|
||||
original code but uses fewer resources. Compiled C# files also lose comments and other documentation.
|
||||
|
||||
##### UndertaleModTool
|
||||
You can download and find more info on UndertaleModTool on the [UndertaleModTool GitHub repository homepage](https://github.com/krzys-h/UndertaleModTool/releases).
|
||||
This is currently the best tool for modifying games made in GameMaker, and supports games made in both GameMaker Studio
|
||||
1 and 2. It allows you to modify code in GameMaker Language (GML).
|
||||
|
||||
Use the tool to open the `data.win` file to see game data and code.
|
||||
Like ILSpy, you won’t be able to see comments.
|
||||
In addition, you will be able to see and modify many hidden fields on items that GameMaker itself will often hide from
|
||||
creators.
|
||||
|
||||
Fonts in particular are notoriously complex, and to add new sprites you may need to modify existing sprite sheets.
|
||||
|
||||
#### What Modifications You Should Make to the Game
|
||||
We talked about this briefly in [Game Modification](#game-modification) section.
|
||||
The next step is to know what you need to make the game do now that you can modify it. Here are your key goals:
|
||||
- Modify the game so that checks are shuffled
|
||||
- Know when the player has completed a check, and react accordingly
|
||||
- Listen for messages from the Archipelago server
|
||||
- Modify the game to display messages from the Archipelago server
|
||||
- Add interface for connecting to the Archipelago server with passwords and sessions
|
||||
- Add commands for manually rewarding, re-syncing, forfeiting, and other actions
|
||||
|
||||
To elaborate, you need to be able to inform the server whenever you check locations, print out messages that you receive
|
||||
from the server in-game so players can read them, award items when the server tells you to, sync and re-sync when necessary,
|
||||
avoid double-awarding items while still maintaining game file integrity, and allow players to manually enter commands in
|
||||
case the client or server make mistakes.
|
||||
|
||||
Refer to the [Network Protocol documentation](../NetworkProtocol.md) for how to communicate with Archipelago's servers.
|
||||
|
||||
### Modifying Console Games
|
||||
#### My Game is a recent game for the PS4/Xbox-One/Nintendo Switch/etc
|
||||
Most games for recent generations of console platforms are inaccessible to the typical modder. It is generally advised
|
||||
that you do not attempt to work with these games as they are difficult to modify and are protected by their copyright
|
||||
holders. Most modern AAA game studios will provide a modding interface or otherwise deny modifications for their console
|
||||
games.
|
||||
|
||||
There is some traction on this changing as studios are finding ways to include game modifications in console games.
|
||||
|
||||
#### My Game isn’t that old, it’s for the Wii/PS2/360/etc
|
||||
This is very complex, but doable.
|
||||
It is typically necessary to use Assembly (ASM) to modify these games.
|
||||
If you don't have good knowledge of stuff like Assembly programming, this is not where you want to learn it.
|
||||
There exist many disassembly and debugging tools, but more recent content may have lackluster support.
|
||||
|
||||
#### My Game is a classic for the SNES/Sega Genesis/etc
|
||||
That’s a lot more feasible.
|
||||
There are many good tools available for understanding and modifying games on these older consoles, and the emulation
|
||||
community will have figured out the bulk of the console’s secrets. Look for debugging tools, but be ready to learn
|
||||
assembly. Old consoles usually have their own unique dialects of ASM you’ll need to get used to.
|
||||
|
||||
Also make sure there’s a good way to interface with a running emulator, since that’s the only way you can connect these
|
||||
older consoles to the internet. There are also hardware mods and flash carts, which can do the same things an emulator
|
||||
would when connected to a computer. These will require the same sort of interface software to be written in order to
|
||||
work properly--from your perspective the two won't really look any different.
|
||||
|
||||
#### My Game is an exclusive for the Super Baby Magic Dream Boy. It’s this console from the Soviet Union that-
|
||||
Unless you have a circuit schematic for the Super Baby Magic Dream Boy sitting on your desk, no.
|
||||
Obscurity is your enemy – there will likely be little to no emulator or modding information, and you’d essentially be
|
||||
working from scratch. You're welcome to try and break ground on something like this, but understand that community
|
||||
support will range from "extremely limited" to "nonexistent".
|
||||
|
||||
### How to Distribute Game Modifications
|
||||
**NEVER EVER distribute anyone else's copyrighted work UNLESS THEY EXPLICITLY GIVE YOU PERMISSION TO DO SO!!!**
|
||||
|
||||
The right way to distribute modified versions of a game's binaries is to distribute binary patches.
|
||||
|
||||
The common theme is that you can’t distribute anything that wasn't made by you. Patches are files that describe how
|
||||
your modified file differs from the original one without including the original file's content, thus avoiding the issue
|
||||
of distributing someone else’s original work.
|
||||
|
||||
Users who have a copy of the game just need to apply the patch, and those who don’t are unable to play.
|
||||
|
||||
#### Patches
|
||||
The following patch formats are commonly seen in the game modding scene.
|
||||
|
||||
##### IPS
|
||||
IPS patches are a simple list of chunks to replace in the original to generate the output. It is not possible to encode
|
||||
moving of a chunk, so they may inadvertently contain copyrighted material and should be avoided unless you know it's
|
||||
fine.
|
||||
|
||||
##### UPS, BPS, VCDIFF (xdelta), bsdiff
|
||||
Other patch formats generate the difference between two streams (delta patches) with varying complexity. This way it is
|
||||
possible to insert bytes or move chunks without including any original data. Bsdiff is highly optimized and includes
|
||||
compression, so this format is used by APBP.
|
||||
|
||||
Only a bsdiff module is integrated into AP. If the final patch requires or is based on any other patch, convert them to
|
||||
bsdiff or APBP before adding it to the AP source code as "basepatch.bsdiff4" or "basepatch.apbp".
|
||||
|
||||
##### APBP Archipelago Binary Patch
|
||||
Starting with version 4 of the APBP format, this is a ZIP file containing metadata in `archipelago.json` and additional
|
||||
files required by the game / patching process. For ROM-based games the ZIP will include a `delta.bsdiff4` which is the
|
||||
bsdiff between the original and the randomized ROM.
|
||||
|
||||
To make using APBP easy, they can be generated by inheriting from `Patch.APDeltaPatch`.
|
||||
|
||||
#### Mod files
|
||||
Games which support modding will usually just let you drag and drop the mod’s files into a folder somewhere.
|
||||
Mod files come in many forms, but the rules about not distributing other people's content remain the same.
|
||||
They can either be generic and modify the game using a seed or `slot_data` from the AP websocket, or they can be
|
||||
generated per seed.
|
||||
|
||||
If the mod is generated by AP and is installed from a ZIP file, it may be possible to include APBP metadata for easy
|
||||
integration into the Webhost by inheriting from `Patch.APContainer`.
|
||||
101
docs/sphinx/Architecture.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# Archipelago Architecture
|
||||
Archipelago is split into several components. All components must operate in tandem to facilitate randomization
|
||||
and gameplay.
|
||||
|
||||
The components are:
|
||||
* [Archipelago Generator](#archipelago-generator)
|
||||
* [Archipelago Server](#archipelago-server)
|
||||
* [Archipelago Game Client](#archipelago-game-client)
|
||||
|
||||
Some games require additional components in order to facilitate gameplay or communication with Archipelago.
|
||||
The additional components vary from game to game but are typically:
|
||||
* [Retro Console Emulator](#retro-console-emulator)
|
||||
* [Emulator Communication Bridge (SNI)](#emulator-communication-bridge)
|
||||
|
||||
## Archipelago Generator
|
||||
The Archipelago Generator is the part of Archipelago which takes YAML configuration files as input and produces a ZIP
|
||||
file containing the data necessary for the Archipelago Server to service a session. The generator software is standalone
|
||||
from the server or game clients and is run outside the server context. The server may then be pointed to the resulting
|
||||
file to serve that session.
|
||||
|
||||
For more information on using the Archipelago Generator as a user, please visit the user facing MultiWorld Setup Guide
|
||||
section on [Rolling a YAML Locally](https://archipelago.gg/tutorial/Archipelago/setup/en#rolling-a-yaml-locally).
|
||||
|
||||
The Generator functions by using the classes defined in the `/worlds` folder to understand each game's items, location,
|
||||
YAML options, and logic. The "World" classes define these properties in code and are loaded by the generator to allow it
|
||||
to validate YAML options and create a multiworld with cohesive and solvable logic despite the possibility of disparate
|
||||
games being played.
|
||||
|
||||
## Archipelago Server
|
||||
The Archipelago Server facilitates gameplay for a multiworld session. A session may have any number of players.
|
||||
As Archipelago is client-server software the server is still required for sessions even if only a single player is
|
||||
present. The server takes a ZIP file or an ARCHIPELAGO file as input and serves the session using the information from
|
||||
the input to properly serve the game clients over network.
|
||||
|
||||
## Archipelago Game Client
|
||||
Archipelago game clients are currently implemented in two main ways. The first are in-process clients, which operate as
|
||||
a mod loaded within the game process. The game process will then facilitate the WebSocket communication with the
|
||||
Archipelago Server. Typically, more "modern" games will use this approach as they are typically easier to mod or are
|
||||
easier to inject with code at runtime.
|
||||
|
||||
Some examples of Archipelago games implementing the in-process model are:
|
||||
* [Risk of Rain 2](https://github.com/Ijwu/Archipelago.RiskOfRain2)
|
||||
* [Subnautica](https://github.com/Berserker66/ArchipelagoSubnauticaModSrc)
|
||||
* [Hollow Knight](https://github.com/Ijwu/Archipelago.HollowKnight)
|
||||
|
||||
The in-process model can be visualized using the following diagram:
|
||||
```{mermaid}
|
||||
flowchart LR
|
||||
APS[Archipelago Server]
|
||||
APGC[Archipelago Game Client]
|
||||
|
||||
APS <-- WebSockets --> APGC
|
||||
```
|
||||
|
||||
The second model of game client are those which operate out-of-process. The out-of-process clients are shipped with the
|
||||
Archipelago installation and live within the Archipelago codebase. They are implemented in Python using [CommonClient.py](https://github.com/ArchipelagoMW/Archipelago/blob/main/CommonClient.py)
|
||||
as a base. This client model is typically used for games in which runtime modification is difficult to impossible and for
|
||||
games which require additional components such as the emulator communication bridge. This model is also used for clients
|
||||
which communicate with the game from outside the game process to understand game state; the client then communicates
|
||||
updates to the Archipelago server based on the game state.
|
||||
|
||||
Some examples of Archipelago games implementing the out-of-process model are:
|
||||
* [Starcraft 2](https://github.com/ArchipelagoMW/Archipelago/blob/main/Starcraft2Client.py)
|
||||
* [Factorio](https://github.com/ArchipelagoMW/Archipelago/blob/main/FactorioClient.py)
|
||||
* [The Legend of Zelda: Ocarina of Time](https://github.com/ArchipelagoMW/Archipelago/blob/main/OoTClient.py)
|
||||
|
||||
The out-of-process model can be visualized using the following diagram:
|
||||
```{mermaid}
|
||||
flowchart LR
|
||||
APS[Archipelago Server]
|
||||
OOPGC[Out-of-Process Game Client]
|
||||
GP[Game Process]
|
||||
|
||||
APS <-- WebSockets --> OOPGC <--> GP
|
||||
```
|
||||
|
||||
Games which use the [SNI](https://github.com/alttpo/sni) emulator communication bridge can be connected to Archipelago using the [SNIClient](https://github.com/ArchipelagoMW/Archipelago/blob/main/SNIClient.py).
|
||||
|
||||
Games communicating using SNI may be visualized using the following diagram:
|
||||
```{mermaid}
|
||||
flowchart LR
|
||||
APS[Archipelago Server]
|
||||
SNIC[SNIClient]
|
||||
SNI[SNI]
|
||||
GP[Game Process]
|
||||
|
||||
APS <-- WebSockets --> SNIC <-- WebSockets --> SNI <--> GP
|
||||
```
|
||||
|
||||
## Retro Console Emulator
|
||||
Some game implementations require the use of an emulator in order to run the game and to communicate with SNI.
|
||||
These games are typically "retro" games which were released on 8-bit or 16-bit consoles, although newer consoles may be
|
||||
included for some game implementations.
|
||||
|
||||
All emulators currently used in Archipelago game implementations which require them are lua enabled and use a lua script
|
||||
to communicate with SNI.
|
||||
|
||||
## Emulator Communication Bridge
|
||||
All implementations of game clients for which the game is run in an emulator presently use [SuperNintendoInterface or SNI](https://github.com/alttpo/sni)
|
||||
to communicate between the emulator and the SNIClient. The emulator uses lua to communicate to SNI which communicates with
|
||||
the SNIClient which communicates with the Archipelago server.
|
||||
19
docs/sphinx/AutoWorld.md
Normal file
@@ -0,0 +1,19 @@
|
||||
World Class
|
||||
===========
|
||||
|
||||
```{eval-rst}
|
||||
.. currentmodule:: worlds.AutoWorld
|
||||
.. autoclass:: World
|
||||
:members: options, game, topology_present, all_item_and_group_names,
|
||||
item_name_to_id, location_name_to_id, item_name_groups, data_version,
|
||||
required_client_version, required_server_version, hint_blacklist,
|
||||
remote_items, remote_start_inventory, forced_auto_forfeit, hidden,
|
||||
world, player, item_id_to_name, location_id_to_name, item_names,
|
||||
location_names, web, assert_generate, generate_early, create_regions,
|
||||
create_items, set_rules, generate_basic, pre_fill, fill_hook, post_fill,
|
||||
generate_output, fill_slot_data, modify_multidata, write_spoiler_header,
|
||||
write_spoiler, write_spoiler_end, create_item, get_filler_item_name,
|
||||
collect_item, get_pre_fill_items
|
||||
:undoc-members:
|
||||
:special-members: __init__
|
||||
```
|
||||
20
docs/sphinx/Makefile
Normal file
@@ -0,0 +1,20 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line, and also
|
||||
# from the environment for the first two.
|
||||
SPHINXOPTS ?=
|
||||
SPHINXBUILD ?= sphinx-build
|
||||
SOURCEDIR = .
|
||||
BUILDDIR = _build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
@@ -1,4 +1,8 @@
|
||||
```mermaid
|
||||
# Archipelago Network Diagram
|
||||
(Psst, scroll down and zoom in.)
|
||||
|
||||
|
||||
```{mermaid}
|
||||
flowchart LR
|
||||
%% Diagram arranged specifically so output generates no terrible crossing lines.
|
||||
%% AP Server
|
||||
@@ -69,12 +73,6 @@ flowchart LR
|
||||
end
|
||||
SNI <-- Various, depending on SNES device --> SMZ
|
||||
|
||||
%% Donkey Kong Country 3
|
||||
subgraph Donkey Kong Country 3
|
||||
DK3[SNES]
|
||||
end
|
||||
SNI <-- Various, depending on SNES device --> DK3
|
||||
|
||||
%% Native Clients or Games
|
||||
%% Games or clients which compile to native or which the client is integrated in the game.
|
||||
subgraph "Native"
|
||||
@@ -88,12 +86,10 @@ flowchart LR
|
||||
MT[Meritous]
|
||||
TW[The Witness]
|
||||
SA2B[Sonic Adventure 2: Battle]
|
||||
DS3[Dark Souls 3]
|
||||
|
||||
APCLIENTPP <--> SOE
|
||||
APCLIENTPP <--> MT
|
||||
APCLIENTPP <-- The Witness Randomizer --> TW
|
||||
APCLIENTPP <--> DS3
|
||||
APCPP <--> SM64
|
||||
APCPP <--> V6
|
||||
APCPP <--> SA2B
|
||||
@@ -1,35 +1,55 @@
|
||||
# Archipelago General Client
|
||||
# Archipelago Network Protocol
|
||||
## Archipelago Connection Handshake
|
||||
These steps should be followed in order to establish a gameplay connection with an Archipelago session.
|
||||
|
||||
1. Client establishes WebSocket connection to Archipelago server.
|
||||
2. Server accepts connection and responds with a [RoomInfo](#RoomInfo) packet.
|
||||
3. Client may send a [GetDataPackage](#GetDataPackage) packet.
|
||||
4. Server sends a [DataPackage](#DataPackage) packet in return. (If the client sent GetDataPackage.)
|
||||
5. Client sends [Connect](#Connect) packet in order to authenticate with the server.
|
||||
6. Server validates the client's packet and responds with [Connected](#Connected) or [ConnectionRefused](#ConnectionRefused).
|
||||
7. Server may send [ReceivedItems](#ReceivedItems) to the client, in the case that the client is missing items that are queued up for it.
|
||||
8. Server sends [Print](#Print) to all players to notify them of the new client connection.
|
||||
2. Server accepts connection and responds with a [RoomInfo](#roominfo) packet.
|
||||
3. Client may send a [GetDataPackage](#getdatapackage) packet.
|
||||
4. Server sends a [DataPackage](#datapackage) packet in return. (If the client sent GetDataPackage.)
|
||||
5. Client sends [Connect](#connect) packet in order to authenticate with the server.
|
||||
6. Server validates the client's packet and responds with [Connected](#connected) or
|
||||
[ConnectionRefused](#connectionrefused).
|
||||
7. Server may send [ReceivedItems](#receiveditems) to the client, in the case that the client is missing items that
|
||||
are queued up for it.
|
||||
8. Server sends [Print](#print) to all players to notify them of the new client connection.
|
||||
|
||||
In the case that the client does not authenticate properly and receives a [ConnectionRefused](#ConnectionRefused) then the server will maintain the connection and allow for follow-up [Connect](#Connect) packet.
|
||||
In the case that the client does not authenticate properly and receives a [ConnectionRefused](#connectionrefused) then
|
||||
the server will maintain the connection and allow for follow-up [Connect](#connect) packet.
|
||||
|
||||
There are libraries available that implement this network protocol in [Python](https://github.com/ArchipelagoMW/Archipelago/blob/main/CommonClient.py), [Java](https://github.com/ArchipelagoMW/Archipelago.MultiClient.Java), [.Net](https://github.com/ArchipelagoMW/Archipelago.MultiClient.Net) and [C++](https://github.com/black-sliver/apclientpp)
|
||||
There are also a number of community-supported libraries available that implement this network protocol to make integrating with Archipelago easier.
|
||||
|
||||
For Super Nintendo games there are clients available in either [Node](https://github.com/ArchipelagoMW/SuperNintendoClient) or [Python](https://github.com/ArchipelagoMW/Archipelago/blob/main/SNIClient.py), There are also game specific clients available for [The Legend of Zelda: Ocarina of Time](https://github.com/ArchipelagoMW/Z5Client) or [Final Fantasy 1](https://github.com/ArchipelagoMW/Archipelago/blob/main/FF1Client.py)
|
||||
| Language/Runtime | Project | Remarks |
|
||||
|-------------------------------|----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------|
|
||||
| Python | [Archipelago CommonClient](https://github.com/ArchipelagoMW/Archipelago/blob/main/CommonClient.py) | |
|
||||
| | [Archipelago SNIClient](https://github.com/ArchipelagoMW/Archipelago/blob/main/SNIClient.py) | For Super Nintendo Game Support; Utilizes [SNI](https://github.com/alttpo/sni). |
|
||||
| JVM (Java / Kotlin) | [Archipelago.MultiClient.Java](https://github.com/ArchipelagoMW/Archipelago.MultiClient.Java) | |
|
||||
| .NET (C# / C++ / F# / VB.NET) | [Archipelago.MultiClient.Net](https://www.nuget.org/packages/Archipelago.MultiClient.Net) | |
|
||||
| C++ | [apclientpp](https://github.com/black-sliver/apclientpp) | almost-header-only |
|
||||
| | [APCpp](https://github.com/N00byKing/APCpp) | CMake |
|
||||
| JavaScript / TypeScript | [archipelago.js](https://www.npmjs.com/package/archipelago.js) | Browser and Node.js Supported |
|
||||
| Haxe | [hxArchipelago](https://lib.haxe.org/p/hxArchipelago) | |
|
||||
|
||||
## Synchronizing Items
|
||||
When the client receives a [ReceivedItems](#ReceivedItems) packet, if the `index` argument does not match the next index that the client expects then it is expected that the client will re-sync items with the server. This can be accomplished by sending the server a [Sync](#Sync) packet and then a [LocationChecks](#LocationChecks) packet.
|
||||
When the client receives a [ReceivedItems](#receiveditems) packet, if the `index` argument does not match the next index
|
||||
that the client expects then it is expected that the client will re-sync items with the server. This can be accomplished
|
||||
by sending the server a [Sync](#sync) packet and then a [LocationChecks](#locationchecks) packet.
|
||||
|
||||
Even if the client detects a desync, it can still accept the items provided in this packet to prevent gameplay interruption.
|
||||
Even if the client detects a desync, it can still accept the items provided in this packet to prevent gameplay
|
||||
interruption.
|
||||
|
||||
When the client receives a [ReceivedItems](#ReceivedItems) packet and the `index` arg is `0` (zero) then the client should accept the provided `items` list as its full inventory. (Abandon previous inventory.)
|
||||
When the client receives a [ReceivedItems](#receiveditems) packet and the `index` arg is `0` (zero) then the client
|
||||
should accept the provided `items` list as its full inventory. (Abandon previous inventory.)
|
||||
|
||||
# Archipelago Protocol Packets
|
||||
Packets are sent between the multiworld server and client in order to sync information between them. Below is a directory of each packet.
|
||||
## Archipelago Protocol Packets
|
||||
Packets are sent between the multiworld server and client in order to sync information between them.
|
||||
Below is a directory of each packet.
|
||||
|
||||
Packets are simple JSON lists in which any number of ordered network commands can be sent, which are objects. Each command has a "cmd" key, indicating its purpose. All packet argument types documented here refer to JSON types, unless otherwise specified.
|
||||
Packets are simple JSON lists in which any number of ordered network commands can be sent, which are objects.
|
||||
Each command has a "cmd" key, indicating its purpose. All packet argument types documented here refer to JSON types,
|
||||
unless otherwise specified.
|
||||
|
||||
An object can contain the "class" key, which will tell the content data type, such as "Version" in the following example.
|
||||
An object can contain the "class" key, which will tell the content data type, such as "Version" in the following
|
||||
example.
|
||||
|
||||
Example:
|
||||
```javascript
|
||||
@@ -37,40 +57,41 @@ Example:
|
||||
```
|
||||
|
||||
## (Server -> Client)
|
||||
These packets are are sent from the multiworld server to the client. They are not messages which the server accepts.
|
||||
* [RoomInfo](#RoomInfo)
|
||||
* [ConnectionRefused](#ConnectionRefused)
|
||||
* [Connected](#Connected)
|
||||
* [ReceivedItems](#ReceivedItems)
|
||||
* [LocationInfo](#LocationInfo)
|
||||
* [RoomUpdate](#RoomUpdate)
|
||||
* [Print](#Print)
|
||||
* [PrintJSON](#PrintJSON)
|
||||
* [DataPackage](#DataPackage)
|
||||
* [Bounced](#Bounced)
|
||||
* [InvalidPacket](#InvalidPacket)
|
||||
* [Retrieved](#Retrieved)
|
||||
* [SetReply](#SetReply)
|
||||
These packets are sent from the multiworld server to the client. They are not messages which the server accepts.
|
||||
* [RoomInfo](#roominfo)
|
||||
* [ConnectionRefused](#connectionrefused)
|
||||
* [Connected](#connected)
|
||||
* [ReceivedItems](#receiveditems)
|
||||
* [LocationInfo](#locationinfo)
|
||||
* [RoomUpdate](#roomupdate)
|
||||
* [Print](#print)
|
||||
* [PrintJSON](#printjson)
|
||||
* [DataPackage](#datapackage)
|
||||
* [Bounced](#bounced)
|
||||
* [InvalidPacket](#invalidpacket)
|
||||
* [Retrieved](#retrieved)
|
||||
* [SetReply](#setreply)
|
||||
|
||||
### RoomInfo
|
||||
Sent to clients when they connect to an Archipelago server.
|
||||
#### Arguments
|
||||
| Name | Type | Notes |
|
||||
| ---- | ---- | ----- |
|
||||
| version | [NetworkVersion](#NetworkVersion) | Object denoting the version of Archipelago which the server is running. |
|
||||
| version | [NetworkVersion](#networkversion) | Object denoting the version of Archipelago which the server is running. |
|
||||
| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. Example: `WebHost` |
|
||||
| password | bool | Denoted whether a password is required to join this room.|
|
||||
| permissions | dict\[str, [Permission](#Permission)\[int\]\] | Mapping of permission name to [Permission](#Permission), keys are: "forfeit", "collect" and "remaining". |
|
||||
| permissions | dict\[str, [Permission](#permission)\[int\]\] | Mapping of permission name to [Permission](#permission), keys are: "forfeit", "collect" and "remaining". |
|
||||
| hint_cost | int | The amount of points it costs to receive a hint from the server. |
|
||||
| location_check_points | int | The amount of hint points you receive per item/location check completed. ||
|
||||
| games | list\[str\] | List of games present in this multiworld. |
|
||||
| datapackage_version | int | Sum of individual games' datapackage version. Deprecated. Use `datapackage_versions` instead. |
|
||||
| datapackage_versions | dict\[str, int\] | Data versions of the individual games' data packages the server will send. Used to decide which games' caches are outdated. See [Data Package Contents](#Data-Package-Contents). |
|
||||
| datapackage_versions | dict\[str, int\] | Data versions of the individual games' data packages the server will send. Used to decide which games' caches are outdated. See [Data Package Contents](#data-package-contents). |
|
||||
| seed_name | str | uniquely identifying name of this generation |
|
||||
| time | float | Unix time stamp of "now". Send for time synchronization if wanted for things like the DeathLink Bounce. |
|
||||
|
||||
#### forfeit
|
||||
Dictates what is allowed when it comes to a player forfeiting their run. A forfeit is an action which distributes the rest of the items in a player's run to those other players awaiting them.
|
||||
Dictates what is allowed when it comes to a player forfeiting their run. A forfeit is an action which distributes the
|
||||
rest of the items in a player's run to those other players awaiting them.
|
||||
|
||||
* `auto`: Distributes a player's items to other players when they complete their goal.
|
||||
* `enabled`: Denotes that players may forfeit at any time in the game.
|
||||
@@ -79,7 +100,8 @@ Dictates what is allowed when it comes to a player forfeiting their run. A forfe
|
||||
* `goal`: Allows for manual use of forfeit command once a player completes their goal. (Disabled until goal completion)
|
||||
|
||||
#### collect
|
||||
Dictates what is allowed when it comes to a player collecting their run. A collect is an action which sends the rest of the items in a player's run.
|
||||
Dictates what is allowed when it comes to a player collecting their run. A collect is an action which sends the rest of
|
||||
the items in a player's run.
|
||||
|
||||
* `auto`: Automatically when they complete their goal.
|
||||
* `enabled`: Denotes that players may !collect at any time in the game.
|
||||
@@ -113,13 +135,13 @@ Sent to clients when the connection handshake is successfully completed.
|
||||
#### Arguments
|
||||
| Name | Type | Notes |
|
||||
| ---- | ---- | ----- |
|
||||
| team | int | Your team number. See [NetworkPlayer](#NetworkPlayer) for more info on team number. |
|
||||
| slot | int | Your slot number on your team. See [NetworkPlayer](#NetworkPlayer) for more info on the slot number. |
|
||||
| players | list\[[NetworkPlayer](#NetworkPlayer)\] | List denoting other players in the multiworld, whether connected or not. |
|
||||
| team | int | Your team number. See [NetworkPlayer](#networkplayer) for more info on team number. |
|
||||
| slot | int | Your slot number on your team. See [NetworkPlayer](#networkplayer) for more info on the slot number. |
|
||||
| players | list\[[NetworkPlayer](#networkplayer)\] | List denoting other players in the multiworld, whether connected or not. |
|
||||
| missing_locations | list\[int\] | Contains ids of remaining locations that need to be checked. Useful for trackers, among other things. |
|
||||
| checked_locations | list\[int\] | Contains ids of all locations that have been checked. Useful for trackers, among other things. Location ids are in the range of ± 2<sup>53</sup>-1. |
|
||||
| slot_data | dict | Contains a json object for slot related data, differs per game. Empty if not required. |
|
||||
| slot_info | dict\[int, [NetworkSlot](#NetworkSlot)\] | maps each slot to a [NetworkSlot](#NetworkSlot) information |
|
||||
| slot_info | dict\[int, [NetworkSlot](#networkslot)\] | maps each slot to a [NetworkSlot](#networkslot) information |
|
||||
|
||||
### ReceivedItems
|
||||
Sent to clients when they receive an item.
|
||||
@@ -127,78 +149,93 @@ Sent to clients when they receive an item.
|
||||
| Name | Type | Notes |
|
||||
| ---- | ---- | ----- |
|
||||
| index | int | The next empty slot in the list of items for the receiving client. |
|
||||
| items | list\[[NetworkItem](#NetworkItem)\] | The items which the client is receiving. |
|
||||
| items | list\[[NetworkItem](#networkitem)\] | The items which the client is receiving. |
|
||||
|
||||
### LocationInfo
|
||||
Sent to clients to acknowledge a received [LocationScouts](#LocationScouts) packet and responds with the item in the location(s) being scouted.
|
||||
Sent to clients to acknowledge a received [LocationScouts](#locationscouts) packet and responds with the item in the location(s) being scouted.
|
||||
#### Arguments
|
||||
| Name | Type | Notes |
|
||||
| ---- | ---- | ----- |
|
||||
| locations | list\[[NetworkItem](#NetworkItem)\] | Contains list of item(s) in the location(s) scouted. |
|
||||
| locations | list\[[NetworkItem](#networkitem)\] | Contains list of item(s) in the location(s) scouted. |
|
||||
|
||||
### RoomUpdate
|
||||
Sent when there is a need to update information about the present game session. Generally useful for async games.
|
||||
Once authenticated (received Connected), this may also contain data from Connected.
|
||||
#### Arguments
|
||||
The arguments for RoomUpdate are identical to [RoomInfo](#RoomInfo) barring:
|
||||
The arguments for RoomUpdate are identical to [RoomInfo](#roominfo) barring:
|
||||
|
||||
| Name | Type | Notes |
|
||||
| ---- | ---- | ----- |
|
||||
| hint_points | int | New argument. The client's current hint points. |
|
||||
| players | list\[[NetworkPlayer](#NetworkPlayer)\] | Send in the event of an alias rename. Always sends all players, whether connected or not. |
|
||||
| players | list\[[NetworkPlayer](#networkplayer)\] | Send in the event of an alias rename. Always sends all players, whether connected or not. |
|
||||
| checked_locations | list\[int\] | May be a partial update, containing new locations that were checked, especially from a coop partner in the same slot. |
|
||||
| missing_locations | list\[int\] | Should never be sent as an update, if needed is the inverse of checked_locations. |
|
||||
|
||||
All arguments for this packet are optional, only changes are sent.
|
||||
|
||||
### Print
|
||||
Sent to clients purely to display a message to the player.
|
||||
Sent to clients purely to display a message to the player.
|
||||
* *Deprecation warning: clients that connect with version 0.3.5 or higher will nolonger recieve Print packets, instead all messsages are send as [PrintJSON](#PrintJSON)*
|
||||
#### Arguments
|
||||
| Name | Type | Notes |
|
||||
| ---- | ---- | ----- |
|
||||
| text | str | Message to display to player. |
|
||||
|
||||
### PrintJSON
|
||||
Sent to clients purely to display a message to the player. This packet differs from [Print](#Print) in that the data being sent with this packet allows for more configurable or specific messaging.
|
||||
Sent to clients purely to display a message to the player. This packet differs from [Print](#print) in that the data
|
||||
being sent with this packet allows for more configurable or specific messaging.
|
||||
#### Arguments
|
||||
| Name | Type | Notes |
|
||||
| ---- | ---- | ----- |
|
||||
| data | list\[[JSONMessagePart](#JSONMessagePart)\] | Type of this part of the message. |
|
||||
| type | str | May be present to indicate the nature of this message. Known types are Hint and ItemSend. |
|
||||
| type | str | May be present to indicate the [PrintJsonType](#PrintJsonType) of this message. |
|
||||
| receiving | int | Is present if type is Hint or ItemSend and marks the destination player's ID. |
|
||||
| item | [NetworkItem](#NetworkItem) | Is present if type is Hint or ItemSend and marks the source player id, location id, item id and item flags. |
|
||||
| item | [NetworkItem](#networkitem) | Is present if type is Hint or ItemSend and marks the source player id, location id, item id and item flags. |
|
||||
| found | bool | Is present if type is Hint, denotes whether the location hinted for was checked. |
|
||||
| countdown | int | Is present if type is `Countdown`, denotes the amount of seconds remaining on the countdown. |
|
||||
|
||||
##### PrintJsonType
|
||||
PrintJsonType indicates the type of [PrintJson](#PrintJson) packet, different types can be handled differently by the client and can also contain additional arguments. When receiving an unknown type the data's list\[[JSONMessagePart](#JSONMessagePart)\] should still be printed as normal.
|
||||
|
||||
Currently defined types are:
|
||||
| Type | Notes |
|
||||
| ---- | ----- |
|
||||
| ItemSend | The message is in response to a player receiving an item. |
|
||||
| Hint | The message is in response to a player hinting. |
|
||||
| Countdown | The message contains information about the current server Countdown. |
|
||||
|
||||
### DataPackage
|
||||
Sent to clients to provide what is known as a 'data package' which contains information to enable a client to most easily communicate with the Archipelago server. Contents include things like location id to name mappings, among others; see [Data Package Contents](#Data-Package-Contents) for more info.
|
||||
Sent to clients to provide what is known as a 'data package' which contains information to enable a client to most
|
||||
easily communicate with the Archipelago server. Contents include things like location id to name mappings,
|
||||
among others; see [Data Package Contents](#data-package-contents) for more info.
|
||||
|
||||
#### Arguments
|
||||
| Name | Type | Notes |
|
||||
| ---- | ---- | ----- |
|
||||
| data | [DataPackageObject](#Data-Package-Contents) | The data package as a JSON object. |
|
||||
| data | [DataPackageObject](#data-package-contents) | The data package as a JSON object. |
|
||||
|
||||
### Bounced
|
||||
Sent to clients after a client requested this message be sent to them, more info in the [Bounce](#Bounce) package.
|
||||
Sent to clients after a client requested this message be sent to them, more info in the [Bounce](#bounce) package.
|
||||
|
||||
#### Arguments
|
||||
| Name | Type | Notes |
|
||||
| ---- | ---- | ----- |
|
||||
| games | list\[str\] | Optional. Game names this message is targeting |
|
||||
| slots | list\[int\] | Optional. Player slot IDs that this message is targeting |
|
||||
| tags | list\[str\] | Optional. Client [Tags](#Tags) this message is targeting |
|
||||
| data | dict | The data in the [Bounce](#Bounce) package copied |
|
||||
| tags | list\[str\] | Optional. Client [Tags](#tags) this message is targeting |
|
||||
| data | dict | The data in the [Bounce](#bounce) package copied |
|
||||
|
||||
### InvalidPacket
|
||||
Sent to clients if the server caught a problem with a packet. This only occurs for errors that are explicitly checked for.
|
||||
|
||||
#### Arguments
|
||||
| Name | Type | Notes |
|
||||
| ---- | ---- | ----- |
|
||||
| type | str | The [PacketProblemType](#PacketProblemType) that was detected in the packet. |
|
||||
| Name | Type | Notes |
|
||||
|--------------|---------------|-------------------------------------------------------------------------------------------|
|
||||
| type | str | The [PacketProblemType](#packetproblemtype) that was detected in the packet. |
|
||||
| original_cmd | Optional[str] | The `cmd` argument of the faulty packet, will be `None` if the `cmd` failed to be parsed. |
|
||||
| text | str | A descriptive message of the problem at hand. |
|
||||
| text | str | A descriptive message of the problem at hand. |
|
||||
|
||||
##### PacketProblemType
|
||||
#### PacketProblemType
|
||||
`PacketProblemType` indicates the type of problem that was detected in the faulty packet, the known problem types are below but others may be added in the future.
|
||||
|
||||
| Type | Notes |
|
||||
@@ -207,16 +244,19 @@ Sent to clients if the server caught a problem with a packet. This only occurs f
|
||||
| arguments | Arguments of the faulty packet which were not correct. |
|
||||
|
||||
### Retrieved
|
||||
Sent to clients as a response the a [Get](#Get) package.
|
||||
Sent to clients as a response the a [Get](#get) package
|
||||
#### Arguments
|
||||
| Name | Type | Notes |
|
||||
| ---- | ---- | ----- |
|
||||
| keys | dict\[str\, any] | A key-value collection containing all the values for the keys requested in the [Get](#Get) package. |
|
||||
| keys | dict\[str\, any] | A key-value collection containing all the values for the keys requested in the [Get](#get) package. |
|
||||
|
||||
Additional arguments added to the [Get](#Get) package that triggered this [Retrieved](#Retrieved) will also be passed along.
|
||||
Additional arguments added to the [Get](#get) package that triggered this [Retrieved](#retrieved) will also be passed
|
||||
along.
|
||||
|
||||
### SetReply
|
||||
Sent to clients in response to a [Set](#Set) package if want_reply was set to true, or if the client has registered to receive updates for a certain key using the [SetNotify](#SetNotify) package. SetReply packages are sent even if a [Set](#Set) package did not alter the value for the key.
|
||||
Sent to clients in response to a [Set](#set) package if want_reply was set to true, or if the client has registered to
|
||||
receive updates for a certain key using the [SetNotify](#setnotify) package. SetReply packages are sent even if a
|
||||
[Set](#set) package did not alter the value for the key.
|
||||
#### Arguments
|
||||
| Name | Type | Notes |
|
||||
| ---- | ---- | ----- |
|
||||
@@ -224,22 +264,23 @@ Sent to clients in response to a [Set](#Set) package if want_reply was set to tr
|
||||
| value | any | The new value for the key. |
|
||||
| original_value | any | The value the key had before it was updated. |
|
||||
|
||||
Additional arguments added to the [Set](#Set) package that triggered this [SetReply](#SetReply) will also be passed along.
|
||||
Additional arguments added to the [Set](#set) package that triggered this [SetReply](#setreply) will also be passed
|
||||
along.
|
||||
|
||||
## (Client -> Server)
|
||||
These packets are sent purely from client to server. They are not accepted by clients.
|
||||
|
||||
* [Connect](#Connect)
|
||||
* [Sync](#Sync)
|
||||
* [LocationChecks](#LocationChecks)
|
||||
* [LocationScouts](#LocationScouts)
|
||||
* [StatusUpdate](#StatusUpdate)
|
||||
* [Say](#Say)
|
||||
* [GetDataPackage](#GetDataPackage)
|
||||
* [Bounce](#Bounce)
|
||||
* [Get](#Get)
|
||||
* [Set](#Set)
|
||||
* [SetNotify](#SetNotify)
|
||||
* [Connect](#connect)
|
||||
* [Sync](#sync)
|
||||
* [LocationChecks](#locationchecks)
|
||||
* [LocationScouts](#locationscouts)
|
||||
* [StatusUpdate](#statusupdate)
|
||||
* [Say](#say)
|
||||
* [GetDataPackage](#getdatapackage)
|
||||
* [Bounce](#bounce)
|
||||
* [Get](#get)
|
||||
* [Set](#set)
|
||||
* [SetNotify](#setnotify)
|
||||
|
||||
### Connect
|
||||
Sent by the client to initiate a connection to an Archipelago game session.
|
||||
@@ -251,9 +292,9 @@ Sent by the client to initiate a connection to an Archipelago game session.
|
||||
| game | str | The name of the game the client is playing. Example: `A Link to the Past` |
|
||||
| name | str | The player name for this client. |
|
||||
| uuid | str | Unique identifier for player client. |
|
||||
| version | [NetworkVersion](#NetworkVersion) | An object representing the Archipelago version this client supports. |
|
||||
| version | [NetworkVersion](#networkversion) | An object representing the Archipelago version this client supports. |
|
||||
| items_handling | int | Flags configuring which items should be sent by the server. Read below for individual flags. |
|
||||
| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. [Tags](#Tags) |
|
||||
| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. [Tags](#tags) |
|
||||
|
||||
#### items_handling flags
|
||||
| Value | Meaning |
|
||||
@@ -265,7 +306,8 @@ Sent by the client to initiate a connection to an Archipelago game session.
|
||||
| null | Null or undefined loads settings from world definition for backwards compatibility. This is deprecated. |
|
||||
|
||||
#### Authentication
|
||||
Many, if not all, other packets require a successfully authenticated client. This is described in more detail in [Archipelago Connection Handshake](#Archipelago-Connection-Handshake).
|
||||
Many, if not all, other packets require a successfully authenticated client. This is described in more detail in
|
||||
[Archipelago Connection Handshake](#archipelago-connection-handshake).
|
||||
|
||||
### ConnectUpdate
|
||||
Update arguments from the Connect package, currently only updating tags and items_handling is supported.
|
||||
@@ -274,15 +316,16 @@ Update arguments from the Connect package, currently only updating tags and item
|
||||
| Name | Type | Notes |
|
||||
| ---- | ---- | ----- |
|
||||
| items_handling | int | Flags configuring which items should be sent by the server. |
|
||||
| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. [Tags](#Tags) |
|
||||
| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. [Tags](#tags) |
|
||||
|
||||
### Sync
|
||||
Sent to server to request a [ReceivedItems](#ReceivedItems) packet to synchronize items.
|
||||
Sent to server to request a [ReceivedItems](#receiveditems) packet to synchronize items.
|
||||
#### Arguments
|
||||
No arguments necessary.
|
||||
|
||||
### LocationChecks
|
||||
Sent to server to inform it of locations that the client has checked. Used to inform the server of new checks that are made, as well as to sync state.
|
||||
Sent to server to inform it of locations that the client has checked. Used to inform the server of new checks that are
|
||||
made, as well as to sync state.
|
||||
|
||||
#### Arguments
|
||||
| Name | Type | Notes |
|
||||
@@ -290,13 +333,15 @@ Sent to server to inform it of locations that the client has checked. Used to in
|
||||
| locations | list\[int\] | The ids of the locations checked by the client. May contain any number of checks, even ones sent before; duplicates do not cause issues with the Archipelago server. |
|
||||
|
||||
### LocationScouts
|
||||
Sent to the server to inform it of locations the client has seen, but not checked. Useful in cases in which the item may appear in the game world, such as 'ledge items' in A Link to the Past. The server will always respond with a [LocationInfo](#LocationInfo) packet with the items located in the scouted location.
|
||||
Sent to the server to inform it of locations the client has seen, but not checked. Useful in cases in which the item may
|
||||
appear in the game world, such as 'ledge items' in A Link to the Past. The server will always respond with a
|
||||
[LocationInfo](#locationinfo) packet with the items located in the scouted location.
|
||||
|
||||
#### Arguments
|
||||
| Name | Type | Notes |
|
||||
| ---- | ---- | ----- |
|
||||
| locations | list\[int\] | The ids of the locations seen by the client. May contain any number of locations, even ones sent before; duplicates do not cause issues with the Archipelago server. |
|
||||
| create_as_hint | int | If non-zero, the scouted locations get created and broadcasted as a player-visible hint. <br/>If 2 only new hints are broadcast, however this does not remove them from the LocationInfo reply. |
|
||||
| create_as_hint | int | If non-zero, the scouted locations get created and broadcast as a player-visible hint. <br/>If 2 only new hints are broadcast, however this does not remove them from the LocationInfo reply. |
|
||||
|
||||
### StatusUpdate
|
||||
Sent to the server to update on the sender's status. Examples include readiness or goal completion. (Example: defeated Ganon in A Link to the Past)
|
||||
@@ -304,7 +349,7 @@ Sent to the server to update on the sender's status. Examples include readiness
|
||||
#### Arguments
|
||||
| Name | Type | Notes |
|
||||
| ---- | ---- | ----- |
|
||||
| status | ClientStatus\[int\] | One of [Client States](#Client-States). Send as int. Follow the link for more information. |
|
||||
| status | ClientStatus\[int\] | One of [Client States](#client-states). Send as int. Follow the link for more information. |
|
||||
|
||||
### Say
|
||||
Basic chat command which sends text to the server to be distributed to other clients.
|
||||
@@ -318,8 +363,8 @@ Basic chat command which sends text to the server to be distributed to other cli
|
||||
Requests the data package from the server. Does not require client authentication.
|
||||
|
||||
#### Arguments
|
||||
| Name | Type | Notes |
|
||||
|-------| ----- |---------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Name | Type | Notes |
|
||||
|-------| ----- | ---- |
|
||||
| games | list\[str\] | Optional. If specified, will only send back the specified data. Such as, \["Factorio"\] -> Datapackage with only Factorio data. |
|
||||
|
||||
### Bounce
|
||||
@@ -335,29 +380,36 @@ the server will forward the message to all those targets to which any one requir
|
||||
| data | dict | Any data you want to send |
|
||||
|
||||
### Get
|
||||
Used to request a single or multiple values from the server's data storage, see the [Set](#Set) package for how to write values to the data storage. A Get package will be answered with a [Retrieved](#Retrieved) package.
|
||||
Used to request a single or multiple values from the server's data storage, see the [Set](#set) package for how to write
|
||||
values to the data storage. A Get package will be answered with a [Retrieved](#retrieved) package.
|
||||
#### Arguments
|
||||
| Name | Type | Notes |
|
||||
| ------ | ----- | ------ |
|
||||
| keys | list\[str\] | Keys to retrieve the values for. |
|
||||
|
||||
Additional arguments sent in this package will also be added to the [Retrieved](#Retrieved) package it triggers.
|
||||
Additional arguments sent in this package will also be added to the [Retrieved](#retrieved) package it triggers.
|
||||
|
||||
### Set
|
||||
Used to write data to the server's data storage, that data can then be shared across worlds or just saved for later. Values for keys in the data storage can be retrieved with a [Get](#Get) package, or monitored with a [SetNotify](#SetNotify) package.
|
||||
Used to write data to the server's data storage, that data can then be shared across worlds or just saved for later.
|
||||
Values for keys in the data storage can be retrieved with a [Get](#get) package, or monitored with a
|
||||
[SetNotify](#setnotify) package.
|
||||
#### Arguments
|
||||
| Name | Type | Notes |
|
||||
| ------ | ----- | ------ |
|
||||
| key | str | The key to manipulate. |
|
||||
| default | any | The default value to use in case the key has no value on the server. |
|
||||
| want_reply | bool | If set, the server will send a [SetReply](#SetReply) response back to the client. |
|
||||
| operations | list\[[DataStorageOperation](#DataStorageOperation)\] | Operations to apply to the value, multiple operations can be present and they will be executed in order of appearance. |
|
||||
| want_reply | bool | If set, the server will send a [SetReply](#setreply) response back to the client. |
|
||||
| operations | list\[[DataStorageOperation](#datastorageoperation)\] | Operations to apply to the value, multiple operations can be present and they will be executed in order of appearance. |
|
||||
|
||||
Additional arguments sent in this package will also be added to the [SetReply](#SetReply) package it triggers.
|
||||
Additional arguments sent in this package will also be added to the [SetReply](#setreply) package it triggers.
|
||||
|
||||
#### DataStorageOperation
|
||||
A DataStorageOperation manipulates or alters the value of a key in the data storage. If the operation transforms the value from one state to another then the current value of the key is used as the starting point otherwise the [Set](#Set)'s package `default` is used if the key does not exist on the server already.
|
||||
DataStorageOperations consist of an object containing both the operation to be applied, provided in the form of a string, as well as the value to be used for that operation, Example:
|
||||
A DataStorageOperation manipulates or alters the value of a key in the data storage. If the operation transforms the
|
||||
value from one state to another then the current value of the key is used as the starting point otherwise the
|
||||
[Set](#set)'s package `default` is used if the key does not exist on the server already.
|
||||
|
||||
DataStorageOperations consist of an object containing both the operation to be applied, provided in the form of a
|
||||
string, as well as the value to be used for that operation, Example:
|
||||
```json
|
||||
{"operation": "add", "value": 12}
|
||||
```
|
||||
@@ -366,7 +418,7 @@ The following operations can be applied to a datastorage key
|
||||
| Operation | Effect |
|
||||
| ------ | ----- |
|
||||
| replace | Sets the current value of the key to `value`. |
|
||||
| default | If the key has no value yet, sets the current value of the key to `default` of the [Set](#Set)'s package (`value` is ignored). |
|
||||
| default | If the key has no value yet, sets the current value of the key to `default` of the [Set](#set)'s package (`value` is ignored). |
|
||||
| add | Adds `value` to the current value of the key, if both the current value and `value` are arrays then `value` will be appended to the current value. |
|
||||
| mul | Multiplies the current value of the key by `value`. |
|
||||
| pow | Multiplies the current value of the key to the power of `value`. |
|
||||
@@ -380,28 +432,35 @@ The following operations can be applied to a datastorage key
|
||||
| right_shift | Applies a bitwise right-shift to the current value of the key by `value`. |
|
||||
|
||||
### SetNotify
|
||||
Used to register your current session for receiving all [SetReply](#SetReply) packages of certain keys to allow your client to keep track of changes.
|
||||
Used to register your current session for receiving all [SetReply](#setreply) packages of certain keys to allow your client to keep track of changes.
|
||||
#### Arguments
|
||||
| Name | Type | Notes |
|
||||
| ------ | ----- | ------ |
|
||||
| keys | list\[str\] | Keys to receive all [SetReply](#SetReply) packages for. |
|
||||
| keys | list\[str\] | Keys to receive all [SetReply](#setreply) packages for. |
|
||||
|
||||
## Appendix
|
||||
|
||||
### Coop
|
||||
Coop in Archipelago is automatically facilitated by the server, however some of the default behaviour may not be what you desire.
|
||||
Coop in Archipelago is automatically facilitated by the server, however some default behaviour may not be what you
|
||||
desire.
|
||||
|
||||
If the game in question is a remote-items game (attribute on AutoWorld), then all items will always be sent and received.
|
||||
If the game in question is not a remote-items game, then any items that are placed within the same world will not be send by the server.
|
||||
If the game in question is not a remote-items game, then any items that are placed within the same world will not be
|
||||
sent by the server.
|
||||
|
||||
To manually react to others in the same player slot doing checks, listen to [RoomUpdate](#RoomUpdate) -> checked_locations.
|
||||
To manually react to others in the same player slot doing checks, listen to [RoomUpdate](#roomupdate) ->
|
||||
checked_locations.
|
||||
|
||||
### NetworkPlayer
|
||||
A list of objects. Each object denotes one player. Each object has four fields about the player, in this order: `team`, `slot`, `alias`, and `name`. `team` and `slot` are ints, `alias` and `name` are strs.
|
||||
A list of objects. Each object denotes one player. Each object has four fields about the player, in this order: `team`,
|
||||
`slot`, `alias`, and `name`. `team` and `slot` are ints, `alias` and `name` are strings.
|
||||
|
||||
Each player belongs to a `team` and has a `slot`. Team numbers start at `0`. Slot numbers are unique per team and start at `1`. Slot number `0` refers to the Archipelago server; this may appear in instances where the server grants the player an item.
|
||||
Each player belongs to a `team` and has a `slot`. Team numbers start at `0`. Slot numbers are unique per team and start
|
||||
at `1`. Slot number `0` refers to the Archipelago server; this may appear in instances where the server grants the
|
||||
player an item.
|
||||
|
||||
`alias` represents the player's name in current time. `name` is the original name used when the session was generated. This is typically distinct in games which require baking names into ROMs or for async games.
|
||||
`alias` represents the player's name in current time. `name` is the original name used when the session was generated.
|
||||
This is typically distinct in games which require baking names into ROMs or for async games.
|
||||
|
||||
```python
|
||||
from typing import NamedTuple
|
||||
@@ -444,7 +503,8 @@ In JSON this may look like:
|
||||
|
||||
`location` is the location id of the item inside the world. Location ids are in the range of ± 2<sup>53</sup>-1.
|
||||
|
||||
`player` is the player slot of the world the item is located in, except when inside an [LocationInfo](#LocationInfo) Packet then it will be the slot of the player to receive the item
|
||||
`player` is the player slot of the world the item is located in, except when inside an [LocationInfo](#locationinfo)
|
||||
Packet then it will be the slot of the player to receive the item.
|
||||
|
||||
`flags` are bit flags:
|
||||
| Flag | Meaning |
|
||||
@@ -455,7 +515,8 @@ In JSON this may look like:
|
||||
| 0b100 | If set, indicates the item is a trap |
|
||||
|
||||
### JSONMessagePart
|
||||
Message nodes sent along with [PrintJSON](#PrintJSON) packet to be reconstructed into a legible message. The nodes are intended to be read in the order they are listed in the packet.
|
||||
Message nodes sent along with [PrintJSON](#printjson) packet to be reconstructed into a legible message.
|
||||
The nodes are intended to be read in the order they are listed in the packet.
|
||||
|
||||
```python
|
||||
from typing import TypedDict, Optional
|
||||
@@ -467,7 +528,10 @@ class JSONMessagePart(TypedDict):
|
||||
player: Optional[int] # only available if type is either item or location
|
||||
```
|
||||
|
||||
`type` is used to denote the intent of the message part. This can be used to indicate special information which may be rendered differently depending on client. How these types are displayed in Archipelago's ALttP client is not the end-all be-all. Other clients may choose to interpret and display these messages differently.
|
||||
`type` is used to denote the intent of the message part. This can be used to indicate special information which may be
|
||||
rendered differently depending on client. How these types are displayed in Archipelago's ALttP client is not the end-all
|
||||
be-all. Other clients may choose to interpret and display these messages differently.
|
||||
|
||||
Possible values for `type` include:
|
||||
|
||||
| Name | Notes |
|
||||
@@ -483,7 +547,10 @@ Possible values for `type` include:
|
||||
| color | Regular text that should be colored. Only `type` that will contain `color` data. |
|
||||
|
||||
|
||||
`color` is used to denote a console color to display the message part with and is only send if the `type` is `color`. This is limited to console colors due to backwards compatibility needs with games such as ALttP. Although background colors as well as foreground colors are listed, only one may be applied to a [JSONMessagePart](#JSONMessagePart) at a time.
|
||||
`color` is used to denote a console color to display the message part with and is only send if the `type` is `color`.
|
||||
This is limited to console colors due to backwards compatibility needs with games such as ALttP.
|
||||
Although background colors as well as foreground colors are listed, only one may be applied to a
|
||||
[JSONMessagePart](#jsonmessagepart) at a time.
|
||||
|
||||
Color options:
|
||||
* bold
|
||||
@@ -507,10 +574,11 @@ Color options:
|
||||
|
||||
`text` is the content of the message part to be displayed.
|
||||
`player` marks owning player id for location/item,
|
||||
`flags` contains the [NetworkItem](#NetworkItem) flags that belong to the item
|
||||
`flags` contains the [NetworkItem](#networkitem) flags that belong to the item
|
||||
|
||||
### Client States
|
||||
An enumeration containing the possible client states that may be used to inform the server in [StatusUpdate](#StatusUpdate).
|
||||
An enumeration containing the possible client states that may be used to inform the server in
|
||||
[StatusUpdate](#statusupdate).
|
||||
|
||||
```python
|
||||
import enum
|
||||
@@ -522,7 +590,8 @@ class ClientStatus(enum.IntEnum):
|
||||
```
|
||||
|
||||
### NetworkVersion
|
||||
An object representing software versioning. Used in the [Connect](#Connect) packet to allow the client to inform the server of the Archipelago version it supports.
|
||||
An object representing software versioning. Used in the [Connect](#connect) packet to allow the client to inform the
|
||||
server of the Archipelago version it supports.
|
||||
```python
|
||||
from typing import NamedTuple
|
||||
class Version(NamedTuple):
|
||||
@@ -568,9 +637,14 @@ class Permission(enum.IntEnum):
|
||||
```
|
||||
|
||||
### Data Package Contents
|
||||
A data package is a JSON object which may contain arbitrary metadata to enable a client to interact with the Archipelago server most easily. Currently, this package is used to send ID to name mappings so that clients need not maintain their own mappings.
|
||||
A data package is a JSON object which may contain arbitrary metadata to enable a client to interact with the Archipelago
|
||||
server most easily. Currently, this package is used to send ID to name mappings so that clients need not maintain their
|
||||
own mappings.
|
||||
|
||||
We encourage clients to cache the data package they receive on disk, or otherwise not tied to a session. You will know when your cache is outdated if the [RoomInfo](#RoomInfo) packet or the datapackage itself denote a different version. A special case is datapackage version 0, where it is expected the package is custom and should not be cached.
|
||||
We encourage clients to cache the data package they receive on disk, or otherwise not tied to a session.
|
||||
You will know when your cache is outdated if the [RoomInfo](#roominfo) packet or the datapackage itself denote a
|
||||
different version. A special case is datapackage version 0, where it is expected the package is custom and should not be
|
||||
cached.
|
||||
|
||||
Note:
|
||||
* Any ID is unique to its type across AP: Item 56 only exists once and Location 56 only exists once.
|
||||
@@ -598,7 +672,7 @@ Tags are represented as a list of strings, the common Client tags follow:
|
||||
| Name | Notes |
|
||||
|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| AP | Signifies that this client is a reference client, its usefulness is mostly in debugging to compare client behaviours more easily. |
|
||||
| IgnoreGame | Deprecated. See Tracker and TextOnly. Tells the server to ignore the "game" attribute in the [Connect](#Connect) packet. |
|
||||
| IgnoreGame | Deprecated. See Tracker and TextOnly. Tells the server to ignore the "game" attribute in the [Connect](#connect) packet. |
|
||||
| DeathLink | Client participates in the DeathLink mechanic, therefore will send and receive DeathLink bounce packets |
|
||||
| Tracker | Tells the server that this client will not send locations and is actually a Tracker. When specified and used with empty or null `game` in [Connect](#connect), game and game's version validation will be skipped. |
|
||||
| TextOnly | Tells the server that this client will not send locations and is intended for chat. When specified and used with empty or null `game` in [Connect](#connect), game and game's version validation will be skipped. |
|
||||
@@ -6,26 +6,20 @@ required to send and receive items between the game and server.
|
||||
|
||||
Client implementation is out of scope of this document. Please refer to an
|
||||
existing game that provides a similar API to yours.
|
||||
Refer to the following documents as well:
|
||||
- [network protocol.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/network%20protocol.md)
|
||||
- [adding games.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/adding%20games.md)
|
||||
|
||||
Archipelago will be abbreviated as "AP" from now on.
|
||||
|
||||
|
||||
## Language
|
||||
|
||||
AP worlds are written in python3.
|
||||
Clients that connect to the server to sync items can be in any language that
|
||||
allows using WebSockets.
|
||||
|
||||
|
||||
## Coding style
|
||||
|
||||
AP follows all the PEPs. When in doubt use an IDE with coding style
|
||||
linter, for example PyCharm Community Edition.
|
||||
|
||||
|
||||
## Docstrings
|
||||
|
||||
Docstrings are strings attached to an object in Python that describe what the
|
||||
@@ -40,7 +34,6 @@ class MyGameWorld(World):
|
||||
website."""
|
||||
```
|
||||
|
||||
|
||||
## Definitions
|
||||
|
||||
This section will cover various classes and objects you can use for your world.
|
||||
@@ -63,7 +56,7 @@ for your world specifically on the webhost.
|
||||
`theme` to be used for your game specific AP pages. Available themes:
|
||||
| dirt | grass (default) | grassFlowers | ice | jungle | ocean | partyTime | stone |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| <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"> | <img src="img/theme_stone.JPG" width="100"> |
|
||||
| <img src="_static/theme_dirt.JPG" width="200"> | <img src="_static/theme_grass.JPG" width="200"> | <img src="_static/theme_grassFlowers.JPG" width="200"> | <img src="_static/theme_ice.JPG" width="200"> | <img src="_static/theme_jungle.JPG" width="200"> | <img src="_static/theme_ocean.JPG" width="200"> | <img src="_static/theme_partyTime.JPG" width="200"> | <img src="_static/theme_stone.JPG" width="200"> |
|
||||
|
||||
`bug_report_page` (optional) can be a link to a bug reporting page, most likely a GitHub issue page, that will be placed by the site to help direct users to report bugs.
|
||||
|
||||
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 209 KiB After Width: | Height: | Size: 209 KiB |
|
Before Width: | Height: | Size: 216 KiB After Width: | Height: | Size: 216 KiB |
|
Before Width: | Height: | Size: 214 KiB After Width: | Height: | Size: 214 KiB |
|
Before Width: | Height: | Size: 214 KiB After Width: | Height: | Size: 214 KiB |
|
Before Width: | Height: | Size: 257 KiB After Width: | Height: | Size: 257 KiB |
|
Before Width: | Height: | Size: 221 KiB After Width: | Height: | Size: 221 KiB |
|
Before Width: | Height: | Size: 245 KiB After Width: | Height: | Size: 245 KiB |
|
Before Width: | Height: | Size: 193 KiB After Width: | Height: | Size: 193 KiB |
56
docs/sphinx/conf.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
# This file only contains a selection of the most common options. For a full
|
||||
# list see the documentation:
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||
|
||||
# -- Path setup --------------------------------------------------------------
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
import os
|
||||
import sys
|
||||
sys.path.insert(0, os.path.abspath('../..'))
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = 'Archipelago'
|
||||
copyright = '2022, Archipelago Team and Contributors'
|
||||
author = 'Archipelago Team and Contributors'
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
"myst_parser",
|
||||
"sphinxcontrib.mermaid",
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.autosummary",
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This pattern also affects html_static_path and html_extra_path.
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
|
||||
myst_heading_anchors = 4
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
html_theme = 'alabaster'
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
70
docs/sphinx/index.md
Normal file
@@ -0,0 +1,70 @@
|
||||
% Archipelago documentation master file, created by
|
||||
% sphinx-quickstart on Wed Jul 6 20:09:51 2022.
|
||||
% You can adapt this file completely to your liking, but it should at least
|
||||
% contain the root `toctree` directive.
|
||||
|
||||
Welcome to Archipelago's Technical Documentation!
|
||||
=================================================
|
||||
|
||||
## What is Archipelago?
|
||||
Archipelago provides a generic framework for developing multiworld capability for game randomizers.
|
||||
In all cases, presently, Archipelago is also the randomizer itself.
|
||||
|
||||
Archipelago is end-user facing software intended to facilitate randomizer and multiworld play for a variety of
|
||||
supported games.
|
||||
|
||||
Archipelago presently supports the following games:
|
||||
* The Legend of Zelda: A Link to the Past
|
||||
* Factorio
|
||||
* Minecraft
|
||||
* Subnautica
|
||||
* Slay the Spire
|
||||
* Risk of Rain 2
|
||||
* The Legend of Zelda: Ocarina of Time
|
||||
* Timespinner
|
||||
* Super Metroid
|
||||
* Secret of Evermore
|
||||
* Final Fantasy
|
||||
* Rogue Legacy
|
||||
* VVVVVV
|
||||
* Raft
|
||||
* Super Mario 64
|
||||
* Meritous
|
||||
* Super Metroid/Link to the Past combo randomizer (SMZ3)
|
||||
* ChecksFinder
|
||||
* ArchipIDLE
|
||||
* Hollow Knight
|
||||
* The Witness
|
||||
* Sonic Adventure 2: Battle
|
||||
* Starcraft 2: Wings of Liberty
|
||||
* Donkey Kong Country 3
|
||||
* Dark Souls 3
|
||||
|
||||
For more information on the technical architecture of Archipelago,
|
||||
please refer to the [Archipelago Technical Architecture](Architecture.md) document.
|
||||
|
||||
## Contributing to Archipelago
|
||||
Contributions to the Archipelago code are welcome and dearly appreciated. Contributions may occur as changes to website
|
||||
content, changes to Archipelago core code, additions of game integrations, or alterations to website functionality.
|
||||
|
||||
Please visit our [contributing guidelines on our GitHub README](https://github.com/ArchipelagoMW/Archipelago#contributing)
|
||||
for some more information on what may be expected.
|
||||
|
||||
For information on contributing a game integration, check out our [document on adding games to Archipelago](./AddingGames.md).
|
||||
|
||||
## Table of Contents
|
||||
```{toctree}
|
||||
---
|
||||
maxdepth: 2
|
||||
caption: "Documentation contents:"
|
||||
---
|
||||
AddingGames
|
||||
WorldAPI
|
||||
NetworkProtocol
|
||||
NetworkDiagram
|
||||
```
|
||||
|
||||
## Indices and tables
|
||||
* {ref}`genindex`
|
||||
* {ref}`modindex`
|
||||
* {ref}`search`
|
||||
35
docs/sphinx/make.bat
Normal file
@@ -0,0 +1,35 @@
|
||||
@ECHO OFF
|
||||
|
||||
pushd %~dp0
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set SOURCEDIR=.
|
||||
set BUILDDIR=_build
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
%SPHINXBUILD% >NUL 2>NUL
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.https://www.sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||
goto end
|
||||
|
||||
:help
|
||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||
|
||||
:end
|
||||
popd
|
||||
4
docs/sphinx/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
sphinx==5.0.2
|
||||
sphinx_rtd_theme==1.0.0
|
||||
readthedocs-sphinx-search==0.1.2
|
||||
myst-parser==0.18
|
||||
@@ -3,6 +3,6 @@ websockets>=10.3
|
||||
PyYAML>=6.0
|
||||
jellyfish>=0.9.0
|
||||
jinja2>=3.1.2
|
||||
schema>=0.7.4
|
||||
schema>=0.7.5
|
||||
kivy>=2.1.0
|
||||
bsdiff4>=1.2.2
|
||||
@@ -52,3 +52,13 @@ class TestIDs(unittest.TestCase):
|
||||
else:
|
||||
for location_id in world_type.location_id_to_name:
|
||||
self.assertGreater(location_id, 0)
|
||||
|
||||
def testDuplicateItemIDs(self):
|
||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest(game=gamename):
|
||||
self.assertEqual(len(world_type.item_id_to_name), len(world_type.item_name_to_id))
|
||||
|
||||
def testDuplicateLocationIDs(self):
|
||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest(game=gamename):
|
||||
self.assertEqual(len(world_type.location_id_to_name), len(world_type.location_name_to_id))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import unittest
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
from . import setup_default_world
|
||||
|
||||
|
||||
class TestBase(unittest.TestCase):
|
||||
@@ -29,3 +30,17 @@ class TestBase(unittest.TestCase):
|
||||
with self.subTest(group_name, group_name=group_name):
|
||||
for item in items:
|
||||
self.assertIn(item, world_type.item_name_to_id)
|
||||
|
||||
def testItemCountGreaterEqualLocations(self):
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
|
||||
if game_name in {"Final Fantasy"}:
|
||||
continue
|
||||
with self.subTest("Game", game=game_name):
|
||||
world = setup_default_world(world_type)
|
||||
location_count = sum(0 if location.event or location.item else 1 for location in world.get_locations())
|
||||
self.assertGreaterEqual(
|
||||
len(world.itempool),
|
||||
location_count,
|
||||
f"{game_name} Item count MUST meet or exceede the number of locations",
|
||||
)
|
||||
|
||||
23
test/webhost/TestFileGeneration.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""Tests for successful generation of WebHost cached files. Can catch some other deeper errors."""
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
import WebHost
|
||||
|
||||
|
||||
class TestFileGeneration(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.correct_path = os.path.join(os.path.dirname(WebHost.__file__), "WebHostLib")
|
||||
# should not create the folder *here*
|
||||
self.incorrect_path = os.path.join(os.path.split(os.path.dirname(__file__))[0], "WebHostLib")
|
||||
|
||||
def testOptions(self):
|
||||
WebHost.create_options_files()
|
||||
self.assertTrue(os.path.exists(os.path.join(self.correct_path, "static", "generated", "configs")))
|
||||
self.assertFalse(os.path.exists(os.path.join(self.incorrect_path, "static", "generated", "configs")))
|
||||
|
||||
def testTutorial(self):
|
||||
WebHost.create_ordered_tutorials_file()
|
||||
self.assertTrue(os.path.exists(os.path.join(self.correct_path, "static", "generated", "tutorials.json")))
|
||||
self.assertFalse(os.path.exists(os.path.join(self.incorrect_path, "static", "generated", "tutorials.json")))
|
||||
@@ -27,7 +27,8 @@ class AutoWorldRegister(type):
|
||||
|
||||
# build rest
|
||||
dct["item_names"] = frozenset(dct["item_name_to_id"])
|
||||
dct["item_name_groups"] = dct.get("item_name_groups", {})
|
||||
dct["item_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set
|
||||
in dct.get("item_name_groups", {}).items()}
|
||||
dct["item_name_groups"]["Everything"] = dct["item_names"]
|
||||
dct["location_names"] = frozenset(dct["location_name_to_id"])
|
||||
dct["all_item_and_group_names"] = frozenset(dct["item_names"] | set(dct.get("item_name_groups", {})))
|
||||
@@ -97,89 +98,109 @@ def call_stage(world: "MultiWorld", method_name: str, *args: Any) -> None:
|
||||
|
||||
class WebWorld:
|
||||
"""Webhost integration"""
|
||||
# display a settings page. Can be a link to an out-of-ap settings tool too.
|
||||
|
||||
settings_page: Union[bool, str] = True
|
||||
|
||||
# docs folder will be scanned for game info pages using this list in the format '{language}_{game_name}.md'
|
||||
"""display a settings page. Can be a link to a specific page or external tool."""
|
||||
|
||||
game_info_languages: List[str] = ['en']
|
||||
"""docs folder will be scanned for game info pages using this list in the format '{language}_{game_name}.md'"""
|
||||
|
||||
# docs folder will also be scanned for tutorial guides given the relevant information in this list. Each Tutorial
|
||||
# class is to be used for one guide.
|
||||
tutorials: List["Tutorial"]
|
||||
"""docs folder will also be scanned for tutorial guides. Each Tutorial class is to be used for one guide."""
|
||||
|
||||
# Choose a theme for your /game/* pages
|
||||
# Available: dirt, grass, grassFlowers, ice, jungle, ocean, partyTime, stone
|
||||
theme = "grass"
|
||||
"""Choose a theme for you /game/* pages.
|
||||
Available: dirt, grass, grassFlowers, ice, jungle, ocean, partyTime, stone"""
|
||||
|
||||
# display a link to a bug report page, most likely a link to a GitHub issue page.
|
||||
bug_report_page: Optional[str]
|
||||
"""display a link to a bug report page, most likely a link to a GitHub issue page."""
|
||||
|
||||
|
||||
class World(metaclass=AutoWorldRegister):
|
||||
"""A World object encompasses a game's Items, Locations, Rules and additional data or functionality required.
|
||||
A Game should have its own subclass of World in which it defines the required data structures."""
|
||||
|
||||
option_definitions: Dict[str, Option[Any]] = {} # link your Options mapping
|
||||
game: str # name the game
|
||||
topology_present: bool = False # indicate if world type has any meaningful layout/pathing
|
||||
option_definitions: Dict[str, Option[Any]] = {}
|
||||
""" link your Options mapping """
|
||||
|
||||
game: str
|
||||
""" name of the game the world is for """
|
||||
|
||||
topology_present: bool = False
|
||||
""" indicate if world type has any meaningful layout/pathing """
|
||||
|
||||
# gets automatically populated with all item and item group names
|
||||
all_item_and_group_names: FrozenSet[str] = frozenset()
|
||||
""" gets automatically populated with all item and item group names """
|
||||
|
||||
# map names to their IDs
|
||||
item_name_to_id: Dict[str, int] = {}
|
||||
""" map item names to their IDs """
|
||||
|
||||
location_name_to_id: Dict[str, int] = {}
|
||||
""" map location names to their IDs """
|
||||
|
||||
# maps item group names to sets of items. Example: "Weapons" -> {"Sword", "Bow"}
|
||||
item_name_groups: Dict[str, Set[str]] = {}
|
||||
""" maps item group names to sets of items. Example: "Weapons" -> {"Sword", "Bow"} """
|
||||
|
||||
# increment this every time something in your world's names/id mappings changes.
|
||||
# While this is set to 0 in *any* AutoWorld, the entire DataPackage is considered in testing mode and will be
|
||||
# retrieved by clients on every connection.
|
||||
data_version: int = 1
|
||||
"""increment this every time something in your world's names/id mappings changes.
|
||||
While this is set to 0 in *any* AutoWorld, the entire DataPackage is considered in testing mode and will be
|
||||
retrieved by clients on every connection.
|
||||
"""
|
||||
|
||||
# override this if changes to a world break forward-compatibility of the client
|
||||
# The base version of (0, 1, 6) is provided for backwards compatibility and does *not* need to be updated in the
|
||||
# future. Protocol level compatibility check moved to MultiServer.min_client_version.
|
||||
required_client_version: Tuple[int, int, int] = (0, 1, 6)
|
||||
""" override this if changes to a world break forward-compatibility of the client
|
||||
The base version of (0, 1, 6) is provided for backwards compatibility and does *not* need to be updated in the
|
||||
future. Protocol level compatibility check moved to MultiServer.min_client_version.
|
||||
"""
|
||||
|
||||
# update this if the resulting multidata breaks forward-compatibility of the server
|
||||
required_server_version: Tuple[int, int, int] = (0, 2, 4)
|
||||
""" update this if the resulting multidata breaks forward-compatibility of the server """
|
||||
|
||||
hint_blacklist: FrozenSet[str] = frozenset() # any names that should not be hintable
|
||||
hint_blacklist: FrozenSet[str] = frozenset()
|
||||
""" any names that should not be hintable """
|
||||
|
||||
# NOTE: remote_items and remote_start_inventory are now available in the network protocol for the client to set.
|
||||
# These values will be removed.
|
||||
# if a world is set to remote_items, then it just needs to send location checks to the server and the server
|
||||
# sends back the items
|
||||
# if a world is set to remote_items = False, then the server never sends an item where receiver == finder,
|
||||
# the client finds its own items in its own world.
|
||||
remote_items: bool = True
|
||||
""" NOTE: remote_items and remote_start_inventory are now available in the network protocol for the client to set.
|
||||
These values will be removed.
|
||||
if a world is set to remote_items, then it just needs to send location checks to the server and the server
|
||||
sends back the items
|
||||
if a world is set to remote_items = False, then the server never sends an item where receiver == finder,
|
||||
the client finds its own items in its own world.
|
||||
"""
|
||||
|
||||
# If remote_start_inventory is true, the start_inventory/world.precollected_items is sent on connection,
|
||||
# otherwise the world implementation is in charge of writing the items to their output data.
|
||||
remote_start_inventory: bool = True
|
||||
""" If remote_start_inventory is true, the start_inventory/world.precollected_items is sent on connection,
|
||||
otherwise the world implementation is in charge of writing the items to their output data.
|
||||
"""
|
||||
|
||||
# For games where after a victory it is impossible to go back in and get additional/remaining Locations checked.
|
||||
# this forces forfeit: auto for those games.
|
||||
forced_auto_forfeit: bool = False
|
||||
""" For games where after a victory it is impossible to go back in and get additional/remaining Locations checked.
|
||||
this forces forfeit: auto for those games.
|
||||
"""
|
||||
|
||||
# Hide World Type from various views. Does not remove functionality.
|
||||
hidden: bool = False
|
||||
""" Hide World Type from various views. Does not remove functionality. """
|
||||
|
||||
# see WebWorld for options
|
||||
web: WebWorld = WebWorld()
|
||||
|
||||
# autoset on creation:
|
||||
world: "MultiWorld"
|
||||
""" autoset on creation """
|
||||
|
||||
player: int
|
||||
""" autoset on creation """
|
||||
|
||||
# automatically generated
|
||||
item_id_to_name: Dict[int, str]
|
||||
location_id_to_name: Dict[int, str]
|
||||
""" automatically generated inverse of item_name_to_id """
|
||||
|
||||
item_names: Set[str] # set of all potential item names
|
||||
location_names: Set[str] # set of all potential location names
|
||||
location_id_to_name: Dict[int, str]
|
||||
""" automatically generated inverse of location_name_to_id """
|
||||
|
||||
item_names: Set[str]
|
||||
""" set of all potential item names """
|
||||
|
||||
location_names: Set[str]
|
||||
""" set of all potential location names """
|
||||
|
||||
zip_path: Optional[pathlib.Path] = None # If loaded from a .apworld, this is the Path to it.
|
||||
__file__: str # path it was loaded from
|
||||
@@ -283,8 +304,8 @@ class World(metaclass=AutoWorldRegister):
|
||||
return item.name
|
||||
return None
|
||||
|
||||
# called to create all_state, return Items that are created during pre_fill
|
||||
def get_pre_fill_items(self) -> List["Item"]:
|
||||
""" called to create all_state, return Items that are created during pre_fill """
|
||||
return []
|
||||
|
||||
# following methods should not need to be overridden.
|
||||
|
||||
@@ -5,14 +5,15 @@ import typing
|
||||
|
||||
folder = os.path.dirname(__file__)
|
||||
|
||||
__all__ = {
|
||||
__all__ = [
|
||||
"lookup_any_item_id_to_name",
|
||||
"lookup_any_location_id_to_name",
|
||||
"network_data_package",
|
||||
"AutoWorldRegister",
|
||||
"world_sources",
|
||||
"folder",
|
||||
}
|
||||
"World"
|
||||
]
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from .AutoWorld import World
|
||||
|
||||
@@ -212,9 +212,7 @@ def parse_arguments(argv, no_defaults=False):
|
||||
Alternatively, can be a ALttP Rom patched with a Link
|
||||
sprite that will be extracted.
|
||||
''')
|
||||
parser.add_argument('--gui', help='Launch the GUI', action='store_true')
|
||||
|
||||
parser.add_argument('--enemizercli', default=defval('EnemizerCLI/EnemizerCLI.Core'))
|
||||
parser.add_argument('--shufflebosses', default=defval('none'), choices=['none', 'basic', 'normal', 'chaos',
|
||||
"singularity"])
|
||||
|
||||
|
||||
@@ -282,8 +282,8 @@ class ShieldPalette(Palette):
|
||||
display_name = "Shield Palette"
|
||||
|
||||
|
||||
class LinkPalette(Palette):
|
||||
display_name = "Link Palette"
|
||||
# class LinkPalette(Palette):
|
||||
# display_name = "Link Palette"
|
||||
|
||||
|
||||
class HeartBeep(Choice):
|
||||
@@ -387,7 +387,7 @@ alttp_options: typing.Dict[str, type(Option)] = {
|
||||
"hud_palettes": HUDPalette,
|
||||
"sword_palettes": SwordPalette,
|
||||
"shield_palettes": ShieldPalette,
|
||||
"link_palettes": LinkPalette,
|
||||
# "link_palettes": LinkPalette,
|
||||
"heartbeep": HeartBeep,
|
||||
"heartcolor": HeartColor,
|
||||
"quickswap": QuickSwap,
|
||||
|
||||
@@ -34,7 +34,7 @@ from worlds.alttp.Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts
|
||||
DeathMountain_texts, \
|
||||
LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, \
|
||||
SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names
|
||||
from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen
|
||||
from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen, parse_yaml
|
||||
from worlds.alttp.Items import ItemFactory, item_table, item_name_groups, progression_items
|
||||
from worlds.alttp.EntranceShuffle import door_addresses
|
||||
from worlds.alttp.Options import smallkey_shuffle
|
||||
@@ -551,18 +551,22 @@ class Sprite():
|
||||
Sprite.base_data = Sprite.sprite + Sprite.palette + Sprite.glove_palette
|
||||
|
||||
def from_ap_sprite(self, filedata):
|
||||
filedata = filedata.decode("utf-8-sig")
|
||||
import yaml
|
||||
obj = yaml.safe_load(filedata)
|
||||
if obj["min_format_version"] > 1:
|
||||
raise Exception("Sprite file requires an updated reader.")
|
||||
self.author_name = obj["author"]
|
||||
self.name = obj["name"]
|
||||
if obj["data"]: # skip patching for vanilla content
|
||||
data = bsdiff4.patch(Sprite.base_data, obj["data"])
|
||||
self.sprite = data[:self.sprite_size]
|
||||
self.palette = data[self.sprite_size:self.palette_size]
|
||||
self.glove_palette = data[self.sprite_size + self.palette_size:]
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
obj = parse_yaml(filedata.decode("utf-8-sig"))
|
||||
if obj["min_format_version"] > 1:
|
||||
raise Exception("Sprite file requires an updated reader.")
|
||||
self.author_name = obj["author"]
|
||||
self.name = obj["name"]
|
||||
if obj["data"]: # skip patching for vanilla content
|
||||
data = bsdiff4.patch(Sprite.base_data, obj["data"])
|
||||
self.sprite = data[:self.sprite_size]
|
||||
self.palette = data[self.sprite_size:self.palette_size]
|
||||
self.glove_palette = data[self.sprite_size + self.palette_size:]
|
||||
except Exception:
|
||||
logger = logging.getLogger("apsprite")
|
||||
logger.exception("Error parsing apsprite file")
|
||||
self.valid = False
|
||||
|
||||
@property
|
||||
def author_game_display(self) -> str:
|
||||
@@ -659,7 +663,7 @@ class Sprite():
|
||||
|
||||
@staticmethod
|
||||
def parse_zspr(filedata, expected_kind):
|
||||
logger = logging.getLogger('ZSPR')
|
||||
logger = logging.getLogger("ZSPR")
|
||||
headerstr = "<4xBHHIHIHH6x"
|
||||
headersize = struct.calcsize(headerstr)
|
||||
if len(filedata) < headersize:
|
||||
@@ -667,7 +671,7 @@ class Sprite():
|
||||
version, csum, icsum, sprite_offset, sprite_size, palette_offset, palette_size, kind = struct.unpack_from(
|
||||
headerstr, filedata)
|
||||
if version not in [1]:
|
||||
logger.error('Error parsing ZSPR file: Version %g not supported', version)
|
||||
logger.error("Error parsing ZSPR file: Version %g not supported", version)
|
||||
return None
|
||||
if kind != expected_kind:
|
||||
return None
|
||||
@@ -676,36 +680,42 @@ class Sprite():
|
||||
stream.seek(headersize)
|
||||
|
||||
def read_utf16le(stream):
|
||||
"Decodes a null-terminated UTF-16_LE string of unknown size from a stream"
|
||||
"""Decodes a null-terminated UTF-16_LE string of unknown size from a stream"""
|
||||
raw = bytearray()
|
||||
while True:
|
||||
char = stream.read(2)
|
||||
if char in [b'', b'\x00\x00']:
|
||||
if char in [b"", b"\x00\x00"]:
|
||||
break
|
||||
raw += char
|
||||
return raw.decode('utf-16_le')
|
||||
return raw.decode("utf-16_le")
|
||||
|
||||
sprite_name = read_utf16le(stream)
|
||||
author_name = read_utf16le(stream)
|
||||
author_credits_name = stream.read().split(b"\x00", 1)[0].decode()
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
sprite_name = read_utf16le(stream)
|
||||
author_name = read_utf16le(stream)
|
||||
author_credits_name = stream.read().split(b"\x00", 1)[0].decode()
|
||||
|
||||
# Ignoring the Author Rom name for the time being.
|
||||
# Ignoring the Author Rom name for the time being.
|
||||
|
||||
real_csum = sum(filedata) % 0x10000
|
||||
if real_csum != csum or real_csum ^ 0xFFFF != icsum:
|
||||
logger.warning('ZSPR file has incorrect checksum. It may be corrupted.')
|
||||
real_csum = sum(filedata) % 0x10000
|
||||
if real_csum != csum or real_csum ^ 0xFFFF != icsum:
|
||||
logger.warning("ZSPR file has incorrect checksum. It may be corrupted.")
|
||||
|
||||
sprite = filedata[sprite_offset:sprite_offset + sprite_size]
|
||||
palette = filedata[palette_offset:palette_offset + palette_size]
|
||||
sprite = filedata[sprite_offset:sprite_offset + sprite_size]
|
||||
palette = filedata[palette_offset:palette_offset + palette_size]
|
||||
|
||||
if len(sprite) != sprite_size or len(palette) != palette_size:
|
||||
logger.error('Error parsing ZSPR file: Unexpected end of file')
|
||||
if len(sprite) != sprite_size or len(palette) != palette_size:
|
||||
logger.error("Error parsing ZSPR file: Unexpected end of file")
|
||||
return None
|
||||
|
||||
return sprite, palette, sprite_name, author_name, author_credits_name
|
||||
|
||||
except Exception:
|
||||
logger.exception("Error parsing ZSPR file")
|
||||
return None
|
||||
|
||||
return (sprite, palette, sprite_name, author_name, author_credits_name)
|
||||
|
||||
def decode_palette(self):
|
||||
"Returns the palettes as an array of arrays of 15 colors"
|
||||
"""Returns the palettes as an array of arrays of 15 colors"""
|
||||
|
||||
def array_chunk(arr, size):
|
||||
return list(zip(*[iter(arr)] * size))
|
||||
|
||||
@@ -4,6 +4,7 @@ import random
|
||||
import threading
|
||||
import typing
|
||||
|
||||
import Utils
|
||||
from BaseClasses import Item, CollectionState, Tutorial
|
||||
from .Dungeons import create_dungeons
|
||||
from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect
|
||||
@@ -136,6 +137,10 @@ class ALTTPWorld(World):
|
||||
|
||||
create_items = generate_itempool
|
||||
|
||||
enemizer_path: str = Utils.get_options()["generator"]["enemizer_path"] \
|
||||
if os.path.isabs(Utils.get_options()["generator"]["enemizer_path"]) \
|
||||
else Utils.local_path(Utils.get_options()["generator"]["enemizer_path"])
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.dungeon_local_item_names = set()
|
||||
self.dungeon_specific_item_names = set()
|
||||
@@ -150,12 +155,12 @@ class ALTTPWorld(World):
|
||||
raise FileNotFoundError(rom_file)
|
||||
|
||||
def generate_early(self):
|
||||
if self.use_enemizer():
|
||||
check_enemizer(self.enemizer_path)
|
||||
|
||||
player = self.player
|
||||
world = self.world
|
||||
|
||||
if self.use_enemizer():
|
||||
check_enemizer(world.enemizer)
|
||||
|
||||
# system for sharing ER layouts
|
||||
self.er_seed = str(world.random.randint(0, 2 ** 64))
|
||||
|
||||
@@ -360,7 +365,7 @@ class ALTTPWorld(World):
|
||||
patch_rom(world, rom, player, use_enemizer)
|
||||
|
||||
if use_enemizer:
|
||||
patch_enemizer(world, player, rom, world.enemizer, output_directory)
|
||||
patch_enemizer(world, player, rom, self.enemizer_path, output_directory)
|
||||
|
||||
if world.is_race:
|
||||
patch_race_rom(rom, world, player)
|
||||
@@ -373,7 +378,7 @@ class ALTTPWorld(World):
|
||||
'hud': world.hud_palettes[player],
|
||||
'sword': world.sword_palettes[player],
|
||||
'shield': world.shield_palettes[player],
|
||||
'link': world.link_palettes[player]
|
||||
# 'link': world.link_palettes[player]
|
||||
}
|
||||
palettes_options = {key: option.current_key for key, option in palettes_options.items()}
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ async def dkc3_game_watcher(ctx: Context):
|
||||
return
|
||||
|
||||
new_checks = []
|
||||
from worlds.dkc3.Rom import location_rom_data, item_rom_data
|
||||
from worlds.dkc3.Rom import location_rom_data, item_rom_data, boss_location_ids, level_unlock_map
|
||||
for loc_id, loc_data in location_rom_data.items():
|
||||
if loc_id not in ctx.locations_checked:
|
||||
data = await snes_read(ctx, WRAM_START + loc_data[0], 1)
|
||||
@@ -186,22 +186,40 @@ async def dkc3_game_watcher(ctx: Context):
|
||||
|
||||
# DKC3_TODO: This method of collect should work, however it does not unlock the next level correctly when previous is flagged
|
||||
# Handle Collected Locations
|
||||
#for loc_id in ctx.checked_locations:
|
||||
# if loc_id not in ctx.locations_checked:
|
||||
# loc_data = location_rom_data[loc_id]
|
||||
# data = await snes_read(ctx, WRAM_START + loc_data[0], 1)
|
||||
# invert_bit = ((len(loc_data) >= 3) and loc_data[2])
|
||||
# if not invert_bit:
|
||||
# masked_data = data[0] | (1 << loc_data[1])
|
||||
# print("Collected Location: ", hex(loc_data[0]), " | ", loc_data[1])
|
||||
# snes_buffered_write(ctx, WRAM_START + loc_data[0], bytes([masked_data]))
|
||||
# await snes_flush_writes(ctx)
|
||||
# else:
|
||||
# masked_data = data[0] & ~(1 << loc_data[1])
|
||||
# print("Collected Inverted Location: ", hex(loc_data[0]), " | ", loc_data[1])
|
||||
# snes_buffered_write(ctx, WRAM_START + loc_data[0], bytes([masked_data]))
|
||||
# await snes_flush_writes(ctx)
|
||||
# ctx.locations_checked.add(loc_id)
|
||||
for loc_id in ctx.checked_locations:
|
||||
if loc_id not in ctx.locations_checked and loc_id not in boss_location_ids:
|
||||
loc_data = location_rom_data[loc_id]
|
||||
data = await snes_read(ctx, WRAM_START + loc_data[0], 1)
|
||||
invert_bit = ((len(loc_data) >= 3) and loc_data[2])
|
||||
if not invert_bit:
|
||||
masked_data = data[0] | (1 << loc_data[1])
|
||||
#print("Collected Location: ", hex(loc_data[0]), " | ", loc_data[1])
|
||||
snes_buffered_write(ctx, WRAM_START + loc_data[0], bytes([masked_data]))
|
||||
|
||||
if (loc_data[1] == 1):
|
||||
# Make the next levels accessible
|
||||
level_id = loc_data[0] - 0x632
|
||||
levels_to_tiles = await snes_read(ctx, ROM_START + 0x3FF800, 0x60)
|
||||
tiles_to_levels = await snes_read(ctx, ROM_START + 0x3FF860, 0x60)
|
||||
tile_id = levels_to_tiles[level_id] if levels_to_tiles[level_id] != 0xFF else level_id
|
||||
tile_id = tile_id + 0x632
|
||||
#print("Tile ID: ", hex(tile_id))
|
||||
if tile_id in level_unlock_map:
|
||||
for next_level_address in level_unlock_map[tile_id]:
|
||||
next_level_id = next_level_address - 0x632
|
||||
next_tile_id = tiles_to_levels[next_level_id] if tiles_to_levels[next_level_id] != 0xFF else next_level_id
|
||||
next_tile_id = next_tile_id + 0x632
|
||||
#print("Next Level ID: ", hex(next_tile_id))
|
||||
next_data = await snes_read(ctx, WRAM_START + next_tile_id, 1)
|
||||
snes_buffered_write(ctx, WRAM_START + next_tile_id, bytes([next_data[0] | 0x01]))
|
||||
|
||||
await snes_flush_writes(ctx)
|
||||
else:
|
||||
masked_data = data[0] & ~(1 << loc_data[1])
|
||||
print("Collected Inverted Location: ", hex(loc_data[0]), " | ", loc_data[1])
|
||||
snes_buffered_write(ctx, WRAM_START + loc_data[0], bytes([masked_data]))
|
||||
await snes_flush_writes(ctx)
|
||||
ctx.locations_checked.add(loc_id)
|
||||
|
||||
# Calculate Boomer Cost Text
|
||||
boomer_cost_text = await snes_read(ctx, WRAM_START + 0xAAFD, 2)
|
||||
|
||||
@@ -221,6 +221,55 @@ level_location_table = {
|
||||
LocationName.rocket_rush_dk: 0xDC30A0,
|
||||
}
|
||||
|
||||
kong_location_table = {
|
||||
LocationName.lakeside_limbo_kong: 0xDC3100,
|
||||
LocationName.doorstop_dash_kong: 0xDC3104,
|
||||
LocationName.tidal_trouble_kong: 0xDC3108,
|
||||
LocationName.skiddas_row_kong: 0xDC310C,
|
||||
LocationName.murky_mill_kong: 0xDC3110,
|
||||
|
||||
LocationName.barrel_shield_bust_up_kong: 0xDC3114,
|
||||
LocationName.riverside_race_kong: 0xDC3118,
|
||||
LocationName.squeals_on_wheels_kong: 0xDC311C,
|
||||
LocationName.springin_spiders_kong: 0xDC3120,
|
||||
LocationName.bobbing_barrel_brawl_kong: 0xDC3124,
|
||||
|
||||
LocationName.bazzas_blockade_kong: 0xDC3128,
|
||||
LocationName.rocket_barrel_ride_kong: 0xDC312C,
|
||||
LocationName.kreeping_klasps_kong: 0xDC3130,
|
||||
LocationName.tracker_barrel_trek_kong: 0xDC3134,
|
||||
LocationName.fish_food_frenzy_kong: 0xDC3138,
|
||||
|
||||
LocationName.fire_ball_frenzy_kong: 0xDC313C,
|
||||
LocationName.demolition_drain_pipe_kong: 0xDC3140,
|
||||
LocationName.ripsaw_rage_kong: 0xDC3144,
|
||||
LocationName.blazing_bazookas_kong: 0xDC3148,
|
||||
LocationName.low_g_labyrinth_kong: 0xDC314C,
|
||||
|
||||
LocationName.krevice_kreepers_kong: 0xDC3150,
|
||||
LocationName.tearaway_toboggan_kong: 0xDC3154,
|
||||
LocationName.barrel_drop_bounce_kong: 0xDC3158,
|
||||
LocationName.krack_shot_kroc_kong: 0xDC315C,
|
||||
LocationName.lemguin_lunge_kong: 0xDC3160,
|
||||
|
||||
LocationName.buzzer_barrage_kong: 0xDC3164,
|
||||
LocationName.kong_fused_cliffs_kong: 0xDC3168,
|
||||
LocationName.floodlit_fish_kong: 0xDC316C,
|
||||
LocationName.pothole_panic_kong: 0xDC3170,
|
||||
LocationName.ropey_rumpus_kong: 0xDC3174,
|
||||
|
||||
LocationName.konveyor_rope_clash_kong: 0xDC3178,
|
||||
LocationName.creepy_caverns_kong: 0xDC317C,
|
||||
LocationName.lightning_lookout_kong: 0xDC3180,
|
||||
LocationName.koindozer_klamber_kong: 0xDC3184,
|
||||
LocationName.poisonous_pipeline_kong: 0xDC3188,
|
||||
|
||||
LocationName.stampede_sprint_kong: 0xDC318C,
|
||||
LocationName.criss_cross_cliffs_kong: 0xDC3191,
|
||||
LocationName.tyrant_twin_tussle_kong: 0xDC3195,
|
||||
LocationName.swoopy_salvo_kong: 0xDC319A,
|
||||
}
|
||||
|
||||
|
||||
boss_location_table = {
|
||||
LocationName.belchas_barn: 0xDC30A1,
|
||||
@@ -266,6 +315,7 @@ all_locations = {
|
||||
**boss_location_table,
|
||||
**secret_cave_location_table,
|
||||
**brothers_bear_location_table,
|
||||
**kong_location_table,
|
||||
}
|
||||
|
||||
location_table = {}
|
||||
@@ -277,6 +327,9 @@ def setup_locations(world, player: int):
|
||||
if False:#world.include_trade_sequence[player].value:
|
||||
location_table.update({**brothers_bear_location_table})
|
||||
|
||||
if world.kongsanity[player].value:
|
||||
location_table.update({**kong_location_table})
|
||||
|
||||
return location_table
|
||||
|
||||
|
||||
|
||||
@@ -1,197 +1,236 @@
|
||||
# Level Definitions
|
||||
lakeside_limbo_flag = "Lakeside Limbo - Flag"
|
||||
lakeside_limbo_kong = "Lakeside Limbo - KONG"
|
||||
lakeside_limbo_bonus_1 = "Lakeside Limbo - Bonus 1"
|
||||
lakeside_limbo_bonus_2 = "Lakeside Limbo - Bonus 2"
|
||||
lakeside_limbo_dk = "Lakeside Limbo - DK Coin"
|
||||
|
||||
doorstop_dash_flag = "Doorstop Dash - Flag"
|
||||
doorstop_dash_kong = "Doorstop Dash - KONG"
|
||||
doorstop_dash_bonus_1 = "Doorstop Dash - Bonus 1"
|
||||
doorstop_dash_bonus_2 = "Doorstop Dash - Bonus 2"
|
||||
doorstop_dash_dk = "Doorstop Dash - DK Coin"
|
||||
|
||||
tidal_trouble_flag = "Tidal Trouble - Flag"
|
||||
tidal_trouble_kong = "Tidal Trouble - KONG"
|
||||
tidal_trouble_bonus_1 = "Tidal Trouble - Bonus 1"
|
||||
tidal_trouble_bonus_2 = "Tidal Trouble - Bonus 2"
|
||||
tidal_trouble_dk = "Tidal Trouble - DK Coin"
|
||||
|
||||
skiddas_row_flag = "Skidda's Row - Flag"
|
||||
skiddas_row_kong = "Skidda's Row - KONG"
|
||||
skiddas_row_bonus_1 = "Skidda's Row - Bonus 1"
|
||||
skiddas_row_bonus_2 = "Skidda's Row - Bonus 2"
|
||||
skiddas_row_dk = "Skidda's Row - DK Coin"
|
||||
|
||||
murky_mill_flag = "Murky Mill - Flag"
|
||||
murky_mill_kong = "Murky Mill - KONG"
|
||||
murky_mill_bonus_1 = "Murky Mill - Bonus 1"
|
||||
murky_mill_bonus_2 = "Murky Mill - Bonus 2"
|
||||
murky_mill_dk = "Murky Mill - DK Coin"
|
||||
|
||||
barrel_shield_bust_up_flag = "Barrel Shield Bust-Up - Flag"
|
||||
barrel_shield_bust_up_kong = "Barrel Shield Bust-Up - KONG"
|
||||
barrel_shield_bust_up_bonus_1 = "Barrel Shield Bust-Up - Bonus 1"
|
||||
barrel_shield_bust_up_bonus_2 = "Barrel Shield Bust-Up - Bonus 2"
|
||||
barrel_shield_bust_up_dk = "Barrel Shield Bust-Up - DK Coin"
|
||||
|
||||
riverside_race_flag = "Riverside Race - Flag"
|
||||
riverside_race_kong = "Riverside Race - KONG"
|
||||
riverside_race_bonus_1 = "Riverside Race - Bonus 1"
|
||||
riverside_race_bonus_2 = "Riverside Race - Bonus 2"
|
||||
riverside_race_dk = "Riverside Race - DK Coin"
|
||||
|
||||
squeals_on_wheels_flag = "Squeals On Wheels - Flag"
|
||||
squeals_on_wheels_kong = "Squeals On Wheels - KONG"
|
||||
squeals_on_wheels_bonus_1 = "Squeals On Wheels - Bonus 1"
|
||||
squeals_on_wheels_bonus_2 = "Squeals On Wheels - Bonus 2"
|
||||
squeals_on_wheels_dk = "Squeals On Wheels - DK Coin"
|
||||
|
||||
springin_spiders_flag = "Springin' Spiders - Flag"
|
||||
springin_spiders_kong = "Springin' Spiders - KONG"
|
||||
springin_spiders_bonus_1 = "Springin' Spiders - Bonus 1"
|
||||
springin_spiders_bonus_2 = "Springin' Spiders - Bonus 2"
|
||||
springin_spiders_dk = "Springin' Spiders - DK Coin"
|
||||
|
||||
bobbing_barrel_brawl_flag = "Bobbing Barrel Brawl - Flag"
|
||||
bobbing_barrel_brawl_kong = "Bobbing Barrel Brawl - KONG"
|
||||
bobbing_barrel_brawl_bonus_1 = "Bobbing Barrel Brawl - Bonus 1"
|
||||
bobbing_barrel_brawl_bonus_2 = "Bobbing Barrel Brawl - Bonus 2"
|
||||
bobbing_barrel_brawl_dk = "Bobbing Barrel Brawl - DK Coin"
|
||||
|
||||
bazzas_blockade_flag = "Bazza's Blockade - Flag"
|
||||
bazzas_blockade_kong = "Bazza's Blockade - KONG"
|
||||
bazzas_blockade_bonus_1 = "Bazza's Blockade - Bonus 1"
|
||||
bazzas_blockade_bonus_2 = "Bazza's Blockade - Bonus 2"
|
||||
bazzas_blockade_dk = "Bazza's Blockade - DK Coin"
|
||||
|
||||
rocket_barrel_ride_flag = "Rocket Barrel Ride - Flag"
|
||||
rocket_barrel_ride_kong = "Rocket Barrel Ride - KONG"
|
||||
rocket_barrel_ride_bonus_1 = "Rocket Barrel Ride - Bonus 1"
|
||||
rocket_barrel_ride_bonus_2 = "Rocket Barrel Ride - Bonus 2"
|
||||
rocket_barrel_ride_dk = "Rocket Barrel Ride - DK Coin"
|
||||
|
||||
kreeping_klasps_flag = "Kreeping Klasps - Flag"
|
||||
kreeping_klasps_kong = "Kreeping Klasps - KONG"
|
||||
kreeping_klasps_bonus_1 = "Kreeping Klasps - Bonus 1"
|
||||
kreeping_klasps_bonus_2 = "Kreeping Klasps - Bonus 2"
|
||||
kreeping_klasps_dk = "Kreeping Klasps - DK Coin"
|
||||
|
||||
tracker_barrel_trek_flag = "Tracker Barrel Trek - Flag"
|
||||
tracker_barrel_trek_kong = "Tracker Barrel Trek - KONG"
|
||||
tracker_barrel_trek_bonus_1 = "Tracker Barrel Trek - Bonus 1"
|
||||
tracker_barrel_trek_bonus_2 = "Tracker Barrel Trek - Bonus 2"
|
||||
tracker_barrel_trek_dk = "Tracker Barrel Trek - DK Coin"
|
||||
|
||||
fish_food_frenzy_flag = "Fish Food Frenzy - Flag"
|
||||
fish_food_frenzy_kong = "Fish Food Frenzy - KONG"
|
||||
fish_food_frenzy_bonus_1 = "Fish Food Frenzy - Bonus 1"
|
||||
fish_food_frenzy_bonus_2 = "Fish Food Frenzy - Bonus 2"
|
||||
fish_food_frenzy_dk = "Fish Food Frenzy - DK Coin"
|
||||
|
||||
fire_ball_frenzy_flag = "Fire-Ball Frenzy - Flag"
|
||||
fire_ball_frenzy_kong = "Fire-Ball Frenzy - KONG"
|
||||
fire_ball_frenzy_bonus_1 = "Fire-Ball Frenzy - Bonus 1"
|
||||
fire_ball_frenzy_bonus_2 = "Fire-Ball Frenzy - Bonus 2"
|
||||
fire_ball_frenzy_dk = "Fire-Ball Frenzy - DK Coin"
|
||||
|
||||
demolition_drain_pipe_flag = "Demolition Drain-Pipe - Flag"
|
||||
demolition_drain_pipe_kong = "Demolition Drain-Pipe - KONG"
|
||||
demolition_drain_pipe_bonus_1 = "Demolition Drain-Pipe - Bonus 1"
|
||||
demolition_drain_pipe_bonus_2 = "Demolition Drain-Pipe - Bonus 2"
|
||||
demolition_drain_pipe_dk = "Demolition Drain-Pipe - DK Coin"
|
||||
|
||||
ripsaw_rage_flag = "Ripsaw Rage - Flag"
|
||||
ripsaw_rage_kong = "Ripsaw Rage - KONG"
|
||||
ripsaw_rage_bonus_1 = "Ripsaw Rage - Bonus 1"
|
||||
ripsaw_rage_bonus_2 = "Ripsaw Rage - Bonus 2"
|
||||
ripsaw_rage_dk = "Ripsaw Rage - DK Coin"
|
||||
|
||||
blazing_bazookas_flag = "Blazing Bazookas - Flag"
|
||||
blazing_bazookas_bonus_1 = "Blazing Bazookas - Bonus 1"
|
||||
blazing_bazookas_bonus_2 = "Blazing Bazookas - Bonus 2"
|
||||
blazing_bazookas_dk = "Blazing Bazookas - DK Coin"
|
||||
blazing_bazookas_flag = "Blazing Bazukas - Flag"
|
||||
blazing_bazookas_kong = "Blazing Bazukas - KONG"
|
||||
blazing_bazookas_bonus_1 = "Blazing Bazukas - Bonus 1"
|
||||
blazing_bazookas_bonus_2 = "Blazing Bazukas - Bonus 2"
|
||||
blazing_bazookas_dk = "Blazing Bazukas - DK Coin"
|
||||
|
||||
low_g_labyrinth_flag = "Low-G Labyrinth - Flag"
|
||||
low_g_labyrinth_kong = "Low-G Labyrinth - KONG"
|
||||
low_g_labyrinth_bonus_1 = "Low-G Labyrinth - Bonus 1"
|
||||
low_g_labyrinth_bonus_2 = "Low-G Labyrinth - Bonus 2"
|
||||
low_g_labyrinth_dk = "Low-G Labyrinth - DK Coin"
|
||||
|
||||
krevice_kreepers_flag = "Krevice Kreepers - Flag"
|
||||
krevice_kreepers_kong = "Krevice Kreepers - KONG"
|
||||
krevice_kreepers_bonus_1 = "Krevice Kreepers - Bonus 1"
|
||||
krevice_kreepers_bonus_2 = "Krevice Kreepers - Bonus 2"
|
||||
krevice_kreepers_dk = "Krevice Kreepers - DK Coin"
|
||||
|
||||
tearaway_toboggan_flag = "Tearaway Toboggan - Flag"
|
||||
tearaway_toboggan_kong = "Tearaway Toboggan - KONG"
|
||||
tearaway_toboggan_bonus_1 = "Tearaway Toboggan - Bonus 1"
|
||||
tearaway_toboggan_bonus_2 = "Tearaway Toboggan - Bonus 2"
|
||||
tearaway_toboggan_dk = "Tearaway Toboggan - DK Coin"
|
||||
|
||||
barrel_drop_bounce_flag = "Barrel Drop Bounce - Flag"
|
||||
barrel_drop_bounce_kong = "Barrel Drop Bounce - KONG"
|
||||
barrel_drop_bounce_bonus_1 = "Barrel Drop Bounce - Bonus 1"
|
||||
barrel_drop_bounce_bonus_2 = "Barrel Drop Bounce - Bonus 2"
|
||||
barrel_drop_bounce_dk = "Barrel Drop Bounce - DK Coin"
|
||||
|
||||
krack_shot_kroc_flag = "Krack-Shot Kroc - Flag"
|
||||
krack_shot_kroc_kong = "Krack-Shot Kroc - KONG"
|
||||
krack_shot_kroc_bonus_1 = "Krack-Shot Kroc - Bonus 1"
|
||||
krack_shot_kroc_bonus_2 = "Krack-Shot Kroc - Bonus 2"
|
||||
krack_shot_kroc_dk = "Krack-Shot Kroc - DK Coin"
|
||||
|
||||
lemguin_lunge_flag = "Lemguin Lunge - Flag"
|
||||
lemguin_lunge_kong = "Lemguin Lunge - KONG"
|
||||
lemguin_lunge_bonus_1 = "Lemguin Lunge - Bonus 1"
|
||||
lemguin_lunge_bonus_2 = "Lemguin Lunge - Bonus 2"
|
||||
lemguin_lunge_dk = "Lemguin Lunge - DK Coin"
|
||||
|
||||
buzzer_barrage_flag = "Buzzer Barrage - Flag"
|
||||
buzzer_barrage_kong = "Buzzer Barrage - KONG"
|
||||
buzzer_barrage_bonus_1 = "Buzzer Barrage - Bonus 1"
|
||||
buzzer_barrage_bonus_2 = "Buzzer Barrage - Bonus 2"
|
||||
buzzer_barrage_dk = "Buzzer Barrage - DK Coin"
|
||||
|
||||
kong_fused_cliffs_flag = "Kong-Fused Cliffs - Flag"
|
||||
kong_fused_cliffs_kong = "Kong-Fused Cliffs - KONG"
|
||||
kong_fused_cliffs_bonus_1 = "Kong-Fused Cliffs - Bonus 1"
|
||||
kong_fused_cliffs_bonus_2 = "Kong-Fused Cliffs - Bonus 2"
|
||||
kong_fused_cliffs_dk = "Kong-Fused Cliffs - DK Coin"
|
||||
|
||||
floodlit_fish_flag = "Floodlit Fish - Flag"
|
||||
floodlit_fish_kong = "Floodlit Fish - KONG"
|
||||
floodlit_fish_bonus_1 = "Floodlit Fish - Bonus 1"
|
||||
floodlit_fish_bonus_2 = "Floodlit Fish - Bonus 2"
|
||||
floodlit_fish_dk = "Floodlit Fish - DK Coin"
|
||||
|
||||
pothole_panic_flag = "Pothole Panic - Flag"
|
||||
pothole_panic_kong = "Pothole Panic - KONG"
|
||||
pothole_panic_bonus_1 = "Pothole Panic - Bonus 1"
|
||||
pothole_panic_bonus_2 = "Pothole Panic - Bonus 2"
|
||||
pothole_panic_dk = "Pothole Panic - DK Coin"
|
||||
|
||||
ropey_rumpus_flag = "Ropey Rumpus - Flag"
|
||||
ropey_rumpus_kong = "Ropey Rumpus - KONG"
|
||||
ropey_rumpus_bonus_1 = "Ropey Rumpus - Bonus 1"
|
||||
ropey_rumpus_bonus_2 = "Ropey Rumpus - Bonus 2"
|
||||
ropey_rumpus_dk = "Ropey Rumpus - DK Coin"
|
||||
|
||||
konveyor_rope_clash_flag = "Konveyor Rope Klash - Flag"
|
||||
konveyor_rope_clash_kong = "Konveyor Rope Klash - KONG"
|
||||
konveyor_rope_clash_bonus_1 = "Konveyor Rope Klash - Bonus 1"
|
||||
konveyor_rope_clash_bonus_2 = "Konveyor Rope Klash - Bonus 2"
|
||||
konveyor_rope_clash_dk = "Konveyor Rope Klash - DK Coin"
|
||||
|
||||
creepy_caverns_flag = "Creepy Caverns - Flag"
|
||||
creepy_caverns_kong = "Creepy Caverns - KONG"
|
||||
creepy_caverns_bonus_1 = "Creepy Caverns - Bonus 1"
|
||||
creepy_caverns_bonus_2 = "Creepy Caverns - Bonus 2"
|
||||
creepy_caverns_dk = "Creepy Caverns - DK Coin"
|
||||
|
||||
lightning_lookout_flag = "Lightning Lookout - Flag"
|
||||
lightning_lookout_kong = "Lightning Lookout - KONG"
|
||||
lightning_lookout_bonus_1 = "Lightning Lookout - Bonus 1"
|
||||
lightning_lookout_bonus_2 = "Lightning Lookout - Bonus 2"
|
||||
lightning_lookout_dk = "Lightning Lookout - DK Coin"
|
||||
|
||||
koindozer_klamber_flag = "Koindozer Klamber - Flag"
|
||||
koindozer_klamber_kong = "Koindozer Klamber - KONG"
|
||||
koindozer_klamber_bonus_1 = "Koindozer Klamber - Bonus 1"
|
||||
koindozer_klamber_bonus_2 = "Koindozer Klamber - Bonus 2"
|
||||
koindozer_klamber_dk = "Koindozer Klamber - DK Coin"
|
||||
|
||||
poisonous_pipeline_flag = "Poisonous Pipeline - Flag"
|
||||
poisonous_pipeline_kong = "Poisonous Pipeline - KONG"
|
||||
poisonous_pipeline_bonus_1 = "Poisonous Pipeline - Bonus 1"
|
||||
poisonous_pipeline_bonus_2 = "Poisonous Pipeline - Bonus 2"
|
||||
poisonous_pipeline_dk = "Poisonous Pipeline - DK Coin"
|
||||
|
||||
stampede_sprint_flag = "Stampede Sprint - Flag"
|
||||
stampede_sprint_kong = "Stampede Sprint - KONG"
|
||||
stampede_sprint_bonus_1 = "Stampede Sprint - Bonus 1"
|
||||
stampede_sprint_bonus_2 = "Stampede Sprint - Bonus 2"
|
||||
stampede_sprint_bonus_3 = "Stampede Sprint - Bonus 3"
|
||||
stampede_sprint_dk = "Stampede Sprint - DK Coin"
|
||||
|
||||
criss_cross_cliffs_flag = "Criss Kross Cliffs - Flag"
|
||||
criss_cross_cliffs_kong = "Criss Kross Cliffs - KONG"
|
||||
criss_cross_cliffs_bonus_1 = "Criss Kross Cliffs - Bonus 1"
|
||||
criss_cross_cliffs_bonus_2 = "Criss Kross Cliffs - Bonus 2"
|
||||
criss_cross_cliffs_dk = "Criss Kross Cliffs - DK Coin"
|
||||
|
||||
tyrant_twin_tussle_flag = "Tyrant Twin Tussle - Flag"
|
||||
tyrant_twin_tussle_kong = "Tyrant Twin Tussle - KONG"
|
||||
tyrant_twin_tussle_bonus_1 = "Tyrant Twin Tussle - Bonus 1"
|
||||
tyrant_twin_tussle_bonus_2 = "Tyrant Twin Tussle - Bonus 2"
|
||||
tyrant_twin_tussle_bonus_3 = "Tyrant Twin Tussle - Bonus 3"
|
||||
tyrant_twin_tussle_dk = "Tyrant Twin Tussle - DK Coin"
|
||||
|
||||
swoopy_salvo_flag = "Swoopy Salvo - Flag"
|
||||
swoopy_salvo_kong = "Swoopy Salvo - KONG"
|
||||
swoopy_salvo_bonus_1 = "Swoopy Salvo - Bonus 1"
|
||||
swoopy_salvo_bonus_2 = "Swoopy Salvo - Bonus 2"
|
||||
swoopy_salvo_bonus_3 = "Swoopy Salvo - Bonus 3"
|
||||
|
||||
@@ -6,7 +6,7 @@ from Options import Choice, Range, Option, Toggle, DeathLink, DefaultOnToggle, O
|
||||
class Goal(Choice):
|
||||
"""
|
||||
Determines the goal of the seed
|
||||
Knautilus: Reach the Knautilus and defeat Baron K. Roolenstein
|
||||
Knautilus: Scuttle the Knautilus in Krematoa and defeat Baron K. Roolenstein
|
||||
Banana Bird Hunt: Find a certain number of Banana Birds and rescue their mother
|
||||
"""
|
||||
display_name = "Goal"
|
||||
@@ -75,6 +75,13 @@ class PercentageOfBananaBirds(Range):
|
||||
default = 100
|
||||
|
||||
|
||||
class KONGsanity(Toggle):
|
||||
"""
|
||||
Whether collecting all four KONG letters in each level grants a check
|
||||
"""
|
||||
display_name = "KONGsanity"
|
||||
|
||||
|
||||
class LevelShuffle(Toggle):
|
||||
"""
|
||||
Whether levels are shuffled
|
||||
@@ -82,6 +89,41 @@ class LevelShuffle(Toggle):
|
||||
display_name = "Level Shuffle"
|
||||
|
||||
|
||||
class Difficulty(Choice):
|
||||
"""
|
||||
Which Difficulty Level to use
|
||||
NORML: The Normal Difficulty
|
||||
HARDR: Many DK Barrels are removed
|
||||
TUFST: Most DK Barrels and all Midway Barrels are removed
|
||||
"""
|
||||
display_name = "Difficulty"
|
||||
option_norml = 0
|
||||
option_hardr = 1
|
||||
option_tufst = 2
|
||||
default = 0
|
||||
|
||||
@classmethod
|
||||
def get_option_name(cls, value) -> str:
|
||||
if cls.auto_display_name:
|
||||
return cls.name_lookup[value].upper()
|
||||
else:
|
||||
return cls.name_lookup[value]
|
||||
|
||||
|
||||
class Autosave(DefaultOnToggle):
|
||||
"""
|
||||
Whether the game should autosave after each level
|
||||
"""
|
||||
display_name = "Autosave"
|
||||
|
||||
|
||||
class MERRY(Toggle):
|
||||
"""
|
||||
Whether the Bonus Barrels will be Christmas-themed
|
||||
"""
|
||||
display_name = "MERRY"
|
||||
|
||||
|
||||
class MusicShuffle(Toggle):
|
||||
"""
|
||||
Whether music is shuffled
|
||||
@@ -125,7 +167,11 @@ dkc3_options: typing.Dict[str, type(Option)] = {
|
||||
"percentage_of_extra_bonus_coins": PercentageOfExtraBonusCoins,
|
||||
"number_of_banana_birds": NumberOfBananaBirds,
|
||||
"percentage_of_banana_birds": PercentageOfBananaBirds,
|
||||
"kongsanity": KONGsanity,
|
||||
"level_shuffle": LevelShuffle,
|
||||
"difficulty": Difficulty,
|
||||
"autosave": Autosave,
|
||||
"merry": MERRY,
|
||||
"music_shuffle": MusicShuffle,
|
||||
"kong_palette_swap": KongPaletteSwap,
|
||||
"starting_life_count": StartingLifeCount,
|
||||
|
||||
@@ -44,6 +44,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.lakeside_limbo_bonus_2 : [0x657, 3],
|
||||
LocationName.lakeside_limbo_dk : [0x657, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
lakeside_limbo_region_locations[LocationName.lakeside_limbo_kong] = []
|
||||
lakeside_limbo_region = create_region(world, player, active_locations, LocationName.lakeside_limbo_region,
|
||||
lakeside_limbo_region_locations, None)
|
||||
|
||||
@@ -53,6 +55,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.doorstop_dash_bonus_2 : [0x65A, 3],
|
||||
LocationName.doorstop_dash_dk : [0x65A, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
doorstop_dash_region_locations[LocationName.doorstop_dash_kong] = []
|
||||
doorstop_dash_region = create_region(world, player, active_locations, LocationName.doorstop_dash_region,
|
||||
doorstop_dash_region_locations, None)
|
||||
|
||||
@@ -62,6 +66,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.tidal_trouble_bonus_2 : [0x659, 3],
|
||||
LocationName.tidal_trouble_dk : [0x659, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
tidal_trouble_region_locations[LocationName.tidal_trouble_kong] = []
|
||||
tidal_trouble_region = create_region(world, player, active_locations, LocationName.tidal_trouble_region,
|
||||
tidal_trouble_region_locations, None)
|
||||
|
||||
@@ -71,6 +77,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.skiddas_row_bonus_2 : [0x65D, 3],
|
||||
LocationName.skiddas_row_dk : [0x65D, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
skiddas_row_region_locations[LocationName.skiddas_row_kong] = []
|
||||
skiddas_row_region = create_region(world, player, active_locations, LocationName.skiddas_row_region,
|
||||
skiddas_row_region_locations, None)
|
||||
|
||||
@@ -80,6 +88,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.murky_mill_bonus_2 : [0x65C, 3],
|
||||
LocationName.murky_mill_dk : [0x65C, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
murky_mill_region_locations[LocationName.murky_mill_kong] = []
|
||||
murky_mill_region = create_region(world, player, active_locations, LocationName.murky_mill_region,
|
||||
murky_mill_region_locations, None)
|
||||
|
||||
@@ -89,6 +99,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.barrel_shield_bust_up_bonus_2 : [0x662, 3],
|
||||
LocationName.barrel_shield_bust_up_dk : [0x662, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
barrel_shield_bust_up_region_locations[LocationName.barrel_shield_bust_up_kong] = []
|
||||
barrel_shield_bust_up_region = create_region(world, player, active_locations, LocationName.barrel_shield_bust_up_region,
|
||||
barrel_shield_bust_up_region_locations, None)
|
||||
|
||||
@@ -98,6 +110,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.riverside_race_bonus_2 : [0x664, 3],
|
||||
LocationName.riverside_race_dk : [0x664, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
riverside_race_region_locations[LocationName.riverside_race_kong] = []
|
||||
riverside_race_region = create_region(world, player, active_locations, LocationName.riverside_race_region,
|
||||
riverside_race_region_locations, None)
|
||||
|
||||
@@ -107,6 +121,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.squeals_on_wheels_bonus_2 : [0x65B, 3],
|
||||
LocationName.squeals_on_wheels_dk : [0x65B, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
squeals_on_wheels_region_locations[LocationName.squeals_on_wheels_kong] = []
|
||||
squeals_on_wheels_region = create_region(world, player, active_locations, LocationName.squeals_on_wheels_region,
|
||||
squeals_on_wheels_region_locations, None)
|
||||
|
||||
@@ -116,6 +132,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.springin_spiders_bonus_2 : [0x661, 3],
|
||||
LocationName.springin_spiders_dk : [0x661, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
springin_spiders_region_locations[LocationName.springin_spiders_kong] = []
|
||||
springin_spiders_region = create_region(world, player, active_locations, LocationName.springin_spiders_region,
|
||||
springin_spiders_region_locations, None)
|
||||
|
||||
@@ -125,6 +143,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.bobbing_barrel_brawl_bonus_2 : [0x666, 3],
|
||||
LocationName.bobbing_barrel_brawl_dk : [0x666, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
bobbing_barrel_brawl_region_locations[LocationName.bobbing_barrel_brawl_kong] = []
|
||||
bobbing_barrel_brawl_region = create_region(world, player, active_locations, LocationName.bobbing_barrel_brawl_region,
|
||||
bobbing_barrel_brawl_region_locations, None)
|
||||
|
||||
@@ -134,6 +154,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.bazzas_blockade_bonus_2 : [0x667, 3],
|
||||
LocationName.bazzas_blockade_dk : [0x667, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
bazzas_blockade_region_locations[LocationName.bazzas_blockade_kong] = []
|
||||
bazzas_blockade_region = create_region(world, player, active_locations, LocationName.bazzas_blockade_region,
|
||||
bazzas_blockade_region_locations, None)
|
||||
|
||||
@@ -143,6 +165,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.rocket_barrel_ride_bonus_2 : [0x66A, 3],
|
||||
LocationName.rocket_barrel_ride_dk : [0x66A, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
rocket_barrel_ride_region_locations[LocationName.rocket_barrel_ride_kong] = []
|
||||
rocket_barrel_ride_region = create_region(world, player, active_locations, LocationName.rocket_barrel_ride_region,
|
||||
rocket_barrel_ride_region_locations, None)
|
||||
|
||||
@@ -152,6 +176,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.kreeping_klasps_bonus_2 : [0x658, 3],
|
||||
LocationName.kreeping_klasps_dk : [0x658, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
kreeping_klasps_region_locations[LocationName.kreeping_klasps_kong] = []
|
||||
kreeping_klasps_region = create_region(world, player, active_locations, LocationName.kreeping_klasps_region,
|
||||
kreeping_klasps_region_locations, None)
|
||||
|
||||
@@ -161,6 +187,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.tracker_barrel_trek_bonus_2 : [0x66B, 3],
|
||||
LocationName.tracker_barrel_trek_dk : [0x66B, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
tracker_barrel_trek_region_locations[LocationName.tracker_barrel_trek_kong] = []
|
||||
tracker_barrel_trek_region = create_region(world, player, active_locations, LocationName.tracker_barrel_trek_region,
|
||||
tracker_barrel_trek_region_locations, None)
|
||||
|
||||
@@ -170,6 +198,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.fish_food_frenzy_bonus_2 : [0x668, 3],
|
||||
LocationName.fish_food_frenzy_dk : [0x668, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
fish_food_frenzy_region_locations[LocationName.fish_food_frenzy_kong] = []
|
||||
fish_food_frenzy_region = create_region(world, player, active_locations, LocationName.fish_food_frenzy_region,
|
||||
fish_food_frenzy_region_locations, None)
|
||||
|
||||
@@ -179,6 +209,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.fire_ball_frenzy_bonus_2 : [0x66D, 3],
|
||||
LocationName.fire_ball_frenzy_dk : [0x66D, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
fire_ball_frenzy_region_locations[LocationName.fire_ball_frenzy_kong] = []
|
||||
fire_ball_frenzy_region = create_region(world, player, active_locations, LocationName.fire_ball_frenzy_region,
|
||||
fire_ball_frenzy_region_locations, None)
|
||||
|
||||
@@ -188,6 +220,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.demolition_drain_pipe_bonus_2 : [0x672, 3],
|
||||
LocationName.demolition_drain_pipe_dk : [0x672, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
demolition_drain_pipe_region_locations[LocationName.demolition_drain_pipe_kong] = []
|
||||
demolition_drain_pipe_region = create_region(world, player, active_locations, LocationName.demolition_drain_pipe_region,
|
||||
demolition_drain_pipe_region_locations, None)
|
||||
|
||||
@@ -197,6 +231,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.ripsaw_rage_bonus_2 : [0x660, 3],
|
||||
LocationName.ripsaw_rage_dk : [0x660, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
ripsaw_rage_region_locations[LocationName.ripsaw_rage_kong] = []
|
||||
ripsaw_rage_region = create_region(world, player, active_locations, LocationName.ripsaw_rage_region,
|
||||
ripsaw_rage_region_locations, None)
|
||||
|
||||
@@ -206,6 +242,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.blazing_bazookas_bonus_2 : [0x66E, 3],
|
||||
LocationName.blazing_bazookas_dk : [0x66E, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
blazing_bazookas_region_locations[LocationName.blazing_bazookas_kong] = []
|
||||
blazing_bazookas_region = create_region(world, player, active_locations, LocationName.blazing_bazookas_region,
|
||||
blazing_bazookas_region_locations, None)
|
||||
|
||||
@@ -215,6 +253,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.low_g_labyrinth_bonus_2 : [0x670, 3],
|
||||
LocationName.low_g_labyrinth_dk : [0x670, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
low_g_labyrinth_region_locations[LocationName.low_g_labyrinth_kong] = []
|
||||
low_g_labyrinth_region = create_region(world, player, active_locations, LocationName.low_g_labyrinth_region,
|
||||
low_g_labyrinth_region_locations, None)
|
||||
|
||||
@@ -224,6 +264,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.krevice_kreepers_bonus_2 : [0x673, 3],
|
||||
LocationName.krevice_kreepers_dk : [0x673, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
krevice_kreepers_region_locations[LocationName.krevice_kreepers_kong] = []
|
||||
krevice_kreepers_region = create_region(world, player, active_locations, LocationName.krevice_kreepers_region,
|
||||
krevice_kreepers_region_locations, None)
|
||||
|
||||
@@ -233,6 +275,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.tearaway_toboggan_bonus_2 : [0x65F, 3],
|
||||
LocationName.tearaway_toboggan_dk : [0x65F, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
tearaway_toboggan_region_locations[LocationName.tearaway_toboggan_kong] = []
|
||||
tearaway_toboggan_region = create_region(world, player, active_locations, LocationName.tearaway_toboggan_region,
|
||||
tearaway_toboggan_region_locations, None)
|
||||
|
||||
@@ -242,6 +286,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.barrel_drop_bounce_bonus_2 : [0x66C, 3],
|
||||
LocationName.barrel_drop_bounce_dk : [0x66C, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
barrel_drop_bounce_region_locations[LocationName.barrel_drop_bounce_kong] = []
|
||||
barrel_drop_bounce_region = create_region(world, player, active_locations, LocationName.barrel_drop_bounce_region,
|
||||
barrel_drop_bounce_region_locations, None)
|
||||
|
||||
@@ -251,6 +297,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.krack_shot_kroc_bonus_2 : [0x66F, 3],
|
||||
LocationName.krack_shot_kroc_dk : [0x66F, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
krack_shot_kroc_region_locations[LocationName.krack_shot_kroc_kong] = []
|
||||
krack_shot_kroc_region = create_region(world, player, active_locations, LocationName.krack_shot_kroc_region,
|
||||
krack_shot_kroc_region_locations, None)
|
||||
|
||||
@@ -260,6 +308,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.lemguin_lunge_bonus_2 : [0x65E, 3],
|
||||
LocationName.lemguin_lunge_dk : [0x65E, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
lemguin_lunge_region_locations[LocationName.lemguin_lunge_kong] = []
|
||||
lemguin_lunge_region = create_region(world, player, active_locations, LocationName.lemguin_lunge_region,
|
||||
lemguin_lunge_region_locations, None)
|
||||
|
||||
@@ -269,6 +319,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.buzzer_barrage_bonus_2 : [0x676, 3],
|
||||
LocationName.buzzer_barrage_dk : [0x676, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
buzzer_barrage_region_locations[LocationName.buzzer_barrage_kong] = []
|
||||
buzzer_barrage_region = create_region(world, player, active_locations, LocationName.buzzer_barrage_region,
|
||||
buzzer_barrage_region_locations, None)
|
||||
|
||||
@@ -278,6 +330,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.kong_fused_cliffs_bonus_2 : [0x674, 3],
|
||||
LocationName.kong_fused_cliffs_dk : [0x674, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
kong_fused_cliffs_region_locations[LocationName.kong_fused_cliffs_kong] = []
|
||||
kong_fused_cliffs_region = create_region(world, player, active_locations, LocationName.kong_fused_cliffs_region,
|
||||
kong_fused_cliffs_region_locations, None)
|
||||
|
||||
@@ -287,6 +341,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.floodlit_fish_bonus_2 : [0x669, 3],
|
||||
LocationName.floodlit_fish_dk : [0x669, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
floodlit_fish_region_locations[LocationName.floodlit_fish_kong] = []
|
||||
floodlit_fish_region = create_region(world, player, active_locations, LocationName.floodlit_fish_region,
|
||||
floodlit_fish_region_locations, None)
|
||||
|
||||
@@ -296,6 +352,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.pothole_panic_bonus_2 : [0x677, 3],
|
||||
LocationName.pothole_panic_dk : [0x677, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
pothole_panic_region_locations[LocationName.pothole_panic_kong] = []
|
||||
pothole_panic_region = create_region(world, player, active_locations, LocationName.pothole_panic_region,
|
||||
pothole_panic_region_locations, None)
|
||||
|
||||
@@ -305,6 +363,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.ropey_rumpus_bonus_2 : [0x675, 3],
|
||||
LocationName.ropey_rumpus_dk : [0x675, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
ropey_rumpus_region_locations[LocationName.ropey_rumpus_kong] = []
|
||||
ropey_rumpus_region = create_region(world, player, active_locations, LocationName.ropey_rumpus_region,
|
||||
ropey_rumpus_region_locations, None)
|
||||
|
||||
@@ -314,6 +374,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.konveyor_rope_clash_bonus_2 : [0x657, 3],
|
||||
LocationName.konveyor_rope_clash_dk : [0x657, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
konveyor_rope_clash_region_locations[LocationName.konveyor_rope_clash_kong] = []
|
||||
konveyor_rope_clash_region = create_region(world, player, active_locations, LocationName.konveyor_rope_clash_region,
|
||||
konveyor_rope_clash_region_locations, None)
|
||||
|
||||
@@ -323,6 +385,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.creepy_caverns_bonus_2 : [0x678, 3],
|
||||
LocationName.creepy_caverns_dk : [0x678, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
creepy_caverns_region_locations[LocationName.creepy_caverns_kong] = []
|
||||
creepy_caverns_region = create_region(world, player, active_locations, LocationName.creepy_caverns_region,
|
||||
creepy_caverns_region_locations, None)
|
||||
|
||||
@@ -332,6 +396,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.lightning_lookout_bonus_2 : [0x665, 3],
|
||||
LocationName.lightning_lookout_dk : [0x665, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
lightning_lookout_region_locations[LocationName.lightning_lookout_kong] = []
|
||||
lightning_lookout_region = create_region(world, player, active_locations, LocationName.lightning_lookout_region,
|
||||
lightning_lookout_region_locations, None)
|
||||
|
||||
@@ -341,6 +407,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.koindozer_klamber_bonus_2 : [0x679, 3],
|
||||
LocationName.koindozer_klamber_dk : [0x679, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
koindozer_klamber_region_locations[LocationName.koindozer_klamber_kong] = []
|
||||
koindozer_klamber_region = create_region(world, player, active_locations, LocationName.koindozer_klamber_region,
|
||||
koindozer_klamber_region_locations, None)
|
||||
|
||||
@@ -350,6 +418,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.poisonous_pipeline_bonus_2 : [0x671, 3],
|
||||
LocationName.poisonous_pipeline_dk : [0x671, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
poisonous_pipeline_region_locations[LocationName.poisonous_pipeline_kong] = []
|
||||
poisonous_pipeline_region = create_region(world, player, active_locations, LocationName.poisonous_pipeline_region,
|
||||
poisonous_pipeline_region_locations, None)
|
||||
|
||||
@@ -360,6 +430,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.stampede_sprint_bonus_3 : [0x67B, 4],
|
||||
LocationName.stampede_sprint_dk : [0x67B, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
stampede_sprint_region_locations[LocationName.stampede_sprint_kong] = []
|
||||
stampede_sprint_region = create_region(world, player, active_locations, LocationName.stampede_sprint_region,
|
||||
stampede_sprint_region_locations, None)
|
||||
|
||||
@@ -369,6 +441,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.criss_cross_cliffs_bonus_2 : [0x67C, 3],
|
||||
LocationName.criss_cross_cliffs_dk : [0x67C, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
criss_cross_cliffs_region_locations[LocationName.criss_cross_cliffs_kong] = []
|
||||
criss_cross_cliffs_region = create_region(world, player, active_locations, LocationName.criss_cross_cliffs_region,
|
||||
criss_cross_cliffs_region_locations, None)
|
||||
|
||||
@@ -379,6 +453,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.tyrant_twin_tussle_bonus_3 : [0x67D, 4],
|
||||
LocationName.tyrant_twin_tussle_dk : [0x67D, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
tyrant_twin_tussle_region_locations[LocationName.tyrant_twin_tussle_kong] = []
|
||||
tyrant_twin_tussle_region = create_region(world, player, active_locations, LocationName.tyrant_twin_tussle_region,
|
||||
tyrant_twin_tussle_region_locations, None)
|
||||
|
||||
@@ -389,6 +465,8 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.swoopy_salvo_bonus_3 : [0x663, 4],
|
||||
LocationName.swoopy_salvo_dk : [0x663, 5],
|
||||
}
|
||||
if world.kongsanity[player]:
|
||||
swoopy_salvo_region_locations[LocationName.swoopy_salvo_kong] = []
|
||||
swoopy_salvo_region = create_region(world, player, active_locations, LocationName.swoopy_salvo_region,
|
||||
swoopy_salvo_region_locations, None)
|
||||
|
||||
@@ -503,9 +581,7 @@ def create_regions(world, player: int, active_locations):
|
||||
|
||||
sky_high_secret_region_locations = {}
|
||||
if False:#world.include_trade_sequence[player]:
|
||||
sky_high_secret_region_locations.update({
|
||||
LocationName.sky_high_secret: [0x64B, 1],
|
||||
})
|
||||
sky_high_secret_region_locations[LocationName.sky_high_secret] = [0x64B, 1]
|
||||
sky_high_secret_region = create_region(world, player, active_locations, LocationName.sky_high_secret_region,
|
||||
sky_high_secret_region_locations, None)
|
||||
|
||||
@@ -517,9 +593,7 @@ def create_regions(world, player: int, active_locations):
|
||||
|
||||
cifftop_cache_region_locations = {}
|
||||
if False:#world.include_trade_sequence[player]:
|
||||
cifftop_cache_region_locations.update({
|
||||
LocationName.cifftop_cache: [0x64D, 1],
|
||||
})
|
||||
cifftop_cache_region_locations[LocationName.cifftop_cache] = [0x64D, 1]
|
||||
cifftop_cache_region = create_region(world, player, active_locations, LocationName.cifftop_cache_region,
|
||||
cifftop_cache_region_locations, None)
|
||||
|
||||
@@ -622,29 +696,19 @@ def create_regions(world, player: int, active_locations):
|
||||
LocationName.bazaars_general_store_2: [0x615, 3, True],
|
||||
})
|
||||
|
||||
bramble_region_locations.update({
|
||||
LocationName.brambles_bungalow: [0x619, 2],
|
||||
})
|
||||
bramble_region_locations[LocationName.brambles_bungalow] = [0x619, 2]
|
||||
|
||||
#flower_spot_region_locations.update({
|
||||
# LocationName.flower_spot: [0x615, 3, True],
|
||||
#})
|
||||
|
||||
barter_region_locations.update({
|
||||
LocationName.barters_swap_shop: [0x61B, 3],
|
||||
})
|
||||
barter_region_locations[LocationName.barters_swap_shop] = [0x61B, 3]
|
||||
|
||||
barnacle_region_locations.update({
|
||||
LocationName.barnacles_island: [0x61D, 2],
|
||||
})
|
||||
barnacle_region_locations[LocationName.barnacles_island] = [0x61D, 2]
|
||||
|
||||
blue_region_locations.update({
|
||||
LocationName.blues_beach_hut: [0x621, 4],
|
||||
})
|
||||
blue_region_locations[LocationName.blues_beach_hut] = [0x621, 4]
|
||||
|
||||
blizzard_region_locations.update({
|
||||
LocationName.blizzards_basecamp: [0x625, 4, True],
|
||||
})
|
||||
blizzard_region_locations[LocationName.blizzards_basecamp] = [0x625, 4, True]
|
||||
|
||||
bazaar_region = create_region(world, player, active_locations, LocationName.bazaar_region,
|
||||
bazaar_region_locations, None)
|
||||
@@ -817,7 +881,6 @@ def connect_regions(world, player, level_list):
|
||||
level_list[32],
|
||||
level_list[33],
|
||||
level_list[34],
|
||||
LocationName.kastle_kaos_region,
|
||||
LocationName.sewer_stockpile_region,
|
||||
]
|
||||
|
||||
@@ -835,10 +898,16 @@ def connect_regions(world, player, level_list):
|
||||
|
||||
for i in range(0, len(krematoa_levels)):
|
||||
connect(world, player, names, LocationName.krematoa_region, krematoa_levels[i],
|
||||
lambda state: (state.has(ItemName.bonus_coin, player, world.krematoa_bonus_coin_cost[player].value * (i+1))))
|
||||
|
||||
connect(world, player, names, LocationName.krematoa_region, LocationName.knautilus_region,
|
||||
lambda state: (state.has(ItemName.krematoa_cog, player, 5)))
|
||||
lambda state, i=i: (state.has(ItemName.bonus_coin, player, world.krematoa_bonus_coin_cost[player].value * (i+1))))
|
||||
|
||||
if world.goal[player] == "knautilus":
|
||||
connect(world, player, names, LocationName.kaos_kore_region, LocationName.knautilus_region)
|
||||
connect(world, player, names, LocationName.krematoa_region, LocationName.kastle_kaos_region,
|
||||
lambda state: (state.has(ItemName.krematoa_cog, player, 5)))
|
||||
else:
|
||||
connect(world, player, names, LocationName.kaos_kore_region, LocationName.kastle_kaos_region)
|
||||
connect(world, player, names, LocationName.krematoa_region, LocationName.knautilus_region,
|
||||
lambda state: (state.has(ItemName.krematoa_cog, player, 5)))
|
||||
|
||||
|
||||
def create_region(world: MultiWorld, player: int, active_locations, name: str, locations=None, exits=None):
|
||||
|
||||
@@ -11,187 +11,270 @@ import os
|
||||
import math
|
||||
|
||||
|
||||
level_unlock_map = {
|
||||
0x657: [0x65A],
|
||||
0x65A: [0x680, 0x639, 0x659],
|
||||
0x659: [0x65D],
|
||||
0x65D: [0x65C],
|
||||
0x65C: [0x688, 0x64F],
|
||||
|
||||
0x662: [0x681, 0x664],
|
||||
0x664: [0x65B],
|
||||
0x65B: [0x689, 0x661],
|
||||
0x661: [0x63A, 0x666],
|
||||
0x666: [0x650, 0x649],
|
||||
|
||||
0x667: [0x66A],
|
||||
0x66A: [0x682, 0x658],
|
||||
0x658: [0x68A, 0x66B],
|
||||
0x66B: [0x668],
|
||||
0x668: [0x651],
|
||||
|
||||
0x66D: [0x63C, 0x672],
|
||||
0x672: [0x68B, 0x660],
|
||||
0x660: [0x683, 0x66E],
|
||||
0x66E: [0x670],
|
||||
0x670: [0x652],
|
||||
|
||||
0x673: [0x684, 0x65F],
|
||||
0x65F: [0x66C],
|
||||
0x66C: [0x66F],
|
||||
0x66F: [0x65E],
|
||||
0x65E: [0x63D, 0x653, 0x68C, 0x64C],
|
||||
|
||||
0x676: [0x63E, 0x674, 0x685],
|
||||
0x674: [0x63F, 0x669],
|
||||
0x669: [0x677],
|
||||
0x677: [0x68D, 0x675],
|
||||
0x675: [0x654],
|
||||
|
||||
0x67A: [0x640, 0x678],
|
||||
0x678: [0x665],
|
||||
0x665: [0x686, 0x679],
|
||||
0x679: [0x68E, 0x671],
|
||||
|
||||
0x67B: [0x67C],
|
||||
0x67C: [0x67D],
|
||||
0x67D: [0x663],
|
||||
0x663: [0x67E],
|
||||
}
|
||||
|
||||
location_rom_data = {
|
||||
0xDC3000: [0x657, 1], # Lakeside Limbo
|
||||
0xDC3001: [0x657, 2],
|
||||
0xDC3002: [0x657, 3],
|
||||
0xDC3003: [0x657, 5],
|
||||
0xDC3100: [0x657, 7],
|
||||
|
||||
0xDC3004: [0x65A, 1], # Doorstop Dash
|
||||
0xDC3005: [0x65A, 2],
|
||||
0xDC3006: [0x65A, 3],
|
||||
0xDC3007: [0x65A, 5],
|
||||
0xDC3104: [0x65A, 7],
|
||||
|
||||
0xDC3008: [0x659, 1], # Tidal Trouble
|
||||
0xDC3009: [0x659, 2],
|
||||
0xDC300A: [0x659, 3],
|
||||
0xDC300B: [0x659, 5],
|
||||
0xDC3108: [0x659, 7],
|
||||
|
||||
0xDC300C: [0x65D, 1], # Skidda's Row
|
||||
0xDC300D: [0x65D, 2],
|
||||
0xDC300E: [0x65D, 3],
|
||||
0xDC300F: [0x65D, 5],
|
||||
0xDC310C: [0x65D, 7],
|
||||
|
||||
0xDC3010: [0x65C, 1], # Murky Mill
|
||||
0xDC3011: [0x65C, 2],
|
||||
0xDC3012: [0x65C, 3],
|
||||
0xDC3013: [0x65C, 5],
|
||||
0xDC3110: [0x65C, 7],
|
||||
|
||||
|
||||
0xDC3014: [0x662, 1], # Barrel Shield Bust-Up
|
||||
0xDC3015: [0x662, 2],
|
||||
0xDC3016: [0x662, 3],
|
||||
0xDC3017: [0x662, 5],
|
||||
0xDC3114: [0x662, 7],
|
||||
|
||||
0xDC3018: [0x664, 1], # Riverside Race
|
||||
0xDC3019: [0x664, 2],
|
||||
0xDC301A: [0x664, 3],
|
||||
0xDC301B: [0x664, 5],
|
||||
0xDC3118: [0x664, 7],
|
||||
|
||||
0xDC301C: [0x65B, 1], # Squeals on Wheels
|
||||
0xDC301D: [0x65B, 2],
|
||||
0xDC301E: [0x65B, 3],
|
||||
0xDC301F: [0x65B, 5],
|
||||
0xDC311C: [0x65B, 7],
|
||||
|
||||
0xDC3020: [0x661, 1], # Springin' Spiders
|
||||
0xDC3021: [0x661, 2],
|
||||
0xDC3022: [0x661, 3],
|
||||
0xDC3023: [0x661, 5],
|
||||
0xDC3120: [0x661, 7],
|
||||
|
||||
0xDC3024: [0x666, 1], # Bobbing Barrel Brawl
|
||||
0xDC3025: [0x666, 2],
|
||||
0xDC3026: [0x666, 3],
|
||||
0xDC3027: [0x666, 5],
|
||||
0xDC3124: [0x666, 7],
|
||||
|
||||
|
||||
0xDC3028: [0x667, 1], # Bazza's Blockade
|
||||
0xDC3029: [0x667, 2],
|
||||
0xDC302A: [0x667, 3],
|
||||
0xDC302B: [0x667, 5],
|
||||
0xDC3128: [0x667, 7],
|
||||
|
||||
0xDC302C: [0x66A, 1], # Rocket Barrel Ride
|
||||
0xDC302D: [0x66A, 2],
|
||||
0xDC302E: [0x66A, 3],
|
||||
0xDC302F: [0x66A, 5],
|
||||
0xDC312C: [0x66A, 7],
|
||||
|
||||
0xDC3030: [0x658, 1], # Kreeping Klasps
|
||||
0xDC3031: [0x658, 2],
|
||||
0xDC3032: [0x658, 3],
|
||||
0xDC3033: [0x658, 5],
|
||||
0xDC3130: [0x658, 7],
|
||||
|
||||
0xDC3034: [0x66B, 1], # Tracker Barrel Trek
|
||||
0xDC3035: [0x66B, 2],
|
||||
0xDC3036: [0x66B, 3],
|
||||
0xDC3037: [0x66B, 5],
|
||||
0xDC3134: [0x66B, 7],
|
||||
|
||||
0xDC3038: [0x668, 1], # Fish Food Frenzy
|
||||
0xDC3039: [0x668, 2],
|
||||
0xDC303A: [0x668, 3],
|
||||
0xDC303B: [0x668, 5],
|
||||
0xDC3138: [0x668, 7],
|
||||
|
||||
|
||||
0xDC303C: [0x66D, 1], # Fire-ball Frenzy
|
||||
0xDC303D: [0x66D, 2],
|
||||
0xDC303E: [0x66D, 3],
|
||||
0xDC303F: [0x66D, 5],
|
||||
0xDC313C: [0x66D, 7],
|
||||
|
||||
0xDC3040: [0x672, 1], # Demolition Drainpipe
|
||||
0xDC3041: [0x672, 2],
|
||||
0xDC3042: [0x672, 3],
|
||||
0xDC3043: [0x672, 5],
|
||||
0xDC3140: [0x672, 7],
|
||||
|
||||
0xDC3044: [0x660, 1], # Ripsaw Rage
|
||||
0xDC3045: [0x660, 2],
|
||||
0xDC3046: [0x660, 3],
|
||||
0xDC3047: [0x660, 5],
|
||||
0xDC3144: [0x660, 7],
|
||||
|
||||
0xDC3048: [0x66E, 1], # Blazing Bazukas
|
||||
0xDC3049: [0x66E, 2],
|
||||
0xDC304A: [0x66E, 3],
|
||||
0xDC304B: [0x66E, 5],
|
||||
0xDC3148: [0x66E, 7],
|
||||
|
||||
0xDC304C: [0x670, 1], # Low-G Labyrinth
|
||||
0xDC304D: [0x670, 2],
|
||||
0xDC304E: [0x670, 3],
|
||||
0xDC304F: [0x670, 5],
|
||||
0xDC314C: [0x670, 7],
|
||||
|
||||
|
||||
0xDC3050: [0x673, 1], # Krevice Kreepers
|
||||
0xDC3051: [0x673, 2],
|
||||
0xDC3052: [0x673, 3],
|
||||
0xDC3053: [0x673, 5],
|
||||
0xDC3150: [0x673, 7],
|
||||
|
||||
0xDC3054: [0x65F, 1], # Tearaway Toboggan
|
||||
0xDC3055: [0x65F, 2],
|
||||
0xDC3056: [0x65F, 3],
|
||||
0xDC3057: [0x65F, 5],
|
||||
0xDC3154: [0x65F, 7],
|
||||
|
||||
0xDC3058: [0x66C, 1], # Barrel Drop Bounce
|
||||
0xDC3059: [0x66C, 2],
|
||||
0xDC305A: [0x66C, 3],
|
||||
0xDC305B: [0x66C, 5],
|
||||
0xDC3158: [0x66C, 7],
|
||||
|
||||
0xDC305C: [0x66F, 1], # Krack-Shot Kroc
|
||||
0xDC305D: [0x66F, 2],
|
||||
0xDC305E: [0x66F, 3],
|
||||
0xDC305F: [0x66F, 5],
|
||||
0xDC315C: [0x66F, 7],
|
||||
|
||||
0xDC3060: [0x65E, 1], # Lemguin Lunge
|
||||
0xDC3061: [0x65E, 2],
|
||||
0xDC3062: [0x65E, 3],
|
||||
0xDC3063: [0x65E, 5],
|
||||
0xDC3160: [0x65E, 7],
|
||||
|
||||
|
||||
0xDC3064: [0x676, 1], # Buzzer Barrage
|
||||
0xDC3065: [0x676, 2],
|
||||
0xDC3066: [0x676, 3],
|
||||
0xDC3067: [0x676, 5],
|
||||
0xDC3164: [0x676, 7],
|
||||
|
||||
0xDC3068: [0x674, 1], # Kong-Fused Cliffs
|
||||
0xDC3069: [0x674, 2],
|
||||
0xDC306A: [0x674, 3],
|
||||
0xDC306B: [0x674, 5],
|
||||
0xDC3168: [0x674, 7],
|
||||
|
||||
0xDC306C: [0x669, 1], # Floodlit Fish
|
||||
0xDC306D: [0x669, 2],
|
||||
0xDC306E: [0x669, 3],
|
||||
0xDC306F: [0x669, 5],
|
||||
0xDC316C: [0x669, 7],
|
||||
|
||||
0xDC3070: [0x677, 1], # Pothole Panic
|
||||
0xDC3071: [0x677, 2],
|
||||
0xDC3072: [0x677, 3],
|
||||
0xDC3073: [0x677, 5],
|
||||
0xDC3170: [0x677, 7],
|
||||
|
||||
0xDC3074: [0x675, 1], # Ropey Rumpus
|
||||
0xDC3075: [0x675, 2],
|
||||
0xDC3076: [0x675, 3],
|
||||
0xDC3077: [0x675, 5],
|
||||
0xDC3174: [0x675, 7],
|
||||
|
||||
|
||||
0xDC3078: [0x67A, 1], # Konveyor Rope Klash
|
||||
0xDC3079: [0x67A, 2],
|
||||
0xDC307A: [0x67A, 3],
|
||||
0xDC307B: [0x67A, 5],
|
||||
0xDC3178: [0x67A, 7],
|
||||
|
||||
0xDC307C: [0x678, 1], # Creepy Caverns
|
||||
0xDC307D: [0x678, 2],
|
||||
0xDC307E: [0x678, 3],
|
||||
0xDC307F: [0x678, 5],
|
||||
0xDC317C: [0x678, 7],
|
||||
|
||||
0xDC3080: [0x665, 1], # Lightning Lookout
|
||||
0xDC3081: [0x665, 2],
|
||||
0xDC3082: [0x665, 3],
|
||||
0xDC3083: [0x665, 5],
|
||||
0xDC3180: [0x665, 7],
|
||||
|
||||
0xDC3084: [0x679, 1], # Koindozer Klamber
|
||||
0xDC3085: [0x679, 2],
|
||||
0xDC3086: [0x679, 3],
|
||||
0xDC3087: [0x679, 5],
|
||||
0xDC3184: [0x679, 7],
|
||||
|
||||
0xDC3088: [0x671, 1], # Poisonous Pipeline
|
||||
0xDC3089: [0x671, 2],
|
||||
0xDC308A: [0x671, 3],
|
||||
0xDC308B: [0x671, 5],
|
||||
0xDC3188: [0x671, 7],
|
||||
|
||||
|
||||
0xDC308C: [0x67B, 1], # Stampede Sprint
|
||||
@@ -199,23 +282,27 @@ location_rom_data = {
|
||||
0xDC308E: [0x67B, 3],
|
||||
0xDC308F: [0x67B, 4],
|
||||
0xDC3090: [0x67B, 5],
|
||||
0xDC318C: [0x67B, 7],
|
||||
|
||||
0xDC3091: [0x67C, 1], # Criss Kross Cliffs
|
||||
0xDC3092: [0x67C, 2],
|
||||
0xDC3093: [0x67C, 3],
|
||||
0xDC3094: [0x67C, 5],
|
||||
0xDC3191: [0x67C, 7],
|
||||
|
||||
0xDC3095: [0x67D, 1], # Tyrant Twin Tussle
|
||||
0xDC3096: [0x67D, 2],
|
||||
0xDC3097: [0x67D, 3],
|
||||
0xDC3098: [0x67D, 4],
|
||||
0xDC3099: [0x67D, 5],
|
||||
0xDC3195: [0x67D, 7],
|
||||
|
||||
0xDC309A: [0x663, 1], # Swoopy Salvo
|
||||
0xDC309B: [0x663, 2],
|
||||
0xDC309C: [0x663, 3],
|
||||
0xDC309D: [0x663, 4],
|
||||
0xDC309E: [0x663, 5],
|
||||
0xDC319A: [0x663, 7],
|
||||
|
||||
0xDC309F: [0x67E, 1], # Rocket Rush
|
||||
0xDC30A0: [0x67E, 5],
|
||||
@@ -243,7 +330,7 @@ location_rom_data = {
|
||||
#0xDC30B4: [0x64D, 1], # Disabled until Trade Sequence
|
||||
0xDC30B5: [0x64E, 1],
|
||||
|
||||
0xDC30B6: [0x5FD, 4], # Banana Bird Mother
|
||||
0xDC30B6: [0x5FE, 4], # Banana Bird Mother
|
||||
|
||||
# DKC3_TODO: Disabled until Trade Sequence
|
||||
#0xDC30B7: [0x615, 2, True],
|
||||
@@ -256,6 +343,18 @@ location_rom_data = {
|
||||
#0xDC30BE: [0x625, 4, True],
|
||||
}
|
||||
|
||||
boss_location_ids = [
|
||||
0xDC30A1,
|
||||
0xDC30A2,
|
||||
0xDC30A3,
|
||||
0xDC30A4,
|
||||
0xDC30A5,
|
||||
0xDC30A6,
|
||||
0xDC30A7,
|
||||
0xDC30A8,
|
||||
0xDC30B6,
|
||||
]
|
||||
|
||||
|
||||
item_rom_data = {
|
||||
0xDC3001: [0x5D5], # 1-Up Balloon
|
||||
@@ -400,10 +499,13 @@ def patch_rom(world, rom, player, active_level_list):
|
||||
rom.write_byte(0x3484DE, 0xEA)
|
||||
rom.write_byte(0x348528, 0x80) # Prevent Single-Ski Lock
|
||||
|
||||
|
||||
# Make Swanky free
|
||||
rom.write_byte(0x348C48, 0x00)
|
||||
|
||||
rom.write_bytes(0x34AB70, bytearray([0xEA, 0xEA]))
|
||||
rom.write_bytes(0x34ABF7, bytearray([0xEA, 0xEA]))
|
||||
rom.write_bytes(0x34ACD0, bytearray([0xEA, 0xEA]))
|
||||
|
||||
# Banana Bird Costs
|
||||
if world.goal[player] == "banana_bird_hunt":
|
||||
banana_bird_cost = math.floor(world.number_of_banana_birds[player] * world.percentage_of_banana_birds[player] / 100.0)
|
||||
@@ -462,6 +564,25 @@ def patch_rom(world, rom, player, active_level_list):
|
||||
rom.write_byte(0x9130, world.starting_life_count[player].value)
|
||||
rom.write_byte(0x913B, world.starting_life_count[player].value)
|
||||
|
||||
# Cheat options
|
||||
cheat_bytes = [0x00, 0x00]
|
||||
|
||||
if world.merry[player]:
|
||||
cheat_bytes[0] |= 0x01
|
||||
|
||||
if world.autosave[player]:
|
||||
cheat_bytes[0] |= 0x02
|
||||
|
||||
if world.difficulty[player] == "tufst":
|
||||
cheat_bytes[0] |= 0x80
|
||||
cheat_bytes[1] |= 0x80
|
||||
elif world.difficulty[player] == "hardr":
|
||||
cheat_bytes[0] |= 0x00
|
||||
cheat_bytes[1] |= 0x00
|
||||
elif world.difficulty[player] == "norml":
|
||||
cheat_bytes[1] |= 0x40
|
||||
|
||||
rom.write_bytes(0x8303, bytearray(cheat_bytes))
|
||||
|
||||
# Handle Level Shuffle Here
|
||||
if world.level_shuffle[player]:
|
||||
@@ -469,6 +590,9 @@ def patch_rom(world, rom, player, active_level_list):
|
||||
rom.write_byte(level_dict[level_list[i]].nameIDAddress, level_dict[active_level_list[i]].nameID)
|
||||
rom.write_byte(level_dict[level_list[i]].levelIDAddress, level_dict[active_level_list[i]].levelID)
|
||||
|
||||
rom.write_byte(0x3FF800 + level_dict[active_level_list[i]].levelID, level_dict[level_list[i]].levelID)
|
||||
rom.write_byte(0x3FF860 + level_dict[level_list[i]].levelID, level_dict[active_level_list[i]].levelID)
|
||||
|
||||
# First levels of each world
|
||||
rom.write_byte(0x34BC3E, (0x32 + level_dict[active_level_list[0]].levelID))
|
||||
rom.write_byte(0x34BC47, (0x32 + level_dict[active_level_list[5]].levelID))
|
||||
@@ -495,6 +619,52 @@ def patch_rom(world, rom, player, active_level_list):
|
||||
|
||||
rom.write_byte(0x32F339, 0x55)
|
||||
|
||||
# Handle KONGsanity Here
|
||||
if world.kongsanity[player]:
|
||||
# Arich's Hoard KONGsanity fix
|
||||
rom.write_bytes(0x34BA8C, bytearray([0xEA, 0xEA]))
|
||||
|
||||
# Don't hide the level flag if the 0x80 bit is set
|
||||
rom.write_bytes(0x34CE92, bytearray([0x80]))
|
||||
|
||||
# Use the `!` next to level name for indicating KONG letters
|
||||
rom.write_bytes(0x34B8F0, bytearray([0x80]))
|
||||
rom.write_bytes(0x34B8F3, bytearray([0x80]))
|
||||
|
||||
# Hijack to code to set the 0x80 flag for the level when you complete KONG
|
||||
rom.write_bytes(0x3BCD4B, bytearray([0x22, 0x80, 0xFA, 0XB8])) # JSL $B8FA80
|
||||
|
||||
rom.write_bytes(0x38FA80, bytearray([0xDA])) # PHX
|
||||
rom.write_bytes(0x38FA81, bytearray([0x48])) # PHA
|
||||
rom.write_bytes(0x38FA82, bytearray([0x08])) # PHP
|
||||
rom.write_bytes(0x38FA83, bytearray([0xE2, 0x20])) # SEP #20
|
||||
rom.write_bytes(0x38FA85, bytearray([0x48])) # PHA
|
||||
rom.write_bytes(0x38FA86, bytearray([0x18])) # CLC
|
||||
rom.write_bytes(0x38FA87, bytearray([0x6D, 0xD3, 0x18])) # ADC $18D3
|
||||
rom.write_bytes(0x38FA8A, bytearray([0x8D, 0xD3, 0x18])) # STA $18D3
|
||||
rom.write_bytes(0x38FA8D, bytearray([0x68])) # PLA
|
||||
rom.write_bytes(0x38FA8E, bytearray([0xC2, 0x20])) # REP 20
|
||||
rom.write_bytes(0x38FA90, bytearray([0X18])) # CLC
|
||||
rom.write_bytes(0x38FA91, bytearray([0x6D, 0xD5, 0x05])) # ADC $05D5
|
||||
rom.write_bytes(0x38FA94, bytearray([0x8D, 0xD5, 0x05])) # STA $05D5
|
||||
rom.write_bytes(0x38FA97, bytearray([0xAE, 0xB9, 0x05])) # LDX $05B9
|
||||
rom.write_bytes(0x38FA9A, bytearray([0xBD, 0x32, 0x06])) # LDA $0632, X
|
||||
rom.write_bytes(0x38FA9D, bytearray([0x09, 0x80, 0x00])) # ORA #8000
|
||||
rom.write_bytes(0x38FAA0, bytearray([0x9D, 0x32, 0x06])) # STA $0632, X
|
||||
rom.write_bytes(0x38FAA3, bytearray([0xAD, 0xD5, 0x18])) # LDA $18D5
|
||||
rom.write_bytes(0x38FAA6, bytearray([0xD0, 0x03])) # BNE $80EA
|
||||
rom.write_bytes(0x38FAA8, bytearray([0x9C, 0xD9, 0x18])) # STZ $18D9
|
||||
rom.write_bytes(0x38FAAB, bytearray([0xA9, 0x78, 0x00])) # LDA #0078
|
||||
rom.write_bytes(0x38FAAE, bytearray([0x8D, 0xD5, 0x18])) # STA $18D5
|
||||
rom.write_bytes(0x38FAB1, bytearray([0x28])) # PLP
|
||||
rom.write_bytes(0x38FAB2, bytearray([0x68])) # PLA
|
||||
rom.write_bytes(0x38FAB3, bytearray([0xFA])) # PLX
|
||||
rom.write_bytes(0x38FAB4, bytearray([0x6B])) # RTL
|
||||
# End Handle KONGsanity
|
||||
|
||||
# Handle Credits
|
||||
rom.write_bytes(0x32A5DF, bytearray([0x41, 0x52, 0x43, 0x48, 0x49, 0x50, 0x45, 0x4C, 0x41, 0x47, 0x4F, 0x20, 0x4D, 0x4F, 0xC4])) # "ARCHIPELAGO MOD"
|
||||
rom.write_bytes(0x32A5EE, bytearray([0x00, 0x03, 0x50, 0x4F, 0x52, 0x59, 0x47, 0x4F, 0x4E, 0xC5])) # "PORYGONE"
|
||||
|
||||
from Main import __version__
|
||||
rom.name = bytearray(f'D3{__version__.replace(".", "")[0:3]}_{player}_{world.seed:11}\0', 'utf8')[:21]
|
||||
@@ -516,6 +686,17 @@ def patch_rom(world, rom, player, active_level_list):
|
||||
rom.write_byte(0x32DD63, 0xEA)
|
||||
rom.write_byte(0x32DD64, 0xEA)
|
||||
|
||||
# Don't grant Banana Birds at Bears
|
||||
rom.write_byte(0x3492DB, 0xEA)
|
||||
rom.write_byte(0x3492DC, 0xEA)
|
||||
rom.write_byte(0x3492DD, 0xEA)
|
||||
rom.write_byte(0x3493F4, 0xEA)
|
||||
rom.write_byte(0x3493F5, 0xEA)
|
||||
rom.write_byte(0x3493F6, 0xEA)
|
||||
|
||||
# Don't grant present at Blizzard
|
||||
rom.write_byte(0x8454, 0x00)
|
||||
|
||||
# Don't grant Patch and Skis from their bosses
|
||||
rom.write_byte(0x3F3762, 0x00)
|
||||
rom.write_byte(0x3F377B, 0x00)
|
||||
|
||||
@@ -4,7 +4,7 @@ import math
|
||||
import threading
|
||||
|
||||
from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification
|
||||
from .Items import DKC3Item, ItemData, item_table, inventory_table
|
||||
from .Items import DKC3Item, ItemData, item_table, inventory_table, junk_table
|
||||
from .Locations import DKC3Location, all_locations, setup_locations
|
||||
from .Options import dkc3_options
|
||||
from .Regions import create_regions, connect_regions
|
||||
@@ -40,7 +40,7 @@ class DKC3World(World):
|
||||
game: str = "Donkey Kong Country 3"
|
||||
option_definitions = dkc3_options
|
||||
topology_present = False
|
||||
data_version = 1
|
||||
data_version = 2
|
||||
#hint_blacklist = {LocationName.rocket_rush_flag}
|
||||
|
||||
item_name_to_id = {name: data.code for name, data in item_table.items()}
|
||||
@@ -99,10 +99,13 @@ class DKC3World(World):
|
||||
|
||||
# Bosses
|
||||
total_required_locations += number_of_bosses
|
||||
|
||||
|
||||
# Secret Caves
|
||||
total_required_locations += 13
|
||||
|
||||
if self.world.kongsanity[self.player]:
|
||||
total_required_locations += 39
|
||||
|
||||
## Brothers Bear
|
||||
if False:#self.world.include_trade_sequence[self.player]:
|
||||
total_required_locations += 10
|
||||
@@ -118,7 +121,11 @@ class DKC3World(World):
|
||||
|
||||
total_junk_count = total_required_locations - len(itempool)
|
||||
|
||||
itempool += [self.create_item(ItemName.bear_coin)] * total_junk_count
|
||||
junk_pool = []
|
||||
for item_name in self.world.random.choices(list(junk_table.keys()), k=total_junk_count):
|
||||
junk_pool += [self.create_item(item_name)]
|
||||
|
||||
itempool += junk_pool
|
||||
|
||||
self.active_level_list = level_list.copy()
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ def generate_mod(world, output_directory: str):
|
||||
random = multiworld.slot_seeds[player]
|
||||
|
||||
def flop_random(low, high, base=None):
|
||||
"""Guarentees 50% below base and 50% above base, uniform distribution in each direction."""
|
||||
"""Guarantees 50% below base and 50% above base, uniform distribution in each direction."""
|
||||
if base:
|
||||
distance = random.random()
|
||||
if random.randint(0, 1):
|
||||
|
||||
@@ -249,6 +249,10 @@ script.on_event(defines.events.on_player_main_inventory_changed, update_player_e
|
||||
|
||||
function add_samples(force, name, count)
|
||||
local function add_to_table(t)
|
||||
if count <= 0 then
|
||||
-- Fixes a bug with single craft, if a recipe gives 0 of a given item.
|
||||
return
|
||||
end
|
||||
t[name] = (t[name] or 0) + count
|
||||
end
|
||||
-- Add to global table of earned samples for future new players
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
{% from "macros.lua" import dict_to_lua %}
|
||||
-- TODO: Replace the tinting code with an actual rendered picture of the energy bridge icon.
|
||||
-- This tint is so that one is less likely to accidentally mass-produce energy-bridges, then wonder why their rocket is not building.
|
||||
function energy_bridge_tint()
|
||||
return { r = 0, g = 1, b = 0.667, a = 1}
|
||||
end
|
||||
function tint_icon(obj, tint)
|
||||
obj.icons = { {icon = obj.icon, icon_size = obj.icon_size, icon_mipmaps = obj.icon_mipmaps, tint = tint} }
|
||||
obj.icon = nil
|
||||
obj.icon_size = nil
|
||||
obj.icon_mipmaps = nil
|
||||
end
|
||||
local energy_bridge = table.deepcopy(data.raw["accumulator"]["accumulator"])
|
||||
energy_bridge.name = "ap-energy-bridge"
|
||||
energy_bridge.minable.result = "ap-energy-bridge"
|
||||
@@ -6,12 +17,20 @@ energy_bridge.localised_name = "Archipelago EnergyLink Bridge"
|
||||
energy_bridge.energy_source.buffer_capacity = "5MJ"
|
||||
energy_bridge.energy_source.input_flow_limit = "1MW"
|
||||
energy_bridge.energy_source.output_flow_limit = "1MW"
|
||||
tint_icon(energy_bridge, energy_bridge_tint())
|
||||
energy_bridge.picture.layers[1].tint = energy_bridge_tint()
|
||||
energy_bridge.picture.layers[1].hr_version.tint = energy_bridge_tint()
|
||||
energy_bridge.charge_animation.layers[1].layers[1].tint = energy_bridge_tint()
|
||||
energy_bridge.charge_animation.layers[1].layers[1].hr_version.tint = energy_bridge_tint()
|
||||
energy_bridge.discharge_animation.layers[1].layers[1].tint = energy_bridge_tint()
|
||||
energy_bridge.discharge_animation.layers[1].layers[1].hr_version.tint = energy_bridge_tint()
|
||||
data.raw["accumulator"]["ap-energy-bridge"] = energy_bridge
|
||||
|
||||
local energy_bridge_item = table.deepcopy(data.raw["item"]["accumulator"])
|
||||
energy_bridge_item.name = "ap-energy-bridge"
|
||||
energy_bridge_item.localised_name = "Archipelago EnergyLink Bridge"
|
||||
energy_bridge_item.place_result = energy_bridge.name
|
||||
tint_icon(energy_bridge_item, energy_bridge_tint())
|
||||
data.raw["item"]["ap-energy-bridge"] = energy_bridge_item
|
||||
|
||||
local energy_bridge_recipe = table.deepcopy(data.raw["recipe"]["accumulator"])
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
-- Find out if more than one AP mod is loaded, and if so, error out.
|
||||
function mod_is_AP(str)
|
||||
-- lua string.match is way more restrictive than regex. Regex would be "^AP-W?\d{20}-P[1-9]\d*-.+$"
|
||||
local result = string.match(str, "^AP%-W?%d%d%d%d%d%d%d%d%d%d%d%d%d%d%d%d%d%d%d%d%-P[1-9]%d-%-.+$")
|
||||
if result ~= nil then
|
||||
log("Archipelago Mod: " .. result .. " is loaded.")
|
||||
end
|
||||
return result ~= nil
|
||||
end
|
||||
local ap_mod_count = 0
|
||||
for name, _ in pairs(mods) do
|
||||
if mod_is_AP(name) then
|
||||
ap_mod_count = ap_mod_count + 1
|
||||
if ap_mod_count > 1 then
|
||||
error("More than one Archipelago Factorio mod is loaded.")
|
||||
end
|
||||
end
|
||||
end
|
||||
data:extend({
|
||||
{
|
||||
type = "bool-setting",
|
||||
|
||||
@@ -20,7 +20,7 @@ FF1_STARTER_ITEMS = [
|
||||
|
||||
FF1_PROGRESSION_LIST = [
|
||||
"Rod", "Cube", "Lute", "Key", "Chime", "Oxyale",
|
||||
"Ship", "Canoe", "Floater", "Canal",
|
||||
"Ship", "Canoe", "Floater", "Mark", "Sigil", "Canal",
|
||||
"Crown", "Crystal", "Herb", "Tnt", "Adamant", "Slab", "Ruby", "Bottle",
|
||||
"Shard",
|
||||
"EarthOrb", "FireOrb", "WaterOrb", "AirOrb"
|
||||
|
||||
@@ -31,7 +31,7 @@ class FF1World(World):
|
||||
game = "Final Fantasy"
|
||||
topology_present = False
|
||||
remote_items = True
|
||||
data_version = 1
|
||||
data_version = 2
|
||||
remote_start_inventory = True
|
||||
|
||||
ff1_items = FF1Items()
|
||||
|
||||
@@ -190,5 +190,7 @@
|
||||
"Ship": 480,
|
||||
"Bridge": 488,
|
||||
"Canal": 492,
|
||||
"Canoe": 498
|
||||
"Canoe": 498,
|
||||
"Sigil": 499,
|
||||
"Mark": 500
|
||||
}
|
||||
|
||||
@@ -49,11 +49,11 @@ def exclusion_rules(world, player: int, exclude_locations: typing.Set[str]):
|
||||
location.progress_type = LocationProgressType.EXCLUDED
|
||||
|
||||
|
||||
def set_rule(spot, rule: CollectionRule):
|
||||
def set_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], rule: CollectionRule):
|
||||
spot.access_rule = rule
|
||||
|
||||
|
||||
def add_rule(spot, rule: CollectionRule, combine='and'):
|
||||
def add_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], rule: CollectionRule, combine='and'):
|
||||
old_rule = spot.access_rule
|
||||
if combine == 'or':
|
||||
spot.access_rule = lambda state: rule(state) or old_rule(state)
|
||||
@@ -61,35 +61,37 @@ def add_rule(spot, rule: CollectionRule, combine='and'):
|
||||
spot.access_rule = lambda state: rule(state) and old_rule(state)
|
||||
|
||||
|
||||
def forbid_item(location, item: str, player: int):
|
||||
def forbid_item(location: "BaseClasses.Location", item: str, player: int):
|
||||
old_rule = location.item_rule
|
||||
location.item_rule = lambda i: (i.name != item or i.player != player) and old_rule(i)
|
||||
|
||||
|
||||
def forbid_items_for_player(location, items: typing.Set[str], player: int):
|
||||
def forbid_items_for_player(location: "BaseClasses.Location", items: typing.Set[str], player: int):
|
||||
old_rule = location.item_rule
|
||||
location.item_rule = lambda i: (i.player != player or i.name not in items) and old_rule(i)
|
||||
|
||||
|
||||
def forbid_items(location, items: typing.Set[str]):
|
||||
def forbid_items(location: "BaseClasses.Location", items: typing.Set[str]):
|
||||
"""unused, but kept as a debugging tool."""
|
||||
old_rule = location.item_rule
|
||||
location.item_rule = lambda i: i.name not in items and old_rule(i)
|
||||
|
||||
|
||||
def add_item_rule(location, rule: ItemRule):
|
||||
def add_item_rule(location: "BaseClasses.Location", rule: ItemRule):
|
||||
old_rule = location.item_rule
|
||||
location.item_rule = lambda item: rule(item) and old_rule(item)
|
||||
|
||||
|
||||
def item_in_locations(state, item: str, player: int, locations: typing.Sequence):
|
||||
def item_in_locations(state: "BaseClasses.CollectionState", item: str, player: int,
|
||||
locations: typing.Sequence["BaseClasses.Location"]) -> bool:
|
||||
for location in locations:
|
||||
if item_name(state, location[0], location[1]) == (item, player):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def item_name(state, location: str, player: int) -> typing.Optional[typing.Tuple[str, int]]:
|
||||
def item_name(state: "BaseClasses.CollectionState", location: str, player: int) -> \
|
||||
typing.Optional[typing.Tuple[str, int]]:
|
||||
location = state.world.get_location(location, player)
|
||||
if location.item is None:
|
||||
return None
|
||||
|
||||
@@ -276,10 +276,10 @@ def set_advancement_rules(world: MultiWorld, player: int):
|
||||
|
||||
# 1.19 advancements
|
||||
|
||||
# can make a cake, and can reach a pillager outposts for allays
|
||||
set_rule(world.get_location("Birthday Song", player), lambda state: state.can_reach("The Lie", "Location", player))
|
||||
# find allay and craft a noteblock
|
||||
set_rule(world.get_location("You've Got a Friend in Me", player), lambda state: state.has("Progressive Tools", player, 2) and state._mc_has_iron_ingots(player))
|
||||
# can make a cake, and a noteblock, and can reach a pillager outposts for allays
|
||||
set_rule(world.get_location("Birthday Song", player), lambda state: state.can_reach("The Lie", "Location", player) and state.has("Progressive Tools", player, 2) and state._mc_has_iron_ingots(player))
|
||||
# can get to outposts.
|
||||
# set_rule(world.get_location("You've Got a Friend in Me", player), lambda state: True)
|
||||
# craft bucket and adventure to find frog spawning biome
|
||||
set_rule(world.get_location("Bukkit Bukkit", player), lambda state: state.has("Bucket", player) and state._mc_has_iron_ingots(player) and state._mc_can_adventure(player))
|
||||
# I don't like this one its way to easy to get. just a pain to find.
|
||||
|
||||
@@ -7,9 +7,9 @@ config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
|
||||
Recipes are removed from the crafting book and shuffled into the item pool. It can also optionally change which
|
||||
Some recipes are locked from being able to be crafted and shuffled into the item pool. It can also optionally change which
|
||||
structures appear in each dimension. Crafting recipes are re-learned when they are received from other players as item
|
||||
checks, and occasionally when completing your own achievements.
|
||||
checks, and occasionally when completing your own achievements. See below for which recipes are shuffled.
|
||||
|
||||
## What is considered a location check in minecraft?
|
||||
|
||||
@@ -25,3 +25,86 @@ inventory directly.
|
||||
|
||||
Victory is achieved when the player kills the Ender Dragon, enters the portal in The End, and completes the credits
|
||||
sequence either by skipping it or watching hit play out.
|
||||
|
||||
## Which recipes are locked?
|
||||
|
||||
* Archery
|
||||
* Bow
|
||||
* Arrow
|
||||
* Crossbow
|
||||
* Brewing
|
||||
* Blaze Powder
|
||||
* Brewing Stand
|
||||
* Enchanting
|
||||
* Enchanting Table
|
||||
* Bookshelf
|
||||
* Bucket
|
||||
* Flint & Steel
|
||||
* All Beds
|
||||
* Bottles
|
||||
* Shield
|
||||
* Fishing Rod
|
||||
* Fishing Rod
|
||||
* Carrot on a Stick
|
||||
* Warped Fungus on a Stick
|
||||
* Campfire
|
||||
* Campfire
|
||||
* Soul Campfire
|
||||
* Spyglass
|
||||
* Lead
|
||||
* Progressive Weapons
|
||||
* Tier I
|
||||
* Stone Sword
|
||||
* Stone Axe
|
||||
* Tier II
|
||||
* Iron Sword
|
||||
* Iron Axe
|
||||
* Tier III
|
||||
* Diamond Sword
|
||||
* Diamond Axe
|
||||
* Progessive Tools
|
||||
* Tier I
|
||||
* Stone Shovel
|
||||
* Stone Hoe
|
||||
* Tier II
|
||||
* Iron Shovel
|
||||
* Iron Hoe
|
||||
* Tier III
|
||||
* Diamond Shovel
|
||||
* Diamond Hoe
|
||||
* Netherite Ingot
|
||||
* Progressive Armor
|
||||
* Tier I
|
||||
* Iron Helmet
|
||||
* Iron Chestplate
|
||||
* Iron Leggings
|
||||
* Iron Boots
|
||||
* Tier II
|
||||
* Diamond Helmet
|
||||
* Diamond Chestplate
|
||||
* Diamond Leggings
|
||||
* Diamond Boots
|
||||
* Progressive Resource Crafting
|
||||
* Tier I
|
||||
* Iron Ingot from Nuggets
|
||||
* Iron Nugget
|
||||
* Gold Ingot from Nuggets
|
||||
* Gold Nugget
|
||||
* Furnace
|
||||
* Blast Furnace
|
||||
* Tier II
|
||||
* Redstone
|
||||
* Redstone Block
|
||||
* Glowstone
|
||||
* Iron Ingot from Iron Block
|
||||
* Iron Block
|
||||
* Gold Ingot from Gold Block
|
||||
* Gold Block
|
||||
* Diamond
|
||||
* Diamond Block
|
||||
* Netherite Block
|
||||
* Netherite Ingot from Netherite Block
|
||||
* Anvil
|
||||
* Emerald
|
||||
* Emerald Block
|
||||
* Copper Block
|
||||
|
||||
@@ -129,6 +129,8 @@ def getItemGenericName(item):
|
||||
|
||||
|
||||
def isRestrictedDungeonItem(dungeon, item):
|
||||
if not isinstance(item, OOTItem):
|
||||
return False
|
||||
if (item.map or item.compass) and dungeon.world.shuffle_mapcompass == 'dungeon':
|
||||
return item in dungeon.dungeon_items
|
||||
if item.type == 'SmallKey' and dungeon.world.shuffle_smallkeys == 'dungeon':
|
||||
|
||||