update Archipelago

This commit is contained in:
Fabian Dill 2021-01-03 14:32:32 +01:00
parent 08ca4245c1
commit 8ebd36b5a7
22 changed files with 116 additions and 177 deletions

View File

@ -5,14 +5,14 @@ from enum import Enum, unique
import logging import logging
import json import json
from collections import OrderedDict, Counter, deque from collections import OrderedDict, Counter, deque
from typing import Union, Optional, List, Dict, NamedTuple from typing import Union, Optional, List, Dict
import secrets import secrets
import random import random
import worlds.alttp
from worlds.alttp.EntranceShuffle import door_addresses, indirect_connections from worlds.alttp.EntranceShuffle import door_addresses, indirect_connections
from Utils import int16_as_bytes from Utils import int16_as_bytes
from worlds.alttp.Items import item_name_groups from worlds.alttp.Items import item_name_groups
from worlds.generic import PlandoItem, PlandoConnection
class World(): class World():
@ -1312,7 +1312,7 @@ class Spoiler(object):
with open(filename, 'w', encoding="utf-8-sig") as outfile: with open(filename, 'w', encoding="utf-8-sig") as outfile:
outfile.write( outfile.write(
'ALttP Berserker\'s Multiworld Version %s - Seed: %s\n\n' % ( 'Archipelago Version %s - Seed: %s\n\n' % (
self.metadata['version'], self.world.seed)) self.metadata['version'], self.world.seed))
outfile.write('Filling Algorithm: %s\n' % self.world.algorithm) outfile.write('Filling Algorithm: %s\n' % self.world.algorithm)
outfile.write('Players: %d\n' % self.world.players) outfile.write('Players: %d\n' % self.world.players)
@ -1421,14 +1421,3 @@ class Spoiler(object):
outfile.write('\n'.join(path_listings)) outfile.write('\n'.join(path_listings))
class PlandoItem(NamedTuple):
item: str
location: str
world: Union[bool, str] = False # False -> own world, True -> not own world
from_pool: bool = True # if item should be removed from item pool
class PlandoConnection(NamedTuple):
entrance: str
exit: str
direction: str # entrance, exit or both

4
Gui.py
View File

@ -14,7 +14,7 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
import ModuleUpdate import ModuleUpdate
ModuleUpdate.update() ModuleUpdate.update()
from AdjusterMain import adjust from worlds.alttp.AdjusterMain import adjust
from worlds.alttp.EntranceRandomizer import parse_arguments from worlds.alttp.EntranceRandomizer import parse_arguments
from GuiUtils import ToolTips, set_icon, BackgroundTaskProgress from GuiUtils import ToolTips, set_icon, BackgroundTaskProgress
from worlds.alttp.Main import main, get_seed, __version__ as MWVersion from worlds.alttp.Main import main, get_seed, __version__ as MWVersion
@ -24,7 +24,7 @@ from Utils import is_bundled, local_path, output_path, open_file
def guiMain(args=None): def guiMain(args=None):
mainWindow = Tk() mainWindow = Tk()
mainWindow.wm_title("Berserker's Multiworld %s" % MWVersion) mainWindow.wm_title("Archipelago %s" % MWVersion)
set_icon(mainWindow) set_icon(mainWindow)

View File

@ -1378,7 +1378,7 @@ async def main():
multiprocessing.freeze_support() multiprocessing.freeze_support()
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('diff_file', default="", type=str, nargs="?", parser.add_argument('diff_file', default="", type=str, nargs="?",
help='Path to a Berserker Multiworld Binary Patch file') help='Path to a Archipelago Binary Patch file')
parser.add_argument('--snes', default='localhost:8080', help='Address of the QUsb2snes server.') parser.add_argument('--snes', default='localhost:8080', help='Address of the QUsb2snes server.')
parser.add_argument('--connect', default=None, help='Address of the multiworld host.') parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
parser.add_argument('--password', default=None, help='Password of the multiworld host.') parser.add_argument('--password', default=None, help='Password of the multiworld host.')

View File

@ -1,17 +1,3 @@
__author__ = "Berserker55" # you can find me on discord.gg/8Z65BR2
"""
This script launches a Multiplayer "Multiworld" Mystery Game
.yaml files for all participating players should be placed in a /Players folder.
For every player a mystery game is rolled and a ROM created.
After generation the server is automatically launched.
It is still up to the host to forward the correct port (38281 by default) and distribute the roms to the players.
Regular Mystery has to work for this first, such as a ALTTP Base ROM and Enemizer Setup.
A guide can be found here: https://docs.google.com/document/d/19FoqUkuyStMqhOq8uGiocskMo1KMjOW4nEeG81xrKoI/edit
Configuration can be found in host.yaml
"""
import os import os
import subprocess import subprocess
import sys import sys
@ -29,7 +15,6 @@ def feedback(text: str):
if __name__ == "__main__": if __name__ == "__main__":
logging.basicConfig(format='%(message)s', level=logging.INFO) logging.basicConfig(format='%(message)s', level=logging.INFO)
try: try:
logging.info(f"{__author__}'s MultiMystery Launcher")
import ModuleUpdate import ModuleUpdate
ModuleUpdate.update() ModuleUpdate.update()
@ -85,10 +70,10 @@ if __name__ == "__main__":
for i, file in enumerate(player_files, 1): for i, file in enumerate(player_files, 1):
player_string += f"--p{i} \"{os.path.join(player_files_path, file)}\" " player_string += f"--p{i} \"{os.path.join(player_files_path, file)}\" "
if os.path.exists("BerserkerMultiServer.exe"): if os.path.exists("ArchipelagoMystery.exe"):
basemysterycommand = "BerserkerMystery.exe" # compiled windows basemysterycommand = "ArchipelagoMystery.exe" # compiled windows
elif os.path.exists("BerserkerMultiServer"): elif os.path.exists("ArchipelagoMystery"):
basemysterycommand = "BerserkerMystery" # compiled linux basemysterycommand = "ArchipelagoMystery" # compiled linux
else: else:
basemysterycommand = f"py -{py_version} Mystery.py" # source basemysterycommand = f"py -{py_version} Mystery.py" # source
@ -133,7 +118,7 @@ if __name__ == "__main__":
seedname = segment seedname = segment
break break
multidataname = f"AP_{seedname}.multidata" multidataname = f"AP_{seedname}.archipelago"
spoilername = f"AP_{seedname}_Spoiler.txt" spoilername = f"AP_{seedname}_Spoiler.txt"
romfilename = "" romfilename = ""
@ -216,10 +201,10 @@ if __name__ == "__main__":
if not args.disable_autohost: if not args.disable_autohost:
if os.path.exists(os.path.join(output_path, multidataname)): if os.path.exists(os.path.join(output_path, multidataname)):
if os.path.exists("BerserkerMultiServer.exe"): if os.path.exists("ArchipelagoServer.exe"):
baseservercommand = "BerserkerMultiServer.exe" # compiled windows baseservercommand = "ArchipelagoServer.exe" # compiled windows
elif os.path.exists("BerserkerMultiServer"): elif os.path.exists("ArchipelagoServer"):
baseservercommand = "BerserkerMultiServer" # compiled linux baseservercommand = "ArchipelagoServer" # compiled linux
else: else:
baseservercommand = f"py -{py_version} MultiServer.py" # source baseservercommand = f"py -{py_version} MultiServer.py" # source
# don't have a mac to test that. If you try to run compiled on mac, good luck. # don't have a mac to test that. If you try to run compiled on mac, good luck.

View File

@ -117,18 +117,23 @@ class Context(Node):
def load(self, multidatapath: str, use_embedded_server_options: bool = False): def load(self, multidatapath: str, use_embedded_server_options: bool = False):
with open(multidatapath, 'rb') as f: with open(multidatapath, 'rb') as f:
self._load(restricted_loads(zlib.decompress(f.read())), data = f.read()
use_embedded_server_options)
self._load(self._decompress(data), use_embedded_server_options)
self.data_filename = multidatapath self.data_filename = multidatapath
def _decompress(self, data: bytes) -> dict:
format_version = data[0]
if format_version != 1:
raise Exception("Incompatible multidata.")
return restricted_loads(zlib.decompress(data[1:]))
def _load(self, decoded_obj: dict, use_embedded_server_options: bool): def _load(self, decoded_obj: dict, use_embedded_server_options: bool):
if "minimum_versions" in jsonobj: mdata_ver = decoded_obj["minimum_versions"]["server"]
mdata_ver = tuple(jsonobj["minimum_versions"]["server"])
if mdata_ver > Utils._version_tuple: if mdata_ver > Utils._version_tuple:
raise RuntimeError(f"Supplied Multidata requires a server of at least version {mdata_ver}," raise RuntimeError(f"Supplied Multidata requires a server of at least version {mdata_ver},"
f"however this server is of version {Utils._version_tuple}") f"however this server is of version {Utils._version_tuple}")
clients_ver = jsonobj["minimum_versions"].get("clients", []) clients_ver = decoded_obj["minimum_versions"].get("clients", [])
self.minimum_client_versions = {} self.minimum_client_versions = {}
for team, player, version in clients_ver: for team, player, version in clients_ver:
self.minimum_client_versions[team, player] = Utils.Version(*version) self.minimum_client_versions[team, player] = Utils.Version(*version)
@ -191,8 +196,8 @@ class Context(Node):
self.saving = enabled self.saving = enabled
if self.saving: if self.saving:
if not self.save_filename: if not self.save_filename:
self.save_filename = (self.data_filename[:-9] if self.data_filename[-9:] == 'multidata' else ( self.save_filename = (self.data_filename[:-9] if self.data_filename.endswith('.archipelago') else (
self.data_filename + '_')) + 'multisave' self.data_filename + '_')) + 'save'
try: try:
with open(self.save_filename, 'rb') as f: with open(self.save_filename, 'rb') as f:
save_data = restricted_loads(zlib.decompress(f.read())) save_data = restricted_loads(zlib.decompress(f.read()))
@ -210,7 +215,7 @@ class Context(Node):
while self.running: while self.running:
time.sleep(self.auto_save_interval) time.sleep(self.auto_save_interval)
if self.save_dirty: if self.save_dirty:
logging.debug("Saving multisave via thread.") logging.debug("Saving via thread.")
self.save_dirty = False self.save_dirty = False
self._save() self._save()
@ -1319,7 +1324,7 @@ def parse_args() -> argparse.Namespace:
parser.add_argument('--compatibility', default=defaults["compatibility"], type=int, parser.add_argument('--compatibility', default=defaults["compatibility"], type=int,
help=""" help="""
#2 -> recommended for casual/cooperative play, attempt to be compatible with everything across all versions #2 -> recommended for casual/cooperative play, attempt to be compatible with everything across all versions
#1 -> recommended for friendly racing, only allow Berserker's Multiworld, to disallow old /getitem for example #1 -> recommended for friendly racing, tries to block third party clients
#0 -> recommended for tournaments to force a level playing field, only allow an exact version match #0 -> recommended for tournaments to force a level playing field, only allow an exact version match
""") """)
args = parser.parse_args() args = parser.parse_args()
@ -1366,7 +1371,7 @@ async def main(args: argparse.Namespace):
import tkinter.filedialog import tkinter.filedialog
root = tkinter.Tk() root = tkinter.Tk()
root.withdraw() root.withdraw()
data_filename = tkinter.filedialog.askopenfilename(filetypes=(("Multiworld data", "*.multidata"),)) data_filename = tkinter.filedialog.askopenfilename(filetypes=(("Multiworld data", "*.archipelago"),))
ctx.load(data_filename, args.use_embedded_options) ctx.load(data_filename, args.use_embedded_options)

View File

@ -7,17 +7,17 @@ import typing
import os import os
import ModuleUpdate import ModuleUpdate
from BaseClasses import PlandoItem, PlandoConnection from worlds.generic import PlandoItem, PlandoConnection
ModuleUpdate.update() ModuleUpdate.update()
import Bosses
from Utils import parse_yaml from Utils import parse_yaml
from worlds.alttp.Rom import Sprite from worlds.alttp.Rom import Sprite
from worlds.alttp.EntranceRandomizer import parse_arguments from worlds.alttp.EntranceRandomizer import parse_arguments
from worlds.alttp.Main import main as ERmain from worlds.alttp.Main import main as ERmain
from worlds.alttp.Main import get_seed, seeddigits from worlds.alttp.Main import get_seed, seeddigits
from worlds.alttp.Items import item_name_groups, item_table from worlds.alttp.Items import item_name_groups, item_table
from worlds.alttp import Bosses
def mystery_argparse(): def mystery_argparse():

View File

@ -130,9 +130,10 @@ if __name__ == "__main__":
Utils.persistent_store("servers", data['hash'], data['server']) Utils.persistent_store("servers", data['hash'], data['server'])
print(f"Host is {data['server']}") print(f"Host is {data['server']}")
elif rom.endswith("multidata"): elif rom.endswith(".archipelago"):
import json import json
import zlib import zlib
with open(rom, 'rb') as fr: with open(rom, 'rb') as fr:
multidata = zlib.decompress(fr.read()).decode("utf-8") multidata = zlib.decompress(fr.read()).decode("utf-8")
@ -144,7 +145,7 @@ if __name__ == "__main__":
from Utils import get_options from Utils import get_options
multidata["server_options"] = get_options()["server_options"] multidata["server_options"] = get_options()["server_options"]
multidata = zlib.compress(json.dumps(multidata).encode("utf-8"), 9) multidata = zlib.compress(json.dumps(multidata).encode("utf-8"), 9)
with open(rom+"_updated.multidata", 'wb') as f: with open(rom + "_updated.archipelago", 'wb') as f:
f.write(multidata) f.write(multidata)
elif rom.endswith(".zip"): elif rom.endswith(".zip"):

View File

@ -1,63 +1,9 @@
Berserker's Multiworld Archipelago
====================== ======================
A Multiworld implementation for the Legend of Zelda: A Link to the Past Randomizer. A Multiworld implementation for the Legend of Zelda: A Link to the Past Randomizer.
For setup and instructions there's a [Wiki](https://github.com/Berserker66/MultiWorld-Utilities/wiki). For setup and instructions there's a [Wiki](https://github.com/Berserker66/MultiWorld-Utilities/wiki).
Downloads can be found at [Releases](https://github.com/Berserker66/MultiWorld-Utilities/releases), including compiled windows binaries. Downloads can be found at [Releases](https://github.com/Berserker66/MultiWorld-Utilities/releases), including compiled
windows binaries.
Additions/Changes compared to Bonta's V31 Readme is a work in progress.
-----------------
Project
* Available in precompiled form and guided setup for Windows 64Bit on the [Releases](https://github.com/Berserker66/MultiWorld-Utilities/releases) page
* Compatible with Python 3.7 and 3.8. Forward Checks for Python 4.0 are done
* Update modules if they are too old to prevent crashes and other possible issues.
* Autoinstall missing modules
* Allow newer versions of modules than specified, as they will *usually* not break compatibility
* Uses "V32" MSU
* Has support for binary patching to allow legal distribution of multiworld rom files
* Various performance improvements (over 100% faster in most cases)
* Various fixes
* Overworld Glitches Logic
* Newer Entrance Randomizer Logic, allowing more potential item and boss locations
* New Goal: local triforce hunt - Keeps triforce pieces local to your world
MultiMystery.py
* Allows you to generate a Multiworld with individual player mystery weights. Since weights can also be set to 100%, this also allows for individual settings for each player in a regular multiworld.
Basis is a .yaml file that sets these weights. You can find an [playerSettings.yaml](https://github.com/Berserker66/MultiWorld-Utilities/blob/master/playerSettings.yaml) in this project folder to get started
* Additional instructions are at the start of the file. Open with a text editor
* Configuration options can be found in the [host.yaml](https://github.com/Berserker66/MultiWorld-Utilities/blob/master/host.yaml) file
* Allows a new Mode called "Meta-Mystery", allowing certain mystery settings to apply to all players
* For example, everyone gets the same but random goal
MultiServer.py
* Supports automatic port-forwarding, can be enabled in [host.yaml](https://github.com/Berserker66/MultiWorld-Utilities/blob/master/host.yaml)
* Added commands `/hint` and `!hint`. See [host.yaml](https://github.com/Berserker66/MultiWorld-Utilities/blob/master/host.yaml) for more information
* Updates have been made to the following commands:
* `!players` now displays the number of connected players, expected total player count, and which players are missing
* `forfeit` now works when a player is no longer connected
* `/send`, `/hint`, and various other commands now use "fuzzy text matching". It is no longer required to enter a location, player name or item name perfectly
* Some item groups also exist, so `/hint Bottles` lists all bottle varieties
Mystery.py
* Defaults to generating a non-race ROM (Bonta's only makes race ROMs at this time).
If a race ROM is desired, pass --create-race as argument to it
* When an error is generated due to a broken .yaml file, it now mentions in the error trace which file, line, and character is the culprit
* Option for progressive items, allowing you to turn them off (see [playerSettings.yaml](https://github.com/Berserker66/MultiWorld-Utilities/blob/master/playerSettings.yaml) for more information)
* Option for "timer", allows you to configure a timer to display in game and/or options for timed one hit knock out
* Option for "dungeon_counters", allowing you to configure the dungeon item counter
* Option for "glitch_boots", allowing to run glitched modes without automatic boots
* Supports new Meta-Mystery mode. Read [meta.yaml](https://github.com/Berserker66/MultiWorld-Utilities/blob/master/meta.yaml) for details.
* Added `dungeonssimple` and `dungeonsfull` entrance randomizer modes
* Option for local items, allowing certain items to appear in your world only and not in other players' worlds
* Option for linked options
* Added 'l' to dungeon_items to have a local-world keysanity
MultiClient.py
* Has a Webbrowser based UI now
* Awaits a QUsb2Snes connection when started, latching on when available
* Completely redesigned command interface, with `!help` and `/help`
* Running it with a patch file will patch out the multiworld rom and then automatically connect to the host that created the multiworld
* Cheating is now controlled by the server and can be disabled in [host.yaml](https://github.com/Berserker66/MultiWorld-Utilities/blob/master/host.yaml)
* Automatically starts QUsb2Snes, if it isn't running
* Better reconnect to both snes and server

View File

@ -19,10 +19,8 @@ import os
import subprocess import subprocess
import sys import sys
import pickle import pickle
import io
import builtins
import functools import functools
import io
from yaml import load, dump, safe_load from yaml import load, dump, safe_load
@ -380,6 +378,11 @@ safe_builtins = {
'frozenset', 'frozenset',
} }
safe_builtins = {
'set',
'frozenset',
}
class RestrictedUnpickler(pickle.Unpickler): class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name): def find_class(self, module, name):

View File

@ -1,6 +1,3 @@
"""Friendly reminder that if you want to host this somewhere on the internet, that it's licensed under MIT Berserker66
So unless you're Berserker you need to include license information."""
import os import os
import uuid import uuid
import base64 import base64
@ -38,9 +35,9 @@ app.config["JOB_THRESHOLD"] = 2
app.config['SESSION_PERMANENT'] = True app.config['SESSION_PERMANENT'] = True
# waitress uses one thread for I/O, these are for processing of views that then get sent # waitress uses one thread for I/O, these are for processing of views that then get sent
# berserkermulti.world uses gunicorn + nginx; ignoring this option # archipelago.gg uses gunicorn + nginx; ignoring this option
app.config["WAITRESS_THREADS"] = 10 app.config["WAITRESS_THREADS"] = 10
# a default that just works. berserkermulti.world runs on mariadb # a default that just works. archipelago.gg runs on mariadb
app.config["PONY"] = { app.config["PONY"] = {
'provider': 'sqlite', 'provider': 'sqlite',
'filename': os.path.abspath('db.db3'), 'filename': os.path.abspath('db.db3'),
@ -51,7 +48,6 @@ app.config["CACHE_TYPE"] = "simple"
app.config["JSON_AS_ASCII"] = False app.config["JSON_AS_ASCII"] = False
app.autoversion = True app.autoversion = True
app.config["HOSTNAME"] = "berserkermulti.world"
av = Autoversion(app) av = Autoversion(app)
cache = Cache(app) cache = Cache(app)

View File

@ -8,7 +8,6 @@ import time
from pony.orm import db_session, select, commit from pony.orm import db_session, select, commit
from Utils import restricted_loads
class CommonLocker(): class CommonLocker():
"""Uses a file lock to signal that something is already running""" """Uses a file lock to signal that something is already running"""
@ -78,10 +77,10 @@ def handle_generation_failure(result: BaseException):
def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation): def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation):
options = restricted_loads(generation.options) options = generation.options
logging.info(f"Generating {generation.id} for {len(options)} players") logging.info(f"Generating {generation.id} for {len(options)} players")
meta = restricted_loads(generation.meta) meta = generation.meta
pool.apply_async(gen_game, (options,), pool.apply_async(gen_game, (options,),
{"race": meta["race"], "sid": generation.id, "owner": generation.owner}, {"race": meta["race"], "sid": generation.id, "owner": generation.owner},
handle_generation_success, handle_generation_failure) handle_generation_success, handle_generation_failure)

View File

@ -15,7 +15,7 @@ import zlib
from .models import * from .models import *
from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor
from Utils import get_public_ipv4, get_public_ipv6, restricted_loads from Utils import get_public_ipv4, get_public_ipv6, parse_yaml
class CustomClientMessageProcessor(ClientMessageProcessor): class CustomClientMessageProcessor(ClientMessageProcessor):
@ -75,7 +75,7 @@ class WebHostContext(Context):
else: else:
self.port = get_random_port() self.port = get_random_port()
return self._load(restricted_loads(zlib.decompress(room.seed.multidata)), True) return self._load(self._decompress(room.seed.multidata), True)
@db_session @db_session
def init_save(self, enabled: bool = True): def init_save(self, enabled: bool = True):

View File

@ -155,4 +155,4 @@ def upload_to_db(folder, owner, sid, race:bool):
gen.delete() gen.delete()
return seed.id return seed.id
else: else:
raise Exception("Multidata required, but not found.") raise Exception("Multidata required (.archipelago), but not found.")

View File

@ -51,6 +51,6 @@ class Command(db.Entity):
class Generation(db.Entity): class Generation(db.Entity):
id = PrimaryKey(UUID, default=uuid4) id = PrimaryKey(UUID, default=uuid4)
owner = Required(UUID) owner = Required(UUID)
options = Required(bytes, lazy=True) # these didn't work as JSON on mariaDB, so they're getting pickled now options = Required(Json, lazy=True)
meta = Required(bytes, lazy=True) # if state is -1 (error) this will contain an utf-8 encoded error message meta = Required(Json, lazy=True)
state = Required(int, default=0, index=True) state = Required(int, default=0, index=True)

View File

@ -10,7 +10,7 @@ from WebHostLib import app, Seed, Room, Patch
accepted_zip_contents = {"patches": ".apbp", accepted_zip_contents = {"patches": ".apbp",
"spoiler": ".txt", "spoiler": ".txt",
"multidata": "multidata"} "multidata": ".archipelago"}
banned_zip_contents = (".sfc",) banned_zip_contents = (".sfc",)
@ -43,7 +43,7 @@ def uploads():
patches.add(Patch(data=zfile.open(file, "r").read(), player=player)) patches.add(Patch(data=zfile.open(file, "r").read(), player=player))
elif file.filename.endswith(".txt"): elif file.filename.endswith(".txt"):
spoiler = zfile.open(file, "r").read().decode("utf-8-sig") spoiler = zfile.open(file, "r").read().decode("utf-8-sig")
elif file.filename.endswith("multidata"): elif file.filename.endswith(".archipelago"):
try: try:
multidata = json.loads(zlib.decompress(zfile.open(file).read()).decode("utf-8-sig")) multidata = json.loads(zlib.decompress(zfile.open(file).read()).decode("utf-8-sig"))
except: except:
@ -80,4 +80,4 @@ def user_content():
def allowed_file(filename): def allowed_file(filename):
return filename.endswith(('multidata', ".zip")) return filename.endswith(('.archipelago', ".zip"))

View File

@ -34,14 +34,12 @@ server_options:
# "enabled" -> clients can always forfeit # "enabled" -> clients can always forfeit
# "auto" -> automatic forfeit on goal completion, "goal" -> clients can forfeit after achieving their goal # "auto" -> automatic forfeit on goal completion, "goal" -> clients can forfeit after achieving their goal
# "auto-enabled" -> automatic forfeit on goal completion and manual forfeit is also enabled # "auto-enabled" -> automatic forfeit on goal completion and manual forfeit is also enabled
# Warning: Only Berserker's Multiworld clients of version 2.1+ send game beaten information
forfeit_mode: "goal" forfeit_mode: "goal"
# Remaining modes # Remaining modes
# !remaining handling, that tells a client which items remain in their pool # !remaining handling, that tells a client which items remain in their pool
# "enabled" -> Client can always ask for remaining items # "enabled" -> Client can always ask for remaining items
# "disabled" -> Client can never ask for remaining items # "disabled" -> Client can never ask for remaining items
# "goal" -> Client can ask for remaining items after goal completion # "goal" -> Client can ask for remaining items after goal completion
# Warning: Only Berserker's Multiworld clients of version 2.1+ send game beaten information
remaining_mode: "goal" remaining_mode: "goal"
# Automatically shut down the server after this many seconds without new location checks, 0 to keep running # Automatically shut down the server after this many seconds without new location checks, 0 to keep running
auto_shutdown: 0 auto_shutdown: 0

View File

@ -1,6 +1,6 @@
#define sourcepath "build\exe.win-amd64-3.8\" #define sourcepath "build\exe.win-amd64-3.8\"
#define MyAppName "BerserkerMultiWorld" #define MyAppName "Archipelago"
#define MyAppExeName "BerserkerMultiClient.exe" #define MyAppExeName "ArchipelagoClient.exe"
#define MyAppIcon "icon.ico" #define MyAppIcon "icon.ico"
[Setup] [Setup]
@ -11,7 +11,7 @@ AppName={#MyAppName}
AppVerName={#MyAppName} AppVerName={#MyAppName}
DefaultDirName={commonappdata}\{#MyAppName} DefaultDirName={commonappdata}\{#MyAppName}
DisableProgramGroupPage=yes DisableProgramGroupPage=yes
DefaultGroupName=Berserker's Multiworld DefaultGroupName=Archipelago
OutputDir=setups OutputDir=setups
OutputBaseFilename=Setup {#MyAppName} OutputBaseFilename=Setup {#MyAppName}
Compression=lzma2 Compression=lzma2
@ -52,7 +52,7 @@ Name: "{commondesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks:
[Run] [Run]
Filename: "{tmp}\vc_redist.x64.exe"; Parameters: "/passive /norestart"; Check: IsVCRedist64BitNeeded; StatusMsg: "Installing VC++ redistributable..." Filename: "{tmp}\vc_redist.x64.exe"; Parameters: "/passive /norestart"; Check: IsVCRedist64BitNeeded; StatusMsg: "Installing VC++ redistributable..."
Filename: "{app}\BerserkerMultiCreator"; Parameters: "update_sprites"; StatusMsg: "Updating Sprite Library..." Filename: "{app}\ArchipelagoCreator"; Parameters: "update_sprites"; StatusMsg: "Updating Sprite Library..."
[UninstallDelete] [UninstallDelete]
Type: dirifempty; Name: "{app}" Type: dirifempty; Name: "{app}"
@ -60,14 +60,14 @@ Type: dirifempty; Name: "{app}"
[Registry] [Registry]
Root: HKCR; Subkey: ".apbp"; ValueData: "{#MyAppName}patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "" Root: HKCR; Subkey: ".apbp"; ValueData: "{#MyAppName}patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""
Root: HKCR; Subkey: "{#MyAppName}patch"; ValueData: "Berserker's Multiworld Binary Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "" Root: HKCR; Subkey: "{#MyAppName}patch"; ValueData: "Archipelago Binary Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""
Root: HKCR; Subkey: "{#MyAppName}patch\DefaultIcon"; ValueData: "{app}\{#MyAppExeName},0"; ValueType: string; ValueName: "" Root: HKCR; Subkey: "{#MyAppName}patch\DefaultIcon"; ValueData: "{app}\{#MyAppExeName},0"; ValueType: string; ValueName: ""
Root: HKCR; Subkey: "{#MyAppName}patch\shell\open\command"; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; ValueType: string; ValueName: "" Root: HKCR; Subkey: "{#MyAppName}patch\shell\open\command"; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; ValueType: string; ValueName: ""
Root: HKCR; Subkey: ".multidata"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "" Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Berserker's Multiworld Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "" Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\BerserkerMultiServer.exe,0"; ValueType: string; ValueName: "" Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: ""
Root: HKCR; Subkey: "{#MyAppName}multidata\shell\open\command"; ValueData: """{app}\BerserkerMultiServer.exe"" --multidata ""%1"""; ValueType: string; ValueName: "" Root: HKCR; Subkey: "{#MyAppName}multidata\shell\open\command"; ValueData: """{app}\ArchipelagoServer.exe"" --multidata ""%1"""; ValueType: string; ValueName: ""

View File

@ -1,6 +1,6 @@
#define sourcepath "build\exe.win-amd64-3.9\" #define sourcepath "build\exe.win-amd64-3.9\"
#define MyAppName "BerserkerMultiWorld" #define MyAppName "Archipelago"
#define MyAppExeName "BerserkerMultiClient.exe" #define MyAppExeName "ArchipelagoClient.exe"
#define MyAppIcon "icon.ico" #define MyAppIcon "icon.ico"
[Setup] [Setup]
@ -11,7 +11,7 @@ AppName={#MyAppName}
AppVerName={#MyAppName} AppVerName={#MyAppName}
DefaultDirName={commonappdata}\{#MyAppName} DefaultDirName={commonappdata}\{#MyAppName}
DisableProgramGroupPage=yes DisableProgramGroupPage=yes
DefaultGroupName=Berserker's Multiworld DefaultGroupName=Archipelago
OutputDir=setups OutputDir=setups
OutputBaseFilename=Setup {#MyAppName} OutputBaseFilename=Setup {#MyAppName}
Compression=lzma2 Compression=lzma2
@ -52,7 +52,7 @@ Name: "{commondesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks:
[Run] [Run]
Filename: "{tmp}\vc_redist.x64.exe"; Parameters: "/passive /norestart"; Check: IsVCRedist64BitNeeded; StatusMsg: "Installing VC++ redistributable..." Filename: "{tmp}\vc_redist.x64.exe"; Parameters: "/passive /norestart"; Check: IsVCRedist64BitNeeded; StatusMsg: "Installing VC++ redistributable..."
Filename: "{app}\BerserkerMultiCreator"; Parameters: "update_sprites"; StatusMsg: "Updating Sprite Library..." Filename: "{app}\ArchipelagoCreator"; Parameters: "update_sprites"; StatusMsg: "Updating Sprite Library..."
[UninstallDelete] [UninstallDelete]
Type: dirifempty; Name: "{app}" Type: dirifempty; Name: "{app}"
@ -60,14 +60,14 @@ Type: dirifempty; Name: "{app}"
[Registry] [Registry]
Root: HKCR; Subkey: ".bmbp"; ValueData: "{#MyAppName}patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "" Root: HKCR; Subkey: ".bmbp"; ValueData: "{#MyAppName}patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""
Root: HKCR; Subkey: "{#MyAppName}patch"; ValueData: "Berserker's Multiworld Binary Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "" Root: HKCR; Subkey: "{#MyAppName}patch"; ValueData: "Archipelago Binary Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""
Root: HKCR; Subkey: "{#MyAppName}patch\DefaultIcon"; ValueData: "{app}\{#MyAppExeName},0"; ValueType: string; ValueName: "" Root: HKCR; Subkey: "{#MyAppName}patch\DefaultIcon"; ValueData: "{app}\{#MyAppExeName},0"; ValueType: string; ValueName: ""
Root: HKCR; Subkey: "{#MyAppName}patch\shell\open\command"; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; ValueType: string; ValueName: "" Root: HKCR; Subkey: "{#MyAppName}patch\shell\open\command"; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; ValueType: string; ValueName: ""
Root: HKCR; Subkey: ".multidata"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "" Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Berserker's Multiworld Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "" Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\BerserkerMultiServer.exe,0"; ValueType: string; ValueName: "" Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: ""
Root: HKCR; Subkey: "{#MyAppName}multidata\shell\open\command"; ValueData: """{app}\BerserkerMultiServer.exe"" --multidata ""%1"""; ValueType: string; ValueName: "" Root: HKCR; Subkey: "{#MyAppName}multidata\shell\open\command"; ValueData: """{app}\ArchipelagoServer.exe"" --multidata ""%1"""; ValueType: string; ValueName: ""

View File

@ -54,11 +54,11 @@ def manifest_creation():
print("Created Manifest") print("Created Manifest")
scripts = {"MultiClient.py": "BerserkerMultiClient", scripts = {"MultiClient.py": "ArchipelagoClient",
"MultiMystery.py": "BerserkerMultiMystery", "MultiMystery.py": "ArchipelagoMultiMystery",
"MultiServer.py": "BerserkerMultiServer", "MultiServer.py": "ArchipelagoServer",
"gui.py": "BerserkerMultiCreator", "gui.py": "ArchipelagoCreator",
"Mystery.py": "BerserkerMystery"} "Mystery.py": "ArchipelagoMystery"}
exes = [] exes = []
@ -74,9 +74,9 @@ import datetime
buildtime = datetime.datetime.utcnow() buildtime = datetime.datetime.utcnow()
cx_Freeze.setup( cx_Freeze.setup(
name="BerserkerMultiWorld", name="Archipelago",
version=f"{buildtime.year}.{buildtime.month}.{buildtime.day}.{buildtime.hour}", version=f"{buildtime.year}.{buildtime.month}.{buildtime.day}.{buildtime.hour}",
description="BerserkerMultiWorld", description="Archipelago",
executables=exes, executables=exes,
options={ options={
"build_exe": { "build_exe": {

View File

@ -7,12 +7,14 @@ import random
import time import time
import zlib import zlib
import concurrent.futures import concurrent.futures
import pickle
from BaseClasses import MultiWorld, CollectionState, Item, Region, Location from BaseClasses import MultiWorld, CollectionState, Item, Region, Location
from worlds.alttp.Items import ItemFactory from worlds.alttp.Items import ItemFactory, item_table, item_name_groups
from worlds.alttp.Regions import create_regions, create_shops, mark_light_world_regions, lookup_vanilla_location_to_entrance from worlds.alttp.Regions import create_regions, create_shops, mark_light_world_regions, \
lookup_vanilla_location_to_entrance
from worlds.alttp.InvertedRegions import create_inverted_regions, mark_dark_world_regions from worlds.alttp.InvertedRegions import create_inverted_regions, mark_dark_world_regions
from worlds.alttp.EntranceShuffle import link_entrances, link_inverted_entrances from worlds.alttp.EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect
from worlds.alttp.Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, get_hash_string from worlds.alttp.Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, get_hash_string
from worlds.alttp.Rules import set_rules from worlds.alttp.Rules import set_rules
from worlds.alttp.Dungeons import create_dungeons, fill_dungeons, fill_dungeons_restrictive from worlds.alttp.Dungeons import create_dungeons, fill_dungeons, fill_dungeons_restrictive
@ -94,7 +96,7 @@ def main(args, seed=None):
world.rom_seeds = {player: random.Random(world.random.randint(0, 999999999)) for player in range(1, world.players + 1)} world.rom_seeds = {player: random.Random(world.random.randint(0, 999999999)) for player in range(1, world.players + 1)}
logger.info('ALttP Berserker\'s Multiworld Version %s - Seed: %s\n', __version__, world.seed) logger.info('Archipelago Version %s - Seed: %s\n', __version__, world.seed)
parsed_names = parse_player_names(args.names, world.players, args.teams) parsed_names = parse_player_names(args.names, world.players, args.teams)
world.teams = len(parsed_names) world.teams = len(parsed_names)
@ -397,17 +399,17 @@ def main(args, seed=None):
def write_multidata(roms): def write_multidata(roms):
import base64 import base64
import pickle
for future in roms: for future in roms:
rom_name = future.result() rom_name = future.result()
rom_names.append(rom_name) rom_names.append(rom_name)
minimum_versions = {"server": (1, 0, 0)} minimum_versions = {"server": (0, 1, 0)}
multidata = zlib.compress(pickle.dumps({"names": parsed_names, multidata = zlib.compress(pickle.dumps({"names": parsed_names,
"roms": {base64.b64encode(rom_name).decode(): (team, slot) for slot, team, rom_name in rom_names}, "roms": {base64.b64encode(rom_name).decode(): (team, slot) for
slot, team, rom_name in rom_names},
"remote_items": {player for player in range(1, world.players + 1) if "remote_items": {player for player in range(1, world.players + 1) if
world.remote_items[player]}, world.remote_items[player]},
"locations": { "locations": {
(location.address, location.player) : (location.address, location.player):
(location.item.code, location.item.player) (location.item.code, location.item.player)
for location in world.get_filled_locations() if for location in world.get_filled_locations() if
type(location.address) is int}, type(location.address) is int},
@ -415,12 +417,13 @@ def main(args, seed=None):
"server_options": get_options()["server_options"], "server_options": get_options()["server_options"],
"er_hint_data": er_hint_data, "er_hint_data": er_hint_data,
"precollected_items": precollected_items, "precollected_items": precollected_items,
"version": _version_tuple, "version": tuple(_version_tuple),
"tags": ["AP"], "tags": ["AP"],
"minimum_versions": minimum_versions, "minimum_versions": minimum_versions,
}), 9) }), 9)
with open(output_path('%s.multidata' % outfilebase), 'wb') as f: with open(output_path('%s.archipelago' % outfilebase), 'wb') as f:
f.write(bytes([1])) # version of format
f.write(multidata) f.write(multidata)
multidata_task = pool.submit(write_multidata, rom_futures) multidata_task = pool.submit(write_multidata, rom_futures)

View File

@ -0,0 +1,14 @@
from typing import NamedTuple, Union
class PlandoItem(NamedTuple):
item: str
location: str
world: Union[bool, str] = False # False -> own world, True -> not own world
from_pool: bool = True # if item should be removed from item pool
class PlandoConnection(NamedTuple):
entrance: str
exit: str
direction: str # entrance, exit or both