Compare commits

..

5 Commits

Author SHA1 Message Date
CaitSith2
87152f17f5 Merge branch 'main' into show_all_hints 2022-07-02 06:56:15 -07:00
CaitSith2
c1b099d44e Merge branch 'main' into show_all_hints 2022-06-27 02:19:24 -07:00
CaitSith2
d20ade7ff8 Automatically allow spectator slots to see all hints. 2022-06-22 16:18:36 -07:00
CaitSith2
df90ff4ddb Don't need this line. 2022-06-22 04:53:38 -07:00
CaitSith2
a798e8aea2 Add a means to allow a client to opt into seeing ALL hints. 2022-06-22 04:49:31 -07:00
259 changed files with 6057 additions and 15612 deletions

View File

@@ -17,7 +17,7 @@ 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/v0.0.81/sni-v0.0.81-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
Expand-Archive -Path enemizer.zip -DestinationPath EnemizerCLI -Force
@@ -63,7 +63,7 @@ jobs:
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/v0.0.81/sni-v0.0.81-manylinux2014-amd64.tar.xz
tar xf sni-*.tar.xz
rm sni-*.tar.xz
mv sni-* SNI

View File

@@ -51,7 +51,7 @@ jobs:
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/v0.0.81/sni-v0.0.81-manylinux2014-amd64.tar.xz
tar xf sni-*.tar.xz
rm sni-*.tar.xz
mv sni-* SNI

14
.gitignore vendored
View File

@@ -116,9 +116,6 @@ target/
profile_default/
ipython_config.py
# vim editor
*.swp
# SageMath parsed files
*.sage.py
@@ -155,17 +152,10 @@ dmypy.json
# Cython debug symbols
cython_debug/
# minecraft server stuff
#minecraft server stuff
jdk*/
minecraft*/
minecraft_versions.json
# pyenv
#pyenv
.python-version
# OS General Files
.DS_Store
.AppleDouble
.LSOverride
Thumbs.db
[Dd]esktop.ini

View File

@@ -126,6 +126,7 @@ class MultiWorld():
set_player_attr('beemizer_total_chance', 0)
set_player_attr('beemizer_trap_chance', 0)
set_player_attr('escape_assist', [])
set_player_attr('open_pyramid', False)
set_player_attr('treasure_hunt_icon', 'Triforce Piece')
set_player_attr('treasure_hunt_count', 0)
set_player_attr('clock_mode', False)
@@ -166,7 +167,7 @@ class MultiWorld():
self.player_types[new_id] = NetUtils.SlotType.group
self._region_cache[new_id] = {}
world_type = AutoWorld.AutoWorldRegister.world_types[game]
for option_key, option in world_type.option_definitions.items():
for option_key, option in world_type.options.items():
getattr(self, option_key)[new_id] = option(option.default)
for option_key, option in Options.common_options.items():
getattr(self, option_key)[new_id] = option(option.default)
@@ -204,7 +205,7 @@ class MultiWorld():
for player in self.player_ids:
self.custom_data[player] = {}
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]]
for option_key in world_type.option_definitions:
for option_key in world_type.options:
setattr(self, option_key, getattr(args, option_key, {}))
self.worlds[player] = world_type(self, player)
@@ -384,17 +385,25 @@ class MultiWorld():
return self.worlds[player].create_item(item_name)
def push_precollected(self, item: Item):
item.world = self
self.precollected_items[item.player].append(item)
self.state.collect(item, True)
def push_item(self, location: Location, item: Item, collect: bool = True):
assert location.can_fill(self.state, item, False), f"Cannot place {item} into {location}."
location.item = item
item.location = location
if collect:
self.state.collect(item, location.event, location)
if not isinstance(location, Location):
raise RuntimeError(
'Cannot assign item %s to invalid location %s (player %d).' % (item, location, item.player))
logging.debug('Placed %s at %s', item, location)
if location.can_fill(self.state, item, False):
location.item = item
item.location = location
item.world = self # try to not have this here anymore
if collect:
self.state.collect(item, location.event, location)
logging.debug('Placed %s at %s', item, location)
else:
raise RuntimeError('Cannot assign item %s to location %s.' % (item, location))
def get_entrances(self) -> List[Entrance]:
if self._cached_entrances is None:
@@ -1064,25 +1073,26 @@ class LocationProgressType(IntEnum):
class Location:
game: str = "Generic"
player: int
name: str
address: Optional[int]
parent_region: Optional[Region]
# If given as integer, then this is the shop's inventory index
shop_slot: Optional[int] = None
shop_slot_disabled: bool = False
event: bool = False
locked: bool = False
game: str = "Generic"
show_in_spoiler: bool = True
crystal: bool = False
progress_type: LocationProgressType = LocationProgressType.DEFAULT
always_allow = staticmethod(lambda item, state: False)
access_rule = staticmethod(lambda state: True)
item_rule = staticmethod(lambda item: True)
item: Optional[Item] = None
parent_region: Optional[Region]
def __init__(self, player: int, name: str = '', address: Optional[int] = None, parent: Optional[Region] = None):
self.player = player
self.name = name
self.address = address
def __init__(self, player: int, name: str = '', address: int = None, parent=None):
self.name: str = name
self.address: Optional[int] = address
self.parent_region = parent
self.player: int = player
def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool:
return self.always_allow(state, item) or (self.item_rule(item) and (not check_access or self.can_reach(state)))
@@ -1099,6 +1109,7 @@ class Location:
self.item = item
item.location = self
self.event = item.advancement
self.item.world = self.parent_region.world
self.locked = True
def __repr__(self):
@@ -1143,28 +1154,39 @@ class ItemClassification(IntFlag):
class Item:
game: str = "Generic"
__slots__ = ("name", "classification", "code", "player", "location")
location: Optional[Location] = None
world: Optional[MultiWorld] = None
code: Optional[int] = None # an item with ID None is called an Event, and does not get written to multidata
name: str
game: str = "Generic"
type: str = None
classification: ItemClassification
code: Optional[int]
"""an item with code None is called an Event, and does not get written to multidata"""
player: int
location: Optional[Location]
# need to find a decent place for these to live and to allow other games to register texts if they want.
pedestal_credit_text: str = "and the Unknown Item"
sickkid_credit_text: Optional[str] = None
magicshop_credit_text: Optional[str] = None
zora_credit_text: Optional[str] = None
fluteboy_credit_text: Optional[str] = None
# hopefully temporary attributes to satisfy legacy LttP code, proper implementation in subclass ALttPItem
smallkey: bool = False
bigkey: bool = False
map: bool = False
compass: bool = False
def __init__(self, name: str, classification: ItemClassification, code: Optional[int], player: int):
self.name = name
self.classification = classification
self.player = player
self.code = code
self.location = None
@property
def hint_text(self) -> str:
def hint_text(self):
return getattr(self, "_hint_text", self.name.replace("_", " ").replace("-", " "))
@property
def pedestal_hint_text(self) -> str:
def pedestal_hint_text(self):
return getattr(self, "_pedestal_hint_text", self.name.replace("_", " ").replace("-", " "))
@property
@@ -1190,7 +1212,7 @@ class Item:
def __eq__(self, other):
return self.name == other.name and self.player == other.player
def __lt__(self, other: Item) -> bool:
def __lt__(self, other: Item):
if other.player != self.player:
return other.player < self.player
return self.name < other.name
@@ -1198,13 +1220,11 @@ class Item:
def __hash__(self):
return hash((self.name, self.player))
def __repr__(self) -> str:
def __repr__(self):
return self.__str__()
def __str__(self) -> str:
if self.location and self.location.parent_region and self.location.parent_region.world:
return self.location.parent_region.world.get_name_string_for_object(self)
return f"{self.name} (Player {self.player})"
def __str__(self):
return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})'
class Spoiler():
@@ -1388,7 +1408,7 @@ class Spoiler():
outfile.write('Game: %s\n' % self.world.game[player])
for f_option, option in Options.per_game_common_options.items():
write_option(f_option, option)
options = self.world.worlds[player].option_definitions
options = self.world.worlds[player].options
if options:
for f_option, option in options.items():
write_option(f_option, option)
@@ -1411,6 +1431,8 @@ class Spoiler():
outfile.write('Entrance Shuffle: %s\n' % self.world.shuffle[player])
if self.world.shuffle[player] != "vanilla":
outfile.write('Entrance Shuffle Seed %s\n' % self.world.worlds[player].er_seed)
outfile.write('Pyramid hole pre-opened: %s\n' % (
'Yes' if self.world.open_pyramid[player] else 'No'))
outfile.write('Shop inventory shuffle: %s\n' %
bool_to_text("i" in self.world.shop_shuffle[player]))
outfile.write('Shop price shuffle: %s\n' %

View File

@@ -1,8 +1,6 @@
from __future__ import annotations
import os
import sys
import asyncio
import shutil
import ModuleUpdate
ModuleUpdate.update()
@@ -34,24 +32,6 @@ class ChecksFinderContext(CommonContext):
self.send_index: int = 0
self.syncing = False
self.awaiting_bridge = False
# self.game_communication_path: files go in this path to pass data between us and the actual game
if "localappdata" in os.environ:
self.game_communication_path = os.path.expandvars(r"%localappdata%/ChecksFinder")
else:
# not windows. game is an exe so let's see if wine might be around to run it
if "WINEPREFIX" in os.environ:
wineprefix = os.environ["WINEPREFIX"]
elif shutil.which("wine") or shutil.which("wine-stable"):
wineprefix = os.path.expanduser("~/.wine") # default root of wine system data, deep in which is app data
else:
msg = "ChecksFinderClient couldn't detect system type. Unable to infer required game_communication_path"
logger.error("Error: " + msg)
Utils.messagebox("Error", msg, error=True)
sys.exit(1)
self.game_communication_path = os.path.join(
wineprefix,
"drive_c",
os.path.expandvars("users/$USER/Local Settings/Application Data/ChecksFinder"))
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
@@ -61,7 +41,8 @@ class ChecksFinderContext(CommonContext):
async def connection_closed(self):
await super(ChecksFinderContext, self).connection_closed()
for root, dirs, files in os.walk(self.game_communication_path):
path = os.path.expandvars(r"%localappdata%/ChecksFinder")
for root, dirs, files in os.walk(path):
for file in files:
if file.find("obtain") <= -1:
os.remove(root + "/" + file)
@@ -75,25 +56,26 @@ class ChecksFinderContext(CommonContext):
async def shutdown(self):
await super(ChecksFinderContext, self).shutdown()
for root, dirs, files in os.walk(self.game_communication_path):
path = os.path.expandvars(r"%localappdata%/ChecksFinder")
for root, dirs, files in os.walk(path):
for file in files:
if file.find("obtain") <= -1:
os.remove(root+"/"+file)
def on_package(self, cmd: str, args: dict):
if cmd in {"Connected"}:
if not os.path.exists(self.game_communication_path):
os.makedirs(self.game_communication_path)
if not os.path.exists(os.path.expandvars(r"%localappdata%/ChecksFinder")):
os.mkdir(os.path.expandvars(r"%localappdata%/ChecksFinder"))
for ss in self.checked_locations:
filename = f"send{ss}"
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
with open(os.path.expandvars(r"%localappdata%/ChecksFinder/" + filename), 'w') as f:
f.close()
if cmd in {"ReceivedItems"}:
start_index = args["index"]
if start_index != len(self.items_received):
for item in args['items']:
filename = f"AP_{str(NetworkItem(*item).location)}PLR{str(NetworkItem(*item).player)}.item"
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
with open(os.path.expandvars(r"%localappdata%/ChecksFinder/" + filename), 'w') as f:
f.write(str(NetworkItem(*item).item))
f.close()
@@ -101,7 +83,7 @@ class ChecksFinderContext(CommonContext):
if "checked_locations" in args:
for ss in self.checked_locations:
filename = f"send{ss}"
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
with open(os.path.expandvars(r"%localappdata%/ChecksFinder/" + filename), 'w') as f:
f.close()
def run_gui(self):
@@ -127,9 +109,10 @@ async def game_watcher(ctx: ChecksFinderContext):
sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)})
await ctx.send_msgs(sync_msg)
ctx.syncing = False
path = os.path.expandvars(r"%localappdata%/ChecksFinder")
sending = []
victory = False
for root, dirs, files in os.walk(ctx.game_communication_path):
for root, dirs, files in os.walk(path):
for file in files:
if file.find("send") > -1:
st = file.split("send", -1)[1]

View File

@@ -110,6 +110,10 @@ class ClientCommandProcessor(CommandProcessor):
self.output("Unreadied.")
asyncio.create_task(self.ctx.send_msgs([{"cmd": "StatusUpdate", "status": state}]), name="send StatusUpdate")
def _cmd_show_all_hints(self):
"""Allows the player to see all hints, not just the ones that apply to them."""
asyncio.create_task(self.ctx.update_show_all_hints("ShowAllHints" not in self.ctx.tags))
def default(self, raw: str):
raw = self.ctx.on_user_say(raw)
if raw:
@@ -402,6 +406,15 @@ class CommonContext:
}
}])
async def update_show_all_hints(self, show_all_hints: bool):
old_tags = self.tags.copy()
if show_all_hints:
self.tags.add("ShowAllHints")
else:
self.tags -= {"ShowAllHints"}
if old_tags != self.tags and self.server and not self.server.socket.closed:
await self.send_msgs([{"cmd": "ConnectUpdate", "tags": self.tags}])
async def update_death_link(self, death_link: bool):
old_tags = self.tags.copy()
if death_link:
@@ -493,8 +506,7 @@ async def server_loop(ctx: CommonContext, address=None):
logger.info(f'Connecting to Archipelago server at {address}')
try:
socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None)
if ctx.ui is not None:
ctx.ui.update_address_bar(server_url.netloc)
ctx.ui.update_address_bar(server_url.netloc)
ctx.server = Endpoint(socket)
logger.info('Connected')
ctx.server_address = address
@@ -563,21 +575,18 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
f" for each location checked. Use !hint for more information.")
ctx.hint_cost = int(args['hint_cost'])
ctx.check_points = int(args['location_check_points'])
if "players" in args: # TODO remove when servers sending this are outdated
players = args.get("players", [])
if len(players) < 1:
logger.info('No player connected')
else:
players.sort()
current_team = -1
logger.info('Connected Players:')
for network_player in players:
if network_player.team != current_team:
logger.info(f' Team #{network_player.team + 1}')
current_team = network_player.team
logger.info(' %s (Player %d)' % (network_player.alias, network_player.slot))
players = args.get("players", [])
if len(players) < 1:
logger.info('No player connected')
else:
players.sort()
current_team = -1
logger.info('Connected Players:')
for network_player in players:
if network_player.team != current_team:
logger.info(f' Team #{network_player.team + 1}')
current_team = network_player.team
logger.info(' %s (Player %d)' % (network_player.alias, network_player.slot))
# update datapackage
await ctx.prepare_datapackage(set(args["games"]), args["datapackage_versions"])
@@ -727,7 +736,7 @@ if __name__ == '__main__':
class TextContext(CommonContext):
tags = {"AP", "IgnoreGame", "TextOnly"}
game = "" # empty matches any game since 0.3.2
items_handling = 0b111 # receive all items for /received
items_handling = 0 # don't receive any NetworkItems
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:

View File

@@ -20,7 +20,8 @@ import Utils
if __name__ == "__main__":
Utils.init_logging("FactorioClient", exception_logger="Client")
from CommonClient import CommonContext, server_loop, ClientCommandProcessor, logger, gui_enabled, get_base_parser
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, logger, gui_enabled, \
get_base_parser
from MultiServer import mark_raw
from NetUtils import NetworkItem, ClientStatus, JSONtoTextParser, JSONMessagePart
@@ -399,7 +400,6 @@ if __name__ == '__main__':
"Refer to Factorio --help for those.")
parser.add_argument('--rcon-port', default='24242', type=int, help='Port to use to communicate with Factorio')
parser.add_argument('--rcon-password', help='Password to authenticate with RCON.')
parser.add_argument('--server-settings', help='Factorio server settings configuration file.')
args, rest = parser.parse_known_args()
colorama.init()
@@ -410,9 +410,6 @@ if __name__ == '__main__':
factorio_server_logger = logging.getLogger("FactorioServer")
options = Utils.get_options()
executable = options["factorio_options"]["executable"]
server_settings = args.server_settings if args.server_settings else options["factorio_options"].get("server_settings", None)
if server_settings:
server_settings = os.path.abspath(server_settings)
if not os.path.exists(os.path.dirname(executable)):
raise FileNotFoundError(f"Path {os.path.dirname(executable)} does not exist or could not be accessed.")
@@ -424,10 +421,7 @@ if __name__ == '__main__':
else:
raise FileNotFoundError(f"Path {executable} is not an executable file.")
if server_settings and os.path.isfile(server_settings):
server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password, "--server-settings", server_settings, *rest)
else:
server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password, *rest)
server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password, *rest)
asyncio.run(main(args))
colorama.deinit()

16
Fill.py
View File

@@ -42,16 +42,8 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
has_beaten_game = world.has_beaten_game(maximum_exploration_state)
while items_to_place:
# if we have run out of locations to fill,break out of this loop
if not locations:
unplaced_items += items_to_place
break
item_to_place = items_to_place.pop(0)
for item_to_place in items_to_place:
spot_to_fill: typing.Optional[Location] = None
# if minimal accessibility, only check whether location is reachable if game not beatable
if world.accessibility[item_to_place.player] == 'minimal':
perform_access_check = not world.has_beaten_game(maximum_exploration_state,
item_to_place.player) \
@@ -62,7 +54,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
for i, location in enumerate(locations):
if (not single_player_placement or location.player == item_to_place.player) \
and location.can_fill(maximum_exploration_state, item_to_place, perform_access_check):
# popping by index is faster than removing by content,
# poping by index is faster than removing by content,
spot_to_fill = locations.pop(i)
# skipping a scan for the element
break
@@ -220,8 +212,8 @@ def distribute_items_restrictive(world: MultiWorld) -> None:
world.push_item(defaultlocations.pop(i), item_to_place, False)
break
else:
raise Exception(f"Could not place non_local_item {item_to_place} among {defaultlocations}. "
f"Too many non-local items for too few remaining locations.")
logging.warning(
f"Could not place non_local_item {item_to_place} among {defaultlocations}, tossing.")
world.random.shuffle(defaultlocations)

View File

@@ -1,5 +1,3 @@
from __future__ import annotations
import argparse
import logging
import random
@@ -7,9 +5,8 @@ import urllib.request
import urllib.parse
from typing import Set, Dict, Tuple, Callable, Any, Union
import os
from collections import Counter, ChainMap
from collections import Counter
import string
import enum
import ModuleUpdate
@@ -28,43 +25,7 @@ from worlds.alttp.Text import TextTable
from worlds.AutoWorld import AutoWorldRegister
import copy
class PlandoSettings(enum.IntFlag):
items = 0b0001
connections = 0b0010
texts = 0b0100
bosses = 0b1000
@classmethod
def from_option_string(cls, option_string: str) -> PlandoSettings:
result = cls(0)
for part in option_string.split(","):
part = part.strip().lower()
if part:
result = cls._handle_part(part, result)
return result
@classmethod
def from_set(cls, option_set: Set[str]) -> PlandoSettings:
result = cls(0)
for part in option_set:
result = cls._handle_part(part, result)
return result
@classmethod
def _handle_part(cls, part: str, base: PlandoSettings) -> PlandoSettings:
try:
part = cls[part]
except Exception as e:
raise KeyError(f"{part} is not a recognized name for a plando module. "
f"Known options: {', '.join(flag.name for flag in cls)}") from e
else:
return base | part
def __str__(self) -> str:
if self.value:
return ", ".join((flag.name for flag in PlandoSettings if self.value & flag.value))
return "Off"
categories = set(AutoWorldRegister.world_types)
def mystery_argparse():
@@ -103,7 +64,7 @@ def mystery_argparse():
args.weights_file_path = os.path.join(args.player_files_path, args.weights_file_path)
if not os.path.isabs(args.meta_file_path):
args.meta_file_path = os.path.join(args.player_files_path, args.meta_file_path)
args.plando: PlandoSettings = PlandoSettings.from_option_string(args.plando)
args.plando: Set[str] = {arg.strip().lower() for arg in args.plando.split(",")}
return args, options
@@ -133,14 +94,12 @@ def main(args=None, callback=ERmain):
if args.meta_file_path and os.path.exists(args.meta_file_path):
try:
meta_weights = read_weights_yamls(args.meta_file_path)[-1]
weights_cache[args.meta_file_path] = read_weights_yamls(args.meta_file_path)
except Exception as e:
raise ValueError(f"File {args.meta_file_path} is destroyed. Please fix your yaml.") from e
meta_weights = weights_cache[args.meta_file_path][-1]
print(f"Meta: {args.meta_file_path} >> {get_choice('meta_description', meta_weights)}")
try: # meta description allows us to verify that the file named meta.yaml is intentionally a meta file
del(meta_weights["meta_description"])
except Exception as e:
raise ValueError("No meta description found for meta.yaml. Unable to verify.") from e
del(meta_weights["meta_description"])
if args.samesettings:
raise Exception("Cannot mix --samesettings with --meta")
else:
@@ -166,9 +125,9 @@ def main(args=None, callback=ERmain):
player_files[player_id] = filename
player_id += 1
args.multi = max(player_id - 1, args.multi)
args.multi = max(player_id-1, args.multi)
print(f"Generating for {args.multi} player{'s' if args.multi > 1 else ''}, {seed_name} Seed {seed} with plando: "
f"{args.plando}")
f"{', '.join(args.plando)}")
if not weights_cache:
raise Exception(f"No weights found. Provide a general weights file ({args.weights_file_path}) or individual player files. "
@@ -188,28 +147,26 @@ def main(args=None, callback=ERmain):
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()}
{fname: ( tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.samesettings else None)
for fname, yamls in weights_cache.items()}
player_path_cache = {}
for player in range(1, args.multi + 1):
player_path_cache[player] = player_files.get(player, args.weights_file_path)
if meta_weights:
for category_name, category_dict in meta_weights.items():
for key in category_dict:
option = roll_meta_option(key, category_name, category_dict)
option = get_choice(key, category_dict)
if option is not None:
for path in weights_cache:
for player, path in player_path_cache.items():
for yaml in weights_cache[path]:
if category_name is None:
for category in yaml:
if category in AutoWorldRegister.world_types and key in Options.common_options:
yaml[category][key] = option
yaml[key] = option
elif category_name not in yaml:
logging.warning(f"Meta: Category {category_name} is not present in {path}.")
else:
yaml[category_name][key] = option
yaml[category_name][key] = option
player_path_cache = {}
for player in range(1, args.multi + 1):
player_path_cache[player] = player_files.get(player, args.weights_file_path)
name_counter = Counter()
erargs.player_settings = {}
@@ -391,28 +348,6 @@ def update_weights(weights: dict, new_weights: dict, type: str, name: str) -> di
return weights
def roll_meta_option(option_key, game: str, category_dict: Dict) -> Any:
if not game:
return get_choice(option_key, category_dict)
if game in AutoWorldRegister.world_types:
game_world = AutoWorldRegister.world_types[game]
options = ChainMap(game_world.option_definitions, Options.per_game_common_options)
if option_key in options:
if options[option_key].supports_weighting:
return get_choice(option_key, category_dict)
return options[option_key]
if game == "A Link to the Past": # TODO wow i hate this
if option_key in {"glitches_required", "dark_room_logic", "entrance_shuffle", "goals", "triforce_pieces_mode",
"triforce_pieces_percentage", "triforce_pieces_available", "triforce_pieces_extra",
"triforce_pieces_required", "shop_shuffle", "mode", "item_pool", "item_functionality",
"boss_shuffle", "enemy_damage", "enemy_health", "timer", "countdown_start_time",
"red_clock_time", "blue_clock_time", "green_clock_time", "dungeon_counters", "shuffle_prizes",
"misery_mire_medallion", "turtle_rock_medallion", "sprite_pool", "sprite",
"random_sprite_on_event"}:
return get_choice(option_key, category_dict)
raise Exception(f"Error generating meta option {option_key} for {game}.")
def roll_linked_options(weights: dict) -> dict:
weights = copy.deepcopy(weights) # make sure we don't write back to other weights sets in same_settings
for option_set in weights["linked_options"]:
@@ -468,7 +403,7 @@ def roll_triggers(weights: dict, triggers: list) -> dict:
def get_plando_bosses(boss_shuffle: str, plando_options: Set[str]) -> str:
if boss_shuffle in boss_shuffle_options:
return boss_shuffle_options[boss_shuffle]
elif PlandoSettings.bosses in plando_options:
elif "bosses" in plando_options:
options = boss_shuffle.lower().split(";")
remainder_shuffle = "none" # vanilla
bosses = []
@@ -517,7 +452,7 @@ def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str,
setattr(ret, option_key, option(option.default))
def roll_settings(weights: dict, plando_options: PlandoSettings = PlandoSettings.bosses):
def roll_settings(weights: dict, plando_options: Set[str] = frozenset(("bosses",))):
if "linked_options" in weights:
weights = roll_linked_options(weights)
@@ -530,11 +465,17 @@ def roll_settings(weights: dict, plando_options: PlandoSettings = PlandoSettings
if tuplize_version(version) > version_tuple:
raise Exception(f"Settings reports required version of generator is at least {version}, "
f"however generator is of version {__version__}")
required_plando_options = PlandoSettings.from_option_string(requirements.get("plando", ""))
if required_plando_options not in plando_options:
required_plando_options = requirements.get("plando", "")
if required_plando_options:
required_plando_options = set(option.strip() for option in required_plando_options.split(","))
required_plando_options -= plando_options
if required_plando_options:
raise Exception(f"Settings reports required plando module {str(required_plando_options)}, "
f"which is not enabled.")
if len(required_plando_options) == 1:
raise Exception(f"Settings reports required plando module {', '.join(required_plando_options)}, "
f"which is not enabled.")
else:
raise Exception(f"Settings reports required plando modules {', '.join(required_plando_options)}, "
f"which are not enabled.")
ret = argparse.Namespace()
for option_key in Options.per_game_common_options:
@@ -557,18 +498,18 @@ def roll_settings(weights: dict, plando_options: PlandoSettings = PlandoSettings
setattr(ret, option_key, option.from_any(get_choice(option_key, weights, option.default)))
if ret.game in AutoWorldRegister.world_types:
for option_key, option in world_type.option_definitions.items():
for option_key, option in world_type.options.items():
handle_option(ret, game_weights, option_key, option)
for option_key, option in Options.per_game_common_options.items():
# skip setting this option if already set from common_options, defaulting to root option
if not (option_key in Options.common_options and option_key not in game_weights):
handle_option(ret, game_weights, option_key, option)
if PlandoSettings.items in plando_options:
if "items" in plando_options:
ret.plando_items = game_weights.get("plando_items", [])
if ret.game == "Minecraft" or ret.game == "Ocarina of Time":
# bad hardcoded behavior to make this work for now
ret.plando_connections = []
if PlandoSettings.connections in plando_options:
if "connections" in plando_options:
options = game_weights.get("plando_connections", [])
for placement in options:
if roll_percentage(get_choice("percentage", placement, 100)):
@@ -614,6 +555,9 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
ret.goal = goals[goal]
# TODO consider moving open_pyramid to an automatic variable in the core roller, set to True when
# fast ganon + ganon at hole
ret.open_pyramid = get_choice_legacy('open_pyramid', weights, 'goal')
extra_pieces = get_choice_legacy('triforce_pieces_mode', weights, 'available')
@@ -685,7 +629,7 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
raise Exception(f"unknown Medallion {medallion} for {'misery mire' if index == 0 else 'turtle rock'}")
ret.plando_texts = {}
if PlandoSettings.texts in plando_options:
if "texts" in plando_options:
tt = TextTable()
tt.removeUnwantedText()
options = weights.get("plando_texts", [])
@@ -697,7 +641,7 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
ret.plando_texts[at] = str(get_choice_legacy("text", placement))
ret.plando_connections = []
if PlandoSettings.connections in plando_options:
if "connections" in plando_options:
options = weights.get("plando_connections", [])
for placement in options:
if roll_percentage(get_choice_legacy("percentage", placement, 100)):

View File

@@ -126,7 +126,7 @@ components: Iterable[Component] = (
Component('Text Client', 'CommonClient', 'ArchipelagoTextClient'),
# SNI
Component('SNI Client', 'SNIClient',
file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3')),
file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3')),
Component('LttP Adjuster', 'LttPAdjuster'),
# Factorio
Component('Factorio Client', 'FactorioClient'),

View File

@@ -289,7 +289,7 @@ def run_sprite_update():
else:
top.withdraw()
task = BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set())
while not done.is_set():
while not done.isSet():
task.do_events()
logging.info("Done updating sprites")
@@ -300,7 +300,6 @@ def update_sprites(task, on_finish=None):
sprite_dir = user_path("data", "sprites", "alttpr")
os.makedirs(sprite_dir, exist_ok=True)
ctx = get_cert_none_ssl_context()
def finished():
task.close_window()
if on_finish:

View File

@@ -47,6 +47,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
world.item_functionality = args.item_functionality.copy()
world.timer = args.timer.copy()
world.goal = args.goal.copy()
world.open_pyramid = args.open_pyramid.copy()
world.boss_shuffle = args.shufflebosses.copy()
world.enemy_health = args.enemy_health.copy()
world.enemy_damage = args.enemy_damage.copy()
@@ -217,6 +218,9 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
logger.info("Running Item Plando")
for item in world.itempool:
item.world = world
distribute_planned(world)
logger.info('Running Pre Main Fill.')
@@ -360,8 +364,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
for location in world.get_filled_locations():
if type(location.address) == int:
assert location.item.code is not None, "item code None should be event, " \
"location.address should then also be None. Location: " \
f" {location}"
"location.address should then also be None"
locations_data[location.player][location.address] = \
location.item.code, location.item.player, location.item.flags
if location.name in world.start_location_hints[location.player]:
@@ -423,7 +426,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
world.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase))
zipfilename = output_path(f"AP_{world.seed_name}.zip")
logger.info(f"Creating final archive at {zipfilename}")
logger.info(f'Creating final archive at {zipfilename}.')
with zipfile.ZipFile(zipfilename, mode="w", compression=zipfile.ZIP_DEFLATED,
compresslevel=9) as zf:
for file in os.scandir(temp_dir):

View File

@@ -30,8 +30,13 @@ except ImportError:
OperationalError = ConnectionError
import NetUtils
from worlds.AutoWorld import AutoWorldRegister
proxy_worlds = {name: world(None, 0) for name, world in AutoWorldRegister.world_types.items()}
from worlds import network_data_package, lookup_any_item_id_to_name, lookup_any_location_id_to_name
import Utils
from Utils import version_tuple, restricted_loads, Version
from Utils import get_item_name_from_id, get_location_name_from_id, \
version_tuple, restricted_loads, Version
from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \
SlotType
@@ -121,11 +126,6 @@ class Context:
stored_data: typing.Dict[str, object]
stored_data_notification_clients: typing.Dict[str, typing.Set[Client]]
item_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})')
location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})')
all_item_and_group_names: typing.Dict[str, typing.Set[str]]
forced_auto_forfeits: typing.Dict[str, bool]
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
hint_cost: int, item_cheat: bool, forfeit_mode: str = "disabled", collect_mode="disabled",
remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2,
@@ -190,43 +190,8 @@ class Context:
self.stored_data = {}
self.stored_data_notification_clients = collections.defaultdict(weakref.WeakSet)
# init empty to satisfy linter, I suppose
self.gamespackage = {}
self.item_name_groups = {}
self.all_item_and_group_names = {}
self.forced_auto_forfeits = collections.defaultdict(lambda: False)
self.non_hintable_names = {}
self._load_game_data()
self._init_game_data()
# Datapackage retrieval
def _load_game_data(self):
import worlds
self.gamespackage = worlds.network_data_package["games"]
self.item_name_groups = {world_name: world.item_name_groups for world_name, world in
worlds.AutoWorldRegister.world_types.items()}
for world_name, world in worlds.AutoWorldRegister.world_types.items():
self.forced_auto_forfeits[world_name] = world.forced_auto_forfeit
self.non_hintable_names[world_name] = world.hint_blacklist
def _init_game_data(self):
for game_name, game_package in self.gamespackage.items():
for item_name, item_id in game_package["item_name_to_id"].items():
self.item_names[item_id] = item_name
for location_name, location_id in game_package["location_name_to_id"].items():
self.location_names[location_id] = location_name
self.all_item_and_group_names[game_name] = \
set(game_package["item_name_to_id"]) | set(self.item_name_groups[game_name])
def item_names_for_game(self, game: str) -> typing.Dict[str, int]:
return self.gamespackage[game]["item_name_to_id"]
def location_names_for_game(self, game: str) -> typing.Dict[str, int]:
return self.gamespackage[game]["location_name_to_id"]
# General networking
async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[dict]) -> bool:
if not endpoint.socket or not endpoint.socket.open:
return False
@@ -579,12 +544,12 @@ class Context:
finished_msg = f'{self.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1})' \
f' has completed their goal.'
self.notify_all(finished_msg)
if "auto" in self.collect_mode:
collect_player(self, client.team, client.slot)
if "auto" in self.forfeit_mode:
forfeit_player(self, client.team, client.slot)
elif self.forced_auto_forfeits[self.games[client.slot]]:
elif proxy_worlds[self.games[client.slot]].forced_auto_forfeit:
forfeit_player(self, client.team, client.slot)
if "auto" in self.collect_mode:
collect_player(self, client.team, client.slot)
def notify_hints(ctx: Context, team: int, hints: typing.List[NetUtils.Hint], only_new: bool = False):
@@ -594,12 +559,19 @@ def notify_hints(ctx: Context, team: int, hints: typing.List[NetUtils.Hint], onl
if not hints:
return
concerns = collections.defaultdict(list)
all_hints = collections.defaultdict(list)
for hint in sorted(hints, key=operator.attrgetter('found'), reverse=True):
data = (hint, hint.as_network_message())
for player in ctx.slot_set(hint.receiving_player):
concerns[player].append(data)
if not hint.local and data not in concerns[hint.finding_player]:
concerns[hint.finding_player].append(data)
for slot, clients in ctx.clients[team].items():
if not clients or slot == hint.finding_player or slot in ctx.slot_set(hint.receiving_player) or \
(ctx.games[slot] != "Archipelago" and all(["ShowAllHints" not in client.tags for client in clients])):
continue
for client in [client for client in clients if "ShowAllHints" in client.tags or ctx.games[slot] == "Archipelago"]:
all_hints[client].append(data)
# remember hints in all cases
if not hint.found:
# since hints are bidirectional, finding player and receiving player,
@@ -619,6 +591,10 @@ def notify_hints(ctx: Context, team: int, hints: typing.List[NetUtils.Hint], onl
for client in clients:
asyncio.create_task(ctx.send_msgs(client, client_hints))
for client, hint_data in all_hints.items():
client_hints = [datum[1] for datum in sorted(hint_data)]
asyncio.create_task(ctx.send_msgs(client, client_hints))
def update_aliases(ctx: Context, team: int):
cmd = ctx.dumper([{"cmd": "RoomUpdate",
@@ -677,10 +653,9 @@ async def on_client_connected(ctx: Context, client: Client):
'permissions': get_permissions(ctx),
'hint_cost': ctx.hint_cost,
'location_check_points': ctx.location_check_points,
'datapackage_version': sum(game_data["version"] for game_data in ctx.gamespackage.values())
if all(game_data["version"] for game_data in ctx.gamespackage.values()) else 0,
'datapackage_version': network_data_package["version"],
'datapackage_versions': {game: game_data["version"] for game, game_data
in ctx.gamespackage.items()},
in network_data_package["games"].items()},
'seed_name': ctx.seed_name,
'time': time.time(),
}])
@@ -756,16 +731,16 @@ def get_players_string(ctx: Context):
return f'{len(auth_clients)} players of {total} connected ' + text[:-1]
def get_status_string(ctx: Context, team: int, tag: str):
text = f"Player Status on team {team}:"
def get_status_string(ctx: Context, team: int):
text = "Player Status on your team:"
for slot in ctx.locations:
connected = len(ctx.clients[team][slot])
tagged = len([client for client in ctx.clients[team][slot] if tag in client.tags])
death_link = len([client for client in ctx.clients[team][slot] if "DeathLink" in client.tags])
completion_text = f"({len(ctx.location_checks[team, slot])}/{len(ctx.locations[slot])})"
tag_text = f" {tagged} of which are tagged {tag}" if connected and tag else ""
death_text = f" {death_link} of which are death link" if connected else ""
goal_text = " and has finished." if ctx.client_game_state[team, slot] == ClientStatus.CLIENT_GOAL else "."
text += f"\n{ctx.get_aliased_name(team, slot)} has {connected} connection{'' if connected == 1 else 's'}" \
f"{tag_text}{goal_text} {completion_text}"
f"{death_text}{goal_text} {completion_text}"
return text
@@ -858,8 +833,8 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
send_items_to(ctx, team, target_player, new_item)
logging.info('(Team #%d) %s sent %s to %s (%s)' % (
team + 1, ctx.player_names[(team, slot)], ctx.item_names[item_id],
ctx.player_names[(team, target_player)], ctx.location_names[location]))
team + 1, ctx.player_names[(team, slot)], get_item_name_from_id(item_id),
ctx.player_names[(team, target_player)], get_location_name_from_id(location)))
info_text = json_format_send_event(new_item, target_player)
ctx.broadcast_team(team, [info_text])
@@ -874,14 +849,13 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
ctx.save()
def collect_hints(ctx: Context, team: int, slot: int, item_name: str) -> typing.List[NetUtils.Hint]:
def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[NetUtils.Hint]:
hints = []
slots: typing.Set[int] = {slot}
for group_id, group in ctx.groups.items():
if slot in group:
slots.add(group_id)
seeked_item_id = ctx.item_names_for_game(ctx.games[slot])[item_name]
seeked_item_id = proxy_worlds[ctx.games[slot]].item_name_to_id[item]
for finding_player, check_data in ctx.locations.items():
for location_id, (item_id, receiving_player, item_flags) in check_data.items():
if receiving_player in slots and item_id == seeked_item_id:
@@ -894,7 +868,7 @@ def collect_hints(ctx: Context, team: int, slot: int, item_name: str) -> typing.
def collect_hint_location_name(ctx: Context, team: int, slot: int, location: str) -> typing.List[NetUtils.Hint]:
seeked_location: int = ctx.location_names_for_game(ctx.games[slot])[location]
seeked_location: int = proxy_worlds[ctx.games[slot]].location_name_to_id[location]
return collect_hint_location_id(ctx, team, slot, seeked_location)
@@ -911,8 +885,8 @@ def collect_hint_location_id(ctx: Context, team: int, slot: int, seeked_location
def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str:
text = f"[Hint]: {ctx.player_names[team, hint.receiving_player]}'s " \
f"{ctx.item_names[hint.item]} is " \
f"at {ctx.location_names[hint.location]} " \
f"{lookup_any_item_id_to_name[hint.item]} is " \
f"at {get_location_name_from_id(hint.location)} " \
f"in {ctx.player_names[team, hint.finding_player]}'s World"
if hint.entrance:
@@ -1150,11 +1124,9 @@ class ClientMessageProcessor(CommonCommandProcessor):
self.output(get_players_string(self.ctx))
return True
def _cmd_status(self, tag:str="") -> bool:
"""Get status information about your team.
Optionally mention a Tag name and get information on who has that Tag.
For example: DeathLink or EnergyLink."""
self.output(get_status_string(self.ctx, self.client.team, tag))
def _cmd_status(self) -> bool:
"""Get status information about your team."""
self.output(get_status_string(self.ctx, self.client.team))
return True
def _cmd_release(self) -> bool:
@@ -1170,8 +1142,8 @@ class ClientMessageProcessor(CommonCommandProcessor):
forfeit_player(self.ctx, self.client.team, self.client.slot)
return True
elif "disabled" in self.ctx.forfeit_mode:
self.output("Sorry, client item releasing has been disabled on this server. "
"You can ask the server admin for a /release")
self.output(
"Sorry, client item releasing has been disabled on this server. You can ask the server admin for a /release")
return False
else: # is auto or goal
if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL:
@@ -1207,7 +1179,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
if self.ctx.remaining_mode == "enabled":
remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot)
if remaining_item_ids:
self.output("Remaining items: " + ", ".join(self.ctx.item_names[item_id]
self.output("Remaining items: " + ", ".join(lookup_any_item_id_to_name.get(item_id, "unknown item")
for item_id in remaining_item_ids))
else:
self.output("No remaining items found.")
@@ -1220,7 +1192,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL:
remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot)
if remaining_item_ids:
self.output("Remaining items: " + ", ".join(self.ctx.item_names[item_id]
self.output("Remaining items: " + ", ".join(lookup_any_item_id_to_name.get(item_id, "unknown item")
for item_id in remaining_item_ids))
else:
self.output("No remaining items found.")
@@ -1236,7 +1208,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
locations = get_missing_checks(self.ctx, self.client.team, self.client.slot)
if locations:
texts = [f'Missing: {self.ctx.location_names[location]}' for location in locations]
texts = [f'Missing: {get_location_name_from_id(location)}' for location in locations]
texts.append(f"Found {len(locations)} missing location checks")
self.ctx.notify_client_multiple(self.client, texts)
else:
@@ -1249,7 +1221,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
locations = get_checked_checks(self.ctx, self.client.team, self.client.slot)
if locations:
texts = [f'Checked: {self.ctx.location_names[location]}' for location in locations]
texts = [f'Checked: {get_location_name_from_id(location)}' for location in locations]
texts.append(f"Found {len(locations)} done location checks")
self.ctx.notify_client_multiple(self.client, texts)
else:
@@ -1278,13 +1250,11 @@ class ClientMessageProcessor(CommonCommandProcessor):
def _cmd_getitem(self, item_name: str) -> bool:
"""Cheat in an item, if it is enabled on this server"""
if self.ctx.item_cheat:
names = self.ctx.item_names_for_game(self.ctx.games[self.client.slot])
item_name, usable, response = get_intended_text(
item_name,
names
)
world = proxy_worlds[self.ctx.games[self.client.slot]]
item_name, usable, response = get_intended_text(item_name,
world.item_names)
if usable:
new_item = NetworkItem(names[item_name], -1, self.client.slot)
new_item = NetworkItem(world.create_item(item_name).code, -1, self.client.slot)
get_received_items(self.ctx, self.client.team, self.client.slot, False).append(new_item)
get_received_items(self.ctx, self.client.team, self.client.slot, True).append(new_item)
self.ctx.notify_all(
@@ -1310,22 +1280,20 @@ class ClientMessageProcessor(CommonCommandProcessor):
f"You have {points_available} points.")
return True
else:
game = self.ctx.games[self.client.slot]
names = self.ctx.location_names_for_game(game) \
if for_location else \
self.ctx.all_item_and_group_names[game]
world = proxy_worlds[self.ctx.games[self.client.slot]]
names = world.location_names if for_location else world.all_item_and_group_names
hint_name, usable, response = get_intended_text(input_text,
names)
if usable:
if hint_name in self.ctx.non_hintable_names[game]:
if hint_name in world.hint_blacklist:
self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.")
hints = []
elif not for_location and hint_name in self.ctx.item_name_groups[game]: # item group name
elif not for_location and hint_name in world.item_name_groups: # item group name
hints = []
for item_name in self.ctx.item_name_groups[game][hint_name]:
if item_name in self.ctx.item_names_for_game(game): # ensure item has an ID
hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item_name))
elif not for_location and hint_name in self.ctx.item_names_for_game(game): # item name
for item in world.item_name_groups[hint_name]:
if item in world.item_name_to_id: # ensure item has an ID
hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item))
elif not for_location and hint_name in world.item_names: # item name
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name)
else: # location name
hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name)
@@ -1349,8 +1317,6 @@ class ClientMessageProcessor(CommonCommandProcessor):
can_pay = 1000
self.ctx.random.shuffle(not_found_hints)
# By popular vote, make hints prefer non-local placements
not_found_hints.sort(key=lambda hint: int(hint.receiving_player != hint.finding_player))
hints = found_hints
while can_pay > 0:
@@ -1387,12 +1353,12 @@ class ClientMessageProcessor(CommonCommandProcessor):
return False
@mark_raw
def _cmd_hint(self, item_name: str = "") -> bool:
def _cmd_hint(self, item: str = "") -> bool:
"""Use !hint {item_name},
for example !hint Lamp to get a spoiler peek for that item.
If hint costs are on, this will only give you one new result,
you can rerun the command to get more in that case."""
return self.get_hints(item_name)
return self.get_hints(item)
@mark_raw
def _cmd_hint_location(self, location: str = "") -> bool:
@@ -1518,23 +1484,23 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
elif cmd == "GetDataPackage":
exclusions = args.get("exclusions", [])
if "games" in args:
games = {name: game_data for name, game_data in ctx.gamespackage.items()
games = {name: game_data for name, game_data in network_data_package["games"].items()
if name in set(args.get("games", []))}
await ctx.send_msgs(client, [{"cmd": "DataPackage",
"data": {"games": games}}])
# TODO: remove exclusions behaviour around 0.5.0
elif exclusions:
exclusions = set(exclusions)
games = {name: game_data for name, game_data in ctx.gamespackage.items()
games = {name: game_data for name, game_data in network_data_package["games"].items()
if name not in exclusions}
package = {"games": games}
package = network_data_package.copy()
package["games"] = games
await ctx.send_msgs(client, [{"cmd": "DataPackage",
"data": package}])
else:
await ctx.send_msgs(client, [{"cmd": "DataPackage",
"data": {"games": ctx.gamespackage}}])
"data": network_data_package}])
elif client.auth:
if cmd == "ConnectUpdate":
@@ -1590,7 +1556,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
create_as_hint: int = int(args.get("create_as_hint", 0))
hints = []
for location in args["locations"]:
if type(location) is not int:
if type(location) is not int or location not in lookup_any_location_id_to_name:
await ctx.send_msgs(client,
[{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts',
"original_cmd": cmd}])
@@ -1702,14 +1668,6 @@ class ServerCommandProcessor(CommonCommandProcessor):
self.output(get_players_string(self.ctx))
return True
def _cmd_status(self, tag: str = "") -> bool:
"""Get status information about teams.
Optionally mention a Tag name and get information on who has that Tag.
For example: DeathLink or EnergyLink."""
for team in self.ctx.clients:
self.output(get_status_string(self.ctx, team, tag))
return True
def _cmd_exit(self) -> bool:
"""Shutdown the server"""
asyncio.create_task(self.ctx.server.ws_server._close())
@@ -1804,18 +1762,18 @@ class ServerCommandProcessor(CommonCommandProcessor):
seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
if usable:
team, slot = self.ctx.player_name_lookup[seeked_player]
item_name = " ".join(item_name)
names = self.ctx.item_names_for_game(self.ctx.games[slot])
item_name, usable, response = get_intended_text(item_name, names)
item = " ".join(item_name)
world = proxy_worlds[self.ctx.games[slot]]
item, usable, response = get_intended_text(item, world.item_names)
if usable:
amount: int = int(amount)
new_items = [NetworkItem(names[item_name], -1, 0) for _ in range(int(amount))]
new_items = [NetworkItem(world.item_name_to_id[item], -1, 0) for i in range(int(amount))]
send_items_to(self.ctx, team, slot, *new_items)
send_new_items(self.ctx)
self.ctx.notify_all(
'Cheat console: sending ' + ('' if amount == 1 else f'{amount} of ') +
f'"{item_name}" to {self.ctx.get_aliased_name(team, slot)}')
f'"{item}" to {self.ctx.get_aliased_name(team, slot)}')
return True
else:
self.output(response)
@@ -1828,22 +1786,22 @@ class ServerCommandProcessor(CommonCommandProcessor):
"""Sends an item to the specified player"""
return self._cmd_send_multiple(1, player_name, *item_name)
def _cmd_hint(self, player_name: str, *item_name: str) -> bool:
def _cmd_hint(self, player_name: str, *item: str) -> bool:
"""Send out a hint for a player's item to their team"""
seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
if usable:
team, slot = self.ctx.player_name_lookup[seeked_player]
item_name = " ".join(item_name)
game = self.ctx.games[slot]
item_name, usable, response = get_intended_text(item_name, self.ctx.all_item_and_group_names[game])
item = " ".join(item)
world = proxy_worlds[self.ctx.games[slot]]
item, usable, response = get_intended_text(item, world.all_item_and_group_names)
if usable:
if item_name in self.ctx.item_name_groups[game]:
if item in world.item_name_groups:
hints = []
for item_name_from_group in self.ctx.item_name_groups[game][item_name]:
if item_name_from_group in self.ctx.item_names_for_game(game): # ensure item has an ID
hints.extend(collect_hints(self.ctx, team, slot, item_name_from_group))
for item in world.item_name_groups[item]:
if item in world.item_name_to_id: # ensure item has an ID
hints.extend(collect_hints(self.ctx, team, slot, item))
else: # item name
hints = collect_hints(self.ctx, team, slot, item_name)
hints = collect_hints(self.ctx, team, slot, item)
if hints:
notify_hints(self.ctx, team, hints)
@@ -1859,16 +1817,16 @@ class ServerCommandProcessor(CommonCommandProcessor):
self.output(response)
return False
def _cmd_hint_location(self, player_name: str, *location_name: str) -> bool:
def _cmd_hint_location(self, player_name: str, *location: str) -> bool:
"""Send out a hint for a player's location to their team"""
seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
if usable:
team, slot = self.ctx.player_name_lookup[seeked_player]
location_name = " ".join(location_name)
location_name, usable, response = get_intended_text(location_name,
self.ctx.location_names_for_game(self.ctx.games[slot]))
item = " ".join(location)
world = proxy_worlds[self.ctx.games[slot]]
item, usable, response = get_intended_text(item, world.location_names)
if usable:
hints = collect_hint_location_name(self.ctx, team, slot, location_name)
hints = collect_hint_location_name(self.ctx, team, slot, item)
if hints:
notify_hints(self.ctx, team, hints)
else:

View File

@@ -270,7 +270,7 @@ class RawJSONtoTextParser(JSONtoTextParser):
color_codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34,
'magenta': 35, 'cyan': 36, 'white': 37, 'black_bg': 40, 'red_bg': 41, 'green_bg': 42, 'yellow_bg': 43,
'blue_bg': 44, 'magenta_bg': 45, 'cyan_bg': 46, 'white_bg': 47}
'blue_bg': 44, 'purple_bg': 45, 'cyan_bg': 46, 'white_bg': 47}
def color_code(*args):

View File

@@ -48,7 +48,7 @@ deathlink_sent_this_death: we interacted with the multiworld on this death, wait
oot_loc_name_to_id = network_data_package["games"]["Ocarina of Time"]["location_name_to_id"]
script_version: int = 2
script_version: int = 1
def get_item_value(ap_id):
return ap_id - 66000
@@ -186,7 +186,7 @@ async def n64_sync_task(ctx: OoTContext):
data = await asyncio.wait_for(reader.readline(), timeout=10)
data_decoded = json.loads(data.decode())
reported_version = data_decoded.get('scriptVersion', 0)
if reported_version >= script_version:
if reported_version == script_version:
if ctx.game is not None and 'locations' in data_decoded:
# Not just a keep alive ping, parse
asyncio.create_task(parse_payload(data_decoded, ctx, False))

View File

@@ -166,15 +166,13 @@ GAME_ALTTP = "A Link to the Past"
GAME_SM = "Super Metroid"
GAME_SOE = "Secret of Evermore"
GAME_SMZ3 = "SMZ3"
GAME_DKC3 = "Donkey Kong Country 3"
supported_games = {"A Link to the Past", "Super Metroid", "Secret of Evermore", "SMZ3", "Donkey Kong Country 3"}
supported_games = {"A Link to the Past", "Super Metroid", "Secret of Evermore", "SMZ3"}
preferred_endings = {
GAME_ALTTP: "apbp",
GAME_SM: "apm3",
GAME_SOE: "apsoe",
GAME_SMZ3: "apsmz",
GAME_DKC3: "apdkc3"
GAME_SMZ3: "apsmz"
}
@@ -189,8 +187,6 @@ def generate_yaml(patch: bytes, metadata: Optional[dict] = None, game: str = GAM
from worlds.alttp.Rom import LTTPJPN10HASH as ALTTPHASH
from worlds.sm.Rom import SMJUHASH as SMHASH
HASH = ALTTPHASH + SMHASH
elif game == GAME_DKC3:
from worlds.dkc3.Rom import USHASH as HASH
else:
raise RuntimeError(f"Selected game {game} for base rom not found.")
@@ -220,10 +216,7 @@ def create_patch_file(rom_file_to_patch: str, server: str = "", destination: str
meta,
game)
target = destination if destination else os.path.splitext(rom_file_to_patch)[0] + (
".apbp" if game == GAME_ALTTP
else ".apsmz" if game == GAME_SMZ3
else ".apdkc3" if game == GAME_DKC3
else ".apm3")
".apbp" if game == GAME_ALTTP else ".apsmz" if game == GAME_SMZ3 else ".apm3")
write_lzma(bytes, target)
return target
@@ -252,8 +245,6 @@ def get_base_rom_data(game: str):
get_base_rom_bytes = lambda: bytes(read_rom(open(get_base_rom_path(), "rb")))
elif game == GAME_SMZ3:
from worlds.smz3.Rom import get_base_rom_bytes
elif game == GAME_DKC3:
from worlds.dkc3.Rom import get_base_rom_bytes
else:
raise RuntimeError("Selected game for base rom not found.")
return get_base_rom_bytes()
@@ -398,13 +389,6 @@ if __name__ == "__main__":
if 'server' in data:
Utils.persistent_store("servers", data['hash'], data['server'])
print(f"Host is {data['server']}")
elif rom.endswith(".apdkc3"):
print(f"Applying patch {rom}")
data, target = create_rom_file(rom)
print(f"Created rom {target}.")
if 'server' in data:
Utils.persistent_store("servers", data['hash'], data['server'])
print(f"Host is {data['server']}")
elif rom.endswith(".zip"):
print(f"Updating host in patch files contained in {rom}")
@@ -412,9 +396,7 @@ if __name__ == "__main__":
def _handle_zip_file_entry(zfinfo: zipfile.ZipInfo, server: str):
data = zfr.read(zfinfo)
if zfinfo.filename.endswith(".apbp") or \
zfinfo.filename.endswith(".apm3") or \
zfinfo.filename.endswith(".apdkc3"):
if zfinfo.filename.endswith(".apbp") or zfinfo.filename.endswith(".apm3"):
data = update_patch_data(data, server)
with ziplock:
zfw.writestr(zfinfo, data)

View File

@@ -26,8 +26,6 @@ Currently, the following games are supported:
* The Witness
* Sonic Adventure 2: Battle
* Starcraft 2: Wings of Liberty
* Donkey Kong Country 3
* Dark Souls 3
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
@@ -51,7 +49,7 @@ Archipelago was directly forked from bonta0's `multiworld_31` branch of ALttPEnt
## Running Archipelago
For most people all you need to do is head over to the [releases](https://github.com/ArchipelagoMW/Archipelago/releases) page then download and run the appropriate installer. The installers function on Windows only.
If you are running Archipelago from a non-Windows system then the likely scenario is that you are comfortable running source code directly. Please see our doc on [running Archipelago from source](docs/running%20from%20source.md).
If you are running Archipelago from a non-Windows system then the likely scenario is that you are comfortable running source code directly. Please see our wiki page on [running Archipelago from source](https://github.com/ArchipelagoMW/Archipelago/wiki/Running-from-source).
## Related Repositories
This project makes use of multiple other projects. We wouldn't be here without these other repositories and the contributions of their developers, past and present.
@@ -68,7 +66,7 @@ Contributions are welcome. We have a few asks of any new contributors.
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 adding a new game to Archipelago and other documentation on how Archipelago functions, please see the docs folder for the relevant information and feel free to ask any questions in the #archipelago-dev channel in our discord.
## FAQ
For frequently asked questions see the website's [FAQ Page](https://archipelago.gg/faq/en/)

View File

@@ -33,7 +33,7 @@ from worlds.sm.Rom import ROM_PLAYER_LIMIT as SM_ROM_PLAYER_LIMIT
from worlds.smz3.Rom import ROM_PLAYER_LIMIT as SMZ3_ROM_PLAYER_LIMIT
import Utils
from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser
from Patch import GAME_ALTTP, GAME_SM, GAME_SMZ3, GAME_DKC3
from Patch import GAME_ALTTP, GAME_SM, GAME_SMZ3
snes_logger = logging.getLogger("SNES")
@@ -62,7 +62,7 @@ class SNIClientCommandProcessor(ClientCommandProcessor):
def _cmd_snes(self, snes_options: str = "") -> bool:
"""Connect to a snes. Optionally include network address of a snes to connect to,
otherwise show available devices; and a SNES device number if more than one SNES is detected.
Examples: "/snes", "/snes 1", "/snes localhost:23074 1" """
Examples: "/snes", "/snes 1", "/snes localhost:8080 1" """
snes_address = self.ctx.snes_address
snes_device_number = -1
@@ -188,10 +188,7 @@ class Context(CommonContext):
async def shutdown(self):
await super(Context, self).shutdown()
if self.snes_connect_task:
try:
await asyncio.wait_for(self.snes_connect_task, 1)
except asyncio.TimeoutError:
self.snes_connect_task.cancel()
await self.snes_connect_task
def on_package(self, cmd: str, args: dict):
if cmd in {"Connected", "RoomUpdate"}:
@@ -254,9 +251,6 @@ async def deathlink_kill_player(ctx: Context):
if not gamemode or gamemode[0] in SM_DEATH_MODES or (
ctx.death_link_allow_survive and health is not None and health > 0):
ctx.death_state = DeathState.dead
elif ctx.game == GAME_DKC3:
from worlds.dkc3.Client import deathlink_kill_player as dkc3_deathlink_kill_player
await dkc3_deathlink_kill_player(ctx)
ctx.last_death_link = time.time()
@@ -601,7 +595,7 @@ class SNESState(enum.IntEnum):
SNES_ATTACHED = 3
def launch_sni():
def launch_sni(ctx: Context):
sni_path = Utils.get_options()["lttp_options"]["sni"]
if not os.path.isdir(sni_path):
@@ -639,9 +633,11 @@ async def _snes_connect(ctx: Context, address: str):
address = f"ws://{address}" if "://" not in address else address
snes_logger.info("Connecting to SNI at %s ..." % address)
seen_problems = set()
while 1:
succesful = False
while not succesful:
try:
snes_socket = await websockets.connect(address, ping_timeout=None, ping_interval=None)
succesful = True
except Exception as e:
problem = "%s" % e
# only tell the user about new problems, otherwise silently lay in wait for a working connection
@@ -651,7 +647,7 @@ async def _snes_connect(ctx: Context, address: str):
if len(seen_problems) == 1:
# this is the first problem. Let's try launching SNI if it isn't already running
launch_sni()
launch_sni(ctx)
await asyncio.sleep(1)
else:
@@ -1038,48 +1034,44 @@ async def game_watcher(ctx: Context):
if not ctx.rom:
ctx.finished_game = False
ctx.death_link_allow_survive = False
from worlds.dkc3.Client import dkc3_rom_init
init_handled = await dkc3_rom_init(ctx)
if not init_handled:
game_name = await snes_read(ctx, SM_ROMNAME_START, 5)
if game_name is None:
continue
elif game_name[:2] == b"SM":
ctx.game = GAME_SM
# versions lower than 0.3.0 dont have item handling flag nor remote item support
romVersion = int(game_name[2:5].decode('UTF-8'))
if romVersion < 30:
ctx.items_handling = 0b001 # full local
else:
item_handling = await snes_read(ctx, SM_REMOTE_ITEM_FLAG_ADDR, 1)
ctx.items_handling = 0b001 if item_handling is None else item_handling[0]
game_name = await snes_read(ctx, SM_ROMNAME_START, 5)
if game_name is None:
continue
elif game_name[:2] == b"SM":
ctx.game = GAME_SM
# versions lower than 0.3.0 dont have item handling flag nor remote item support
romVersion = int(game_name[2:5].decode('UTF-8'))
if romVersion < 30:
ctx.items_handling = 0b001 # full local
else:
game_name = await snes_read(ctx, SMZ3_ROMNAME_START, 3)
if game_name == b"ZSM":
ctx.game = GAME_SMZ3
ctx.items_handling = 0b101 # local items and remote start inventory
else:
ctx.game = GAME_ALTTP
ctx.items_handling = 0b001 # full local
item_handling = await snes_read(ctx, SM_REMOTE_ITEM_FLAG_ADDR, 1)
ctx.items_handling = 0b001 if item_handling is None else item_handling[0]
else:
game_name = await snes_read(ctx, SMZ3_ROMNAME_START, 3)
if game_name == b"ZSM":
ctx.game = GAME_SMZ3
ctx.items_handling = 0b101 # local items and remote start inventory
else:
ctx.game = GAME_ALTTP
ctx.items_handling = 0b001 # full local
rom = await snes_read(ctx, SM_ROMNAME_START if ctx.game == GAME_SM else SMZ3_ROMNAME_START if ctx.game == GAME_SMZ3 else ROMNAME_START, ROMNAME_SIZE)
if rom is None or rom == bytes([0] * ROMNAME_SIZE):
continue
rom = await snes_read(ctx, SM_ROMNAME_START if ctx.game == GAME_SM else SMZ3_ROMNAME_START if ctx.game == GAME_SMZ3 else ROMNAME_START, ROMNAME_SIZE)
if rom is None or rom == bytes([0] * ROMNAME_SIZE):
continue
ctx.rom = rom
if ctx.game != GAME_SMZ3:
death_link = await snes_read(ctx, DEATH_LINK_ACTIVE_ADDR if ctx.game == GAME_ALTTP else
SM_DEATH_LINK_ACTIVE_ADDR, 1)
if death_link:
ctx.allow_collect = bool(death_link[0] & 0b100)
ctx.death_link_allow_survive = bool(death_link[0] & 0b10)
await ctx.update_death_link(bool(death_link[0] & 0b1))
if not ctx.prev_rom or ctx.prev_rom != ctx.rom:
ctx.locations_checked = set()
ctx.locations_scouted = set()
ctx.locations_info = {}
ctx.prev_rom = ctx.rom
ctx.rom = rom
if ctx.game != GAME_SMZ3:
death_link = await snes_read(ctx, DEATH_LINK_ACTIVE_ADDR if ctx.game == GAME_ALTTP else
SM_DEATH_LINK_ACTIVE_ADDR, 1)
if death_link:
ctx.allow_collect = bool(death_link[0] & 0b100)
ctx.death_link_allow_survive = bool(death_link[0] & 0b10)
await ctx.update_death_link(bool(death_link[0] & 0b1))
if not ctx.prev_rom or ctx.prev_rom != ctx.rom:
ctx.locations_checked = set()
ctx.locations_scouted = set()
ctx.locations_info = {}
ctx.prev_rom = ctx.rom
if ctx.awaiting_rom:
await ctx.server_auth(False)
@@ -1287,9 +1279,6 @@ async def game_watcher(ctx: Context):
color(ctx.item_names[item.item], 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'),
ctx.location_names[item.location], itemOutPtr, len(ctx.items_received)))
await snes_flush_writes(ctx)
elif ctx.game == GAME_DKC3:
from worlds.dkc3.Client import dkc3_game_watcher
await dkc3_game_watcher(ctx)
async def run_game(romfile):
@@ -1307,7 +1296,7 @@ async def main():
parser = get_base_parser()
parser.add_argument('diff_file', default="", type=str, nargs="?",
help='Path to a Archipelago Binary Patch file')
parser.add_argument('--snes', default='localhost:23074', help='Address of the SNI server.')
parser.add_argument('--snes', default='localhost:8080', help='Address of the SNI server.')
parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
args = parser.parse_args()

View File

@@ -19,13 +19,7 @@ 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 Utils import init_logging
if __name__ == "__main__":
init_logging("SC2Client", exception_logger="Client")
@@ -44,38 +38,11 @@ nest_asyncio.apply()
class StarcraftClientProcessor(ClientCommandProcessor):
ctx: SC2Context
def _cmd_difficulty(self, difficulty: str = "") -> bool:
"""Overrides the current difficulty set for the seed. Takes the argument casual, normal, hard, or brutal"""
options = difficulty.split()
num_options = len(options)
difficulty_choice = options[0].lower()
if num_options > 0:
if difficulty_choice == "casual":
self.ctx.difficulty_override = 0
elif difficulty_choice == "normal":
self.ctx.difficulty_override = 1
elif difficulty_choice == "hard":
self.ctx.difficulty_override = 2
elif difficulty_choice == "brutal":
self.ctx.difficulty_override = 3
else:
self.output("Unable to parse difficulty '" + options[0] + "'")
return False
self.output("Difficulty set to " + options[0])
return True
else:
self.output("Difficulty needs to be specified in the command.")
return False
def _cmd_disable_mission_check(self) -> bool:
"""Disables the check to see if a mission is available to play. Meant for co-op runs where one player can play
the next mission in a chain the other player is doing."""
self.ctx.missions_unlocked = True
sc2_logger.info("Mission check has been disabled")
return True
def _cmd_play(self, mission_id: str = "") -> bool:
"""Start a Starcraft 2 mission"""
@@ -91,7 +58,6 @@ class StarcraftClientProcessor(ClientCommandProcessor):
else:
sc2_logger.info(
"Mission ID needs to be specified. Use /unfinished or /available to view ids for available missions.")
return False
return True
@@ -107,17 +73,6 @@ class StarcraftClientProcessor(ClientCommandProcessor):
request_unfinished_missions(self.ctx.checked_locations, self.ctx.mission_req_table, self.ctx.ui, self.ctx)
return True
@mark_raw
def _cmd_set_path(self, path: str = '') -> bool:
"""Manually set the SC2 install directory (if the automatic detection fails)."""
if path:
os.environ["SC2PATH"] = path
check_mod_install()
return True
else:
sc2_logger.warning("When using set_path, you must type the path to your SC2 install directory.")
return False
class SC2Context(CommonContext):
command_processor = StarcraftClientProcessor
@@ -136,7 +91,6 @@ class SC2Context(CommonContext):
missions_unlocked = False
current_tooltip = None
last_loc_list = None
difficulty_override = -1
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
@@ -157,11 +111,6 @@ class SC2Context(CommonContext):
for mission in slot_req_table:
self.mission_req_table[mission] = MissionInfo(**slot_req_table[mission])
# 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"]):
@@ -466,9 +415,8 @@ async def starcraft_launch(ctx: SC2Context, mission_id):
sc2_logger.info(f"Launching {lookup_id_to_mission[mission_id]}. If game does not launch check log file for errors.")
with DllDirectory(None):
run_game(sc2.maps.get(maps_table[mission_id - 1]), [Bot(Race.Terran, ArchipelagoBot(ctx, mission_id),
name="Archipelago", fullscreen=True)], realtime=True)
run_game(sc2.maps.get(maps_table[mission_id - 1]), [Bot(Race.Terran, ArchipelagoBot(ctx, mission_id),
name="Archipelago", fullscreen=True)], realtime=True)
class ArchipelagoBot(sc2.bot_ai.BotAI):
@@ -499,10 +447,7 @@ class ArchipelagoBot(sc2.bot_ai.BotAI):
game_state = 0
if iteration == 0:
start_items = calculate_items(self.ctx.items_received)
if self.ctx.difficulty_override >= 0:
difficulty = calc_difficulty(self.ctx.difficulty_override)
else:
difficulty = calc_difficulty(self.ctx.difficulty)
difficulty = calc_difficulty(self.ctx.difficulty)
await self.chat_send("ArchipelagoLoad {} {} {} {} {} {} {} {} {} {} {} {} {}".format(
difficulty,
start_items[0], start_items[1], start_items[2], start_items[3], start_items[4],
@@ -851,101 +796,6 @@ def initialize_blank_mission_dict(location_table):
return unlocks
def check_game_install_path() -> bool:
# First thing: go to the default location for ExecuteInfo.
# An exception for Windows is included because it's very difficult to find ~\Documents if the user moved it.
if is_windows:
# The next five lines of utterly inscrutable code are brought to you by copy-paste from Stack Overflow.
# https://stackoverflow.com/questions/6227590/finding-the-users-my-documents-path/30924555#
import ctypes.wintypes
CSIDL_PERSONAL = 5 # My Documents
SHGFP_TYPE_CURRENT = 0 # Get current, not default value
buf = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH)
ctypes.windll.shell32.SHGetFolderPathW(None, CSIDL_PERSONAL, None, SHGFP_TYPE_CURRENT, buf)
documentspath = buf.value
einfo = str(documentspath / Path("StarCraft II\\ExecuteInfo.txt"))
else:
einfo = str(sc2.paths.get_home() / Path(sc2.paths.USERPATH[sc2.paths.PF]))
# Check if the file exists.
if os.path.isfile(einfo):
# Open the file and read it, picking out the latest executable's path.
with open(einfo) as f:
content = f.read()
if content:
base = re.search(r" = (.*)Versions", content).group(1)
if os.path.exists(base):
executable = sc2.paths.latest_executeble(Path(base).expanduser() / "Versions")
# Finally, check the path for an actual executable.
# If we find one, great. Set up the SC2PATH.
if os.path.isfile(executable):
sc2_logger.info(f"Found an SC2 install at {base}!")
sc2_logger.debug(f"Latest executable at {executable}.")
os.environ["SC2PATH"] = base
sc2_logger.debug(f"SC2PATH set to {base}.")
return True
else:
sc2_logger.warning(f"We may have found an SC2 install at {base}, but couldn't find {executable}.")
else:
sc2_logger.warning(f"{einfo} pointed to {base}, but we could not find an SC2 install there.")
else:
sc2_logger.warning(f"Couldn't find {einfo}. Please run /set_path with your SC2 install directory.")
return False
def check_mod_install() -> bool:
# Pull up the SC2PATH if set. If not, encourage the user to manually run /set_path.
try:
# Check inside the Mods folder for Archipelago.SC2Mod. If found, tell user. If not, tell user.
if os.path.isfile(modfile := (os.environ["SC2PATH"] / Path("Mods") / Path("Archipelago.SC2Mod"))):
sc2_logger.info(f"Archipelago mod found at {modfile}.")
return True
else:
sc2_logger.warning(f"Archipelago mod could not be found at {modfile}. Please install the mod file there.")
except KeyError:
sc2_logger.warning(f"SC2PATH isn't set. Please run /set_path with the path to your SC2 install.")
return False
class DllDirectory:
# Credit to Black Sliver for this code.
# More info: https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setdlldirectoryw
_old: typing.Optional[str] = None
_new: typing.Optional[str] = None
def __init__(self, new: typing.Optional[str]):
self._new = new
def __enter__(self):
old = self.get()
if self.set(self._new):
self._old = old
def __exit__(self, *args):
if self._old is not None:
self.set(self._old)
@staticmethod
def get() -> str:
if sys.platform == "win32":
n = ctypes.windll.kernel32.GetDllDirectoryW(0, None)
buf = ctypes.create_unicode_buffer(n)
ctypes.windll.kernel32.GetDllDirectoryW(n, buf)
return buf.value
# NOTE: other OS may support os.environ["LD_LIBRARY_PATH"], but this fix is windows-specific
return None
@staticmethod
def set(s: typing.Optional[str]) -> bool:
if sys.platform == "win32":
return ctypes.windll.kernel32.SetDllDirectoryW(s) != 0
# NOTE: other OS may support os.environ["LD_LIBRARY_PATH"], but this fix is windows-specific
return False
if __name__ == '__main__':
colorama.init()
asyncio.run(main())

195
Utils.py
View File

@@ -1,5 +1,6 @@
from __future__ import annotations
import shutil
import typing
import builtins
import os
@@ -11,18 +12,12 @@ import io
import collections
import importlib
import logging
from yaml import load, load_all, dump, SafeLoader
try:
from yaml import CLoader as UnsafeLoader
from yaml import CDumper as Dumper
except ImportError:
from yaml import Loader as UnsafeLoader
from yaml import Dumper
import decimal
if typing.TYPE_CHECKING:
import tkinter
import pathlib
from tkinter import Tk
else:
Tk = typing.Any
def tuplize_version(version: str) -> Version:
@@ -35,13 +30,21 @@ class Version(typing.NamedTuple):
build: int
__version__ = "0.3.4"
__version__ = "0.3.3"
version_tuple = tuplize_version(__version__)
is_linux = sys.platform.startswith("linux")
is_macos = sys.platform == "darwin"
is_linux = sys.platform.startswith('linux')
is_macos = sys.platform == 'darwin'
is_windows = sys.platform in ("win32", "cygwin", "msys")
import jellyfish
from yaml import load, load_all, dump, SafeLoader
try:
from yaml import CLoader as Loader
except ImportError:
from yaml import Loader
def int16_as_bytes(value: int) -> typing.List[int]:
value = value & 0xFFFF
@@ -122,18 +125,17 @@ def home_path(*path: str) -> str:
def user_path(*path: str) -> str:
"""Returns either local_path or home_path based on write permissions."""
if hasattr(user_path, "cached_path"):
if hasattr(user_path, 'cached_path'):
pass
elif os.access(local_path(), os.W_OK):
user_path.cached_path = local_path()
else:
user_path.cached_path = home_path()
# populate home from local - TODO: upgrade feature
if user_path.cached_path != local_path() and not os.path.exists(user_path("host.yaml")):
import shutil
for dn in ("Players", "data/sprites"):
if user_path.cached_path != local_path() and not os.path.exists(user_path('host.yaml')):
for dn in ('Players', 'data/sprites'):
shutil.copytree(local_path(dn), user_path(dn), dirs_exist_ok=True)
for fn in ("manifest.json", "host.yaml"):
for fn in ('manifest.json', 'host.yaml'):
shutil.copy2(local_path(fn), user_path(fn))
return os.path.join(user_path.cached_path, *path)
@@ -148,12 +150,11 @@ def output_path(*path: str):
return path
def open_file(filename: typing.Union[str, "pathlib.Path"]) -> None:
if is_windows:
def open_file(filename):
if sys.platform == 'win32':
os.startfile(filename)
else:
from shutil import which
open_command = which("open") if is_macos else (which("xdg-open") or which("gnome-open") or which("kde-open"))
open_command = 'open' if sys.platform == 'darwin' else 'xdg-open'
subprocess.call([open_command, filename])
@@ -172,9 +173,7 @@ class UniqueKeyLoader(SafeLoader):
parse_yaml = functools.partial(load, Loader=UniqueKeyLoader)
parse_yamls = functools.partial(load_all, Loader=UniqueKeyLoader)
unsafe_parse_yaml = functools.partial(load, Loader=UnsafeLoader)
del load, load_all # should not be used. don't leak their names
unsafe_parse_yaml = functools.partial(load, Loader=Loader)
def get_cert_none_ssl_context():
@@ -192,12 +191,11 @@ def get_public_ipv4() -> str:
ip = socket.gethostbyname(socket.gethostname())
ctx = get_cert_none_ssl_context()
try:
ip = urllib.request.urlopen("https://checkip.amazonaws.com/", context=ctx).read().decode("utf8").strip()
ip = urllib.request.urlopen('https://checkip.amazonaws.com/', context=ctx).read().decode('utf8').strip()
except Exception as e:
# noinspection PyBroadException
try:
ip = urllib.request.urlopen("https://v4.ident.me", context=ctx).read().decode("utf8").strip()
except Exception:
ip = urllib.request.urlopen('https://v4.ident.me', context=ctx).read().decode('utf8').strip()
except:
logging.exception(e)
pass # we could be offline, in a local game, so no point in erroring out
return ip
@@ -210,7 +208,7 @@ def get_public_ipv6() -> str:
ip = socket.gethostbyname(socket.gethostname())
ctx = get_cert_none_ssl_context()
try:
ip = urllib.request.urlopen("https://v6.ident.me", context=ctx).read().decode("utf8").strip()
ip = urllib.request.urlopen('https://v6.ident.me', context=ctx).read().decode('utf8').strip()
except Exception as e:
logging.exception(e)
pass # we could be offline, in a local game, or ipv6 may not be available
@@ -279,12 +277,7 @@ def get_default_options() -> dict:
},
"oot_options": {
"rom_file": "The Legend of Zelda - Ocarina of Time.z64",
},
"dkc3_options": {
"rom_file": "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc",
"sni": "SNI",
"rom_start": True,
},
}
}
return options
@@ -311,19 +304,33 @@ def update_options(src: dict, dest: dict, filename: str, keys: list) -> dict:
@cache_argsless
def get_options() -> dict:
filenames = ("options.yaml", "host.yaml")
locations = []
if os.path.join(os.getcwd()) != local_path():
locations += filenames # use files from cwd only if it's not the local_path
locations += [user_path(filename) for filename in filenames]
if not hasattr(get_options, "options"):
filenames = ("options.yaml", "host.yaml")
locations = []
if os.path.join(os.getcwd()) != local_path():
locations += filenames # use files from cwd only if it's not the local_path
locations += [user_path(filename) for filename in filenames]
for location in locations:
if os.path.exists(location):
with open(location) as f:
options = parse_yaml(f.read())
return update_options(get_default_options(), options, location, list())
for location in locations:
if os.path.exists(location):
with open(location) as f:
options = parse_yaml(f.read())
raise FileNotFoundError(f"Could not find {filenames[1]} to load options.")
get_options.options = update_options(get_default_options(), options, location, list())
break
else:
raise FileNotFoundError(f"Could not find {filenames[1]} to load options.")
return get_options.options
def get_item_name_from_id(code: int) -> str:
from worlds import lookup_any_item_id_to_name
return lookup_any_item_id_to_name.get(code, f'Unknown item (ID:{code})')
def get_location_name_from_id(code: int) -> str:
from worlds import lookup_any_location_id_to_name
return lookup_any_location_id_to_name.get(code, f'Unknown location (ID:{code})')
def persistent_store(category: str, key: typing.Any, value: typing.Any):
@@ -332,10 +339,10 @@ def persistent_store(category: str, key: typing.Any, value: typing.Any):
category = storage.setdefault(category, {})
category[key] = value
with open(path, "wt") as f:
f.write(dump(storage, Dumper=Dumper))
f.write(dump(storage))
def persistent_load() -> typing.Dict[str, dict]:
def persistent_load() -> typing.Dict[dict]:
storage = getattr(persistent_load, "storage", None)
if storage:
return storage
@@ -353,8 +360,8 @@ def persistent_load() -> typing.Dict[str, dict]:
return storage
def get_adjuster_settings(game_name: str):
adjuster_settings = persistent_load().get("adjuster", {}).get(game_name, {})
def get_adjuster_settings(gameName: str):
adjuster_settings = persistent_load().get("adjuster", {}).get(gameName, {})
return adjuster_settings
@@ -370,10 +377,10 @@ def get_unique_identifier():
return uuid
safe_builtins = frozenset((
safe_builtins = {
'set',
'frozenset',
))
}
class RestrictedUnpickler(pickle.Unpickler):
@@ -401,7 +408,8 @@ class RestrictedUnpickler(pickle.Unpickler):
if issubclass(obj, self.options_module.Option):
return obj
# Forbid everything else.
raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden")
raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
(module, name))
def restricted_loads(s):
@@ -410,9 +418,6 @@ def restricted_loads(s):
class KeyedDefaultDict(collections.defaultdict):
"""defaultdict variant that uses the missing key as argument to default_factory"""
default_factory: typing.Callable[[typing.Any], typing.Any]
def __missing__(self, key):
self[key] = value = self.default_factory(key)
return value
@@ -422,10 +427,6 @@ def get_text_between(text: str, start: str, end: str) -> str:
return text[text.index(start) + len(start): text.rindex(end)]
def get_text_after(text: str, start: str) -> str:
return text[text.index(start) + len(start):]
loglevel_mapping = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}
@@ -473,13 +474,9 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, wri
def stream_input(stream, queue):
def queuer():
while 1:
try:
text = stream.readline().strip()
except UnicodeDecodeError as e:
logging.exception(e)
else:
if text:
queue.put_nowait(text)
text = stream.readline().strip()
if text:
queue.put_nowait(text)
from threading import Thread
thread = Thread(target=queuer, name=f"Stream handler for {stream.name}", daemon=True)
@@ -487,11 +484,11 @@ def stream_input(stream, queue):
return thread
def tkinter_center_window(window: "tkinter.Tk") -> None:
def tkinter_center_window(window: Tk):
window.update()
x = int(window.winfo_screenwidth() / 2 - window.winfo_reqwidth() / 2)
y = int(window.winfo_screenheight() / 2 - window.winfo_reqheight() / 2)
window.geometry(f"+{x}+{y}")
xPos = int(window.winfo_screenwidth() / 2 - window.winfo_reqwidth() / 2)
yPos = int(window.winfo_screenheight() / 2 - window.winfo_reqheight() / 2)
window.geometry("+{}+{}".format(xPos, yPos))
class VersionException(Exception):
@@ -508,27 +505,24 @@ def chaining_prefix(index: int, labels: typing.Tuple[str]) -> str:
# noinspection PyPep8Naming
def format_SI_prefix(value, power=1000, power_labels=("", "k", "M", "G", "T", "P", "E", "Z", "Y")) -> str:
def format_SI_prefix(value, power=1000, power_labels=('', 'k', 'M', 'G', 'T', "P", "E", "Z", "Y")) -> str:
"""Formats a value into a value + metric/si prefix. More info at https://en.wikipedia.org/wiki/Metric_prefix"""
import decimal
n = 0
value = decimal.Decimal(value)
limit = power - decimal.Decimal("0.005")
while value >= limit:
while value >= power:
value /= power
n += 1
return f"{value.quantize(decimal.Decimal('1.00'))} {chaining_prefix(n, power_labels)}"
def get_fuzzy_ratio(word1: str, word2: str) -> float:
return (1 - jellyfish.damerau_levenshtein_distance(word1.lower(), word2.lower())
/ max(len(word1), len(word2)))
def get_fuzzy_results(input_word: str, wordlist: typing.Sequence[str], limit: typing.Optional[int] = None) \
-> typing.List[typing.Tuple[str, int]]:
import jellyfish
def get_fuzzy_ratio(word1: str, word2: str) -> float:
return (1 - jellyfish.damerau_levenshtein_distance(word1.lower(), word2.lower())
/ max(len(word1), len(word2)))
limit: int = limit if limit else len(wordlist)
return list(
map(
@@ -546,19 +540,18 @@ def get_fuzzy_results(input_word: str, wordlist: typing.Sequence[str], limit: ty
def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typing.Sequence[str]]]) \
-> typing.Optional[str]:
def run(*args: str):
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
return subprocess.run(args, capture_output=True, text=True).stdout.split('\n', 1)[0] or None
if is_linux:
# prefer native dialog
from shutil import which
kdialog = which("kdialog")
kdialog = shutil.which('kdialog')
if kdialog:
k_filters = '|'.join((f'{text} (*{" *".join(ext)})' for (text, ext) in filetypes))
return run(kdialog, f"--title={title}", "--getopenfilename", ".", k_filters)
zenity = which("zenity")
return run(kdialog, f'--title={title}', '--getopenfilename', '.', k_filters)
zenity = shutil.which('zenity')
if zenity:
z_filters = (f'--file-filter={text} ({", ".join(ext)}) | *{" *".join(ext)}' for (text, ext) in filetypes)
return run(zenity, f"--title={title}", "--file-selection", *z_filters)
return run(zenity, f'--title={title}', '--file-selection', *z_filters)
# fall back to tk
try:
@@ -576,10 +569,10 @@ def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typin
def messagebox(title: str, text: str, error: bool = False) -> None:
def run(*args: str):
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
return subprocess.run(args, capture_output=True, text=True).stdout.split('\n', 1)[0] or None
def is_kivy_running():
if "kivy" in sys.modules:
if 'kivy' in sys.modules:
from kivy.app import App
return App.get_running_app() is not None
return False
@@ -589,15 +582,14 @@ def messagebox(title: str, text: str, error: bool = False) -> None:
MessageBox(title, text, error).open()
return
if is_linux and "tkinter" not in sys.modules:
if is_linux and not 'tkinter' in sys.modules:
# prefer native dialog
from shutil import which
kdialog = which("kdialog")
kdialog = shutil.which('kdialog')
if kdialog:
return run(kdialog, f"--title={title}", "--error" if error else "--msgbox", text)
zenity = which("zenity")
return run(kdialog, f'--title={title}', '--error' if error else '--msgbox', text)
zenity = shutil.which('zenity')
if zenity:
return run(zenity, f"--title={title}", f"--text={text}", "--error" if error else "--info")
return run(zenity, f'--title={title}', f'--text={text}', '--error' if error else '--info')
# fall back to tk
try:
@@ -612,14 +604,3 @@ def messagebox(title: str, text: str, error: bool = False) -> None:
root.withdraw()
showerror(title, text) if error else showinfo(title, text)
root.update()
def title_sorted(data: typing.Sequence, key=None, ignore: typing.Set = frozenset(("a", "the"))):
"""Sorts a sequence of text ignoring typical articles like "a" or "the" in the beginning."""
def sorter(element: str) -> str:
parts = element.split(maxsplit=1)
if parts[0].lower() in ignore:
return parts[1]
else:
return element
return sorted(data, key=lambda i: sorter(key(i)) if key else sorter(i))

View File

@@ -14,7 +14,7 @@ import Utils
Utils.local_path.cached_path = os.path.dirname(__file__)
from WebHostLib import register, app as raw_app
from WebHostLib import app as raw_app
from waitress import serve
from WebHostLib.models import db
@@ -22,13 +22,14 @@ from WebHostLib.autolauncher import autohost, autogen
from WebHostLib.lttpsprites import update_sprites_lttp
from WebHostLib.options import create as create_options_files
from worlds.AutoWorld import AutoWorldRegister
configpath = os.path.abspath("config.yaml")
if not os.path.exists(configpath): # fall back to config.yaml in home
configpath = os.path.abspath(Utils.user_path('config.yaml'))
def get_app():
register()
app = raw_app
if os.path.exists(configpath):
import yaml
@@ -42,39 +43,19 @@ def get_app():
def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]]:
import json
import shutil
import zipfile
zfile: zipfile.ZipInfo
from worlds.AutoWorld import AutoWorldRegister
worlds = {}
data = []
for game, world in AutoWorldRegister.world_types.items():
if hasattr(world.web, 'tutorials') and (not world.hidden or game == 'Archipelago'):
worlds[game] = world
base_target_path = Utils.local_path("WebHostLib", "static", "generated", "docs")
for game, world in worlds.items():
# copy files from world's docs folder to the generated folder
target_path = os.path.join(base_target_path, game)
os.makedirs(target_path, exist_ok=True)
if world.zip_path:
zipfile_path = world.zip_path
assert os.path.isfile(zipfile_path), f"{zipfile_path} is not a valid file(path)."
assert zipfile.is_zipfile(zipfile_path), f"{zipfile_path} is not a valid zipfile."
with zipfile.ZipFile(zipfile_path) as zf:
for zfile in zf.infolist():
if not zfile.is_dir() and "/docs/" in zfile.filename:
zf.extract(zfile, target_path)
else:
source_path = Utils.local_path(os.path.dirname(world.__file__), "docs")
files = os.listdir(source_path)
for file in files:
shutil.copyfile(Utils.local_path(source_path, file), Utils.local_path(target_path, file))
source_path = Utils.local_path(os.path.dirname(sys.modules[world.__module__].__file__), 'docs')
target_path = Utils.local_path("WebHostLib", "static", "generated", "docs", game)
files = os.listdir(source_path)
for file in files:
os.makedirs(os.path.dirname(Utils.local_path(target_path, file)), exist_ok=True)
shutil.copyfile(Utils.local_path(source_path, file), Utils.local_path(target_path, file))
# build a json tutorial dict per game
game_data = {'gameTitle': game, 'tutorials': []}
for tutorial in world.web.tutorials:
@@ -104,7 +85,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] + sorted(data, key=lambda entry: entry["gameTitle"].lower())
json.dump(sorted_data, json_target, indent=2, ensure_ascii=False)
return sorted_data

View File

@@ -1,46 +0,0 @@
# 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.

View File

@@ -3,13 +3,13 @@ import uuid
import base64
import socket
import jinja2.exceptions
from pony.flask import Pony
from flask import Flask
from flask import Flask, request, redirect, url_for, render_template, Response, session, abort, send_from_directory
from flask_caching import Cache
from flask_compress import Compress
from werkzeug.routing import BaseConverter
from worlds.AutoWorld import AutoWorldRegister
from Utils import title_sorted
from .models import *
UPLOAD_FOLDER = os.path.relpath('uploads')
@@ -53,6 +53,8 @@ app.config["PATCH_TARGET"] = "archipelago.gg"
cache = Cache(app)
Compress(app)
from werkzeug.routing import BaseConverter
class B64UUIDConverter(BaseConverter):
@@ -66,18 +68,170 @@ class B64UUIDConverter(BaseConverter):
# short UUID
app.url_map.converters["suuid"] = B64UUIDConverter
app.jinja_env.filters['suuid'] = lambda value: base64.urlsafe_b64encode(value.bytes).rstrip(b'=').decode('ascii')
app.jinja_env.filters["title_sorted"] = title_sorted
def register():
"""Import submodules, triggering their registering on flask routing.
Note: initializes worlds subsystem."""
# has automatic patch integration
import Patch
app.jinja_env.filters['supports_apdeltapatch'] = lambda game_name: game_name in Patch.AutoPatchRegister.patch_types
def get_world_theme(game_name: str):
if game_name in AutoWorldRegister.world_types:
return AutoWorldRegister.world_types[game_name].web.theme
return 'grass'
from WebHostLib.customserver import run_server_process
# to trigger app routing picking up on it
from . import tracker, upload, landing, check, generate, downloads, api, stats, misc
app.register_blueprint(api.api_endpoints)
@app.before_request
def register_session():
session.permanent = True # technically 31 days after the last visit
if not session.get("_id", None):
session["_id"] = uuid4() # uniquely identify each session without needing a login
@app.errorhandler(404)
@app.errorhandler(jinja2.exceptions.TemplateNotFound)
def page_not_found(err):
return render_template('404.html'), 404
# Start Playing Page
@app.route('/start-playing')
def start_playing():
return render_template(f"startPlaying.html")
@app.route('/weighted-settings')
def weighted_settings():
return render_template(f"weighted-settings.html")
# Player settings pages
@app.route('/games/<string:game>/player-settings')
def player_settings(game):
return render_template(f"player-settings.html", game=game, theme=get_world_theme(game))
# Game Info Pages
@app.route('/games/<string:game>/info/<string:lang>')
def game_info(game, lang):
return render_template('gameInfo.html', game=game, lang=lang, theme=get_world_theme(game))
# List of supported games
@app.route('/games')
def games():
worlds = {}
for game, world in AutoWorldRegister.world_types.items():
if not world.hidden:
worlds[game] = world
return render_template("supportedGames.html", worlds=worlds)
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
def tutorial(game, file, lang):
return render_template("tutorial.html", game=game, file=file, lang=lang, theme=get_world_theme(game))
@app.route('/tutorial/')
def tutorial_landing():
worlds = {}
for game, world in AutoWorldRegister.world_types.items():
if not world.hidden:
worlds[game] = world
return render_template("tutorialLanding.html")
@app.route('/faq/<string:lang>/')
def faq(lang):
return render_template("faq.html", lang=lang)
@app.route('/glossary/<string:lang>/')
def terms(lang):
return render_template("glossary.html", lang=lang)
@app.route('/seed/<suuid:seed>')
def view_seed(seed: UUID):
seed = Seed.get(id=seed)
if not seed:
abort(404)
return render_template("viewSeed.html", seed=seed, slot_count=count(seed.slots))
@app.route('/new_room/<suuid:seed>')
def new_room(seed: UUID):
seed = Seed.get(id=seed)
if not seed:
abort(404)
room = Room(seed=seed, owner=session["_id"], tracker=uuid4())
commit()
return redirect(url_for("host_room", room=room.id))
def _read_log(path: str):
if os.path.exists(path):
with open(path, encoding="utf-8-sig") as log:
yield from log
else:
yield f"Logfile {path} does not exist. " \
f"Likely a crash during spinup of multiworld instance or it is still spinning up."
@app.route('/log/<suuid:room>')
def display_log(room: UUID):
room = Room.get(id=room)
if room is None:
return abort(404)
if room.owner == session["_id"]:
return Response(_read_log(os.path.join("logs", str(room.id) + ".txt")), mimetype="text/plain;charset=UTF-8")
return "Access Denied", 403
@app.route('/room/<suuid:room>', methods=['GET', 'POST'])
def host_room(room: UUID):
room = Room.get(id=room)
if room is None:
return abort(404)
if request.method == "POST":
if room.owner == session["_id"]:
cmd = request.form["cmd"]
if cmd:
Command(room=room, commandtext=cmd)
commit()
with db_session:
room.last_activity = datetime.utcnow() # will trigger a spinup, if it's not already running
return render_template("hostRoom.html", room=room)
@app.route('/favicon.ico')
def favicon():
return send_from_directory(os.path.join(app.root_path, 'static/static'),
'favicon.ico', mimetype='image/vnd.microsoft.icon')
@app.route('/discord')
def discord():
return redirect("https://discord.gg/archipelago")
@app.route('/datapackage')
@cache.cached()
def get_datapackge():
"""A pretty print version of /api/datapackage"""
from worlds import network_data_package
import json
return Response(json.dumps(network_data_package, indent=4), mimetype="text/plain")
@app.route('/index')
@app.route('/sitemap')
def get_sitemap():
available_games = []
for game, world in AutoWorldRegister.world_types.items():
if not world.hidden:
available_games.append(game)
return render_template("siteMap.html", games=available_games)
from WebHostLib.customserver import run_server_process
from . import tracker, upload, landing, check, generate, downloads, api, stats # to trigger app routing picking up on it
app.register_blueprint(api.api_endpoints)

View File

@@ -32,14 +32,14 @@ def room_info(room: UUID):
@api_endpoints.route('/datapackage')
@cache.cached()
def get_datapackage():
def get_datapackge():
from worlds import network_data_package
return network_data_package
@api_endpoints.route('/datapackage_version')
@cache.cached()
def get_datapackage_versions():
def get_datapackge_versions():
from worlds import network_data_package, AutoWorldRegister
version_package = {game: world.data_version for game, world in AutoWorldRegister.world_types.items()}
version_package["version"] = network_data_package["version"]

View File

@@ -154,10 +154,8 @@ def autogen(config: dict):
while 1:
time.sleep(0.1)
with db_session:
# for update locks the database row(s) during transaction, preventing writes from elsewhere
to_start = select(
generation for generation in Generation
if generation.state == STATE_QUEUED).for_update()
generation for generation in Generation if generation.state == STATE_QUEUED)
for generation in to_start:
launch_generator(generator_pool, generation)
except AlreadyRunningException:
@@ -184,7 +182,7 @@ class MultiworldInstance():
logging.info(f"Spinning up {self.room_id}")
process = multiprocessing.Process(group=None, target=run_server_process,
args=(self.room_id, self.ponyconfig, get_static_server_data()),
args=(self.room_id, self.ponyconfig),
name="MultiHost")
process.start()
# bind after start to prevent thread sync issues with guardian.
@@ -238,5 +236,5 @@ def run_guardian():
from .models import Room, Generation, STATE_QUEUED, STATE_STARTED, STATE_ERROR, db, Seed
from .customserver import run_server_process, get_static_server_data
from .customserver import run_server_process
from .generate import gen_game

View File

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

View File

@@ -9,13 +9,12 @@ import time
import random
import pickle
import logging
import datetime
import Utils
from .models import db_session, Room, select, commit, Command, db
from .models import *
from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor
from Utils import get_public_ipv4, get_public_ipv6, restricted_loads, cache_argsless
from Utils import get_public_ipv4, get_public_ipv6, restricted_loads
class CustomClientMessageProcessor(ClientMessageProcessor):
@@ -40,7 +39,7 @@ class CustomClientMessageProcessor(ClientMessageProcessor):
import MultiServer
MultiServer.client_message_processor = CustomClientMessageProcessor
del MultiServer
del (MultiServer)
class DBCommandProcessor(ServerCommandProcessor):
@@ -49,20 +48,12 @@ class DBCommandProcessor(ServerCommandProcessor):
class WebHostContext(Context):
def __init__(self, static_server_data: dict):
# static server data is used during _load_game_data to load required data,
# without needing to import worlds system, which takes quite a bit of memory
self.static_server_data = static_server_data
def __init__(self):
super(WebHostContext, self).__init__("", 0, "", "", 1, 40, True, "enabled", "enabled", "enabled", 0, 2)
del self.static_server_data
self.main_loop = asyncio.get_running_loop()
self.video = {}
self.tags = ["AP", "WebHost"]
def _load_game_data(self):
for key, value in self.static_server_data.items():
setattr(self, key, value)
def listen_to_db_commands(self):
cmdprocessor = DBCommandProcessor(self)
@@ -116,32 +107,14 @@ def get_random_port():
return random.randint(49152, 65535)
@cache_argsless
def get_static_server_data() -> dict:
import worlds
data = {
"forced_auto_forfeits": {},
"non_hintable_names": {},
"gamespackage": worlds.network_data_package["games"],
"item_name_groups": {world_name: world.item_name_groups for world_name, world in
worlds.AutoWorldRegister.world_types.items()},
}
for world_name, world in worlds.AutoWorldRegister.world_types.items():
data["forced_auto_forfeits"][world_name] = world.forced_auto_forfeit
data["non_hintable_names"][world_name] = world.hint_blacklist
return data
def run_server_process(room_id, ponyconfig: dict, static_server_data: dict):
def run_server_process(room_id, ponyconfig: dict):
# establish DB connection for multidata and multisave
db.bind(**ponyconfig)
db.generate_mapping(check_tables=False)
async def main():
Utils.init_logging(str(room_id), write_mode="a")
ctx = WebHostContext(static_server_data)
ctx = WebHostContext()
ctx.load(room_id)
ctx.init_save()

View File

@@ -36,14 +36,14 @@ def download_patch(room_id, patch_id):
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}"
new_file.seek(0)
return send_file(new_file, as_attachment=True, download_name=fname)
return send_file(new_file, as_attachment=True, attachment_filename=fname)
else:
patch_data = update_patch_data(patch.data, server=f"{app.config['PATCH_TARGET']}:{last_port}")
patch_data = BytesIO(patch_data)
fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](room_id)}." \
f"{preferred_endings[patch.game]}"
return send_file(patch_data, as_attachment=True, download_name=fname)
return send_file(patch_data, as_attachment=True, attachment_filename=fname)
@app.route("/dl_spoiler/<suuid:seed_id>")
@@ -66,7 +66,7 @@ def download_slot_file(room_id, player_id: int):
from worlds.minecraft import mc_update_output
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apmc"
data = mc_update_output(slot_data.data, server=app.config['PATCH_TARGET'], port=room.last_port)
return send_file(io.BytesIO(data), as_attachment=True, download_name=fname)
return send_file(io.BytesIO(data), as_attachment=True, attachment_filename=fname)
elif slot_data.game == "Factorio":
with zipfile.ZipFile(io.BytesIO(slot_data.data)) as zf:
for name in zf.namelist():
@@ -78,11 +78,9 @@ def download_slot_file(room_id, player_id: int):
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_SP.apv6"
elif slot_data.game == "Super Mario 64":
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_SP.apsm64ex"
elif slot_data.game == "Dark Souls III":
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}.json"
else:
return "Game download not supported."
return send_file(io.BytesIO(slot_data.data), as_attachment=True, download_name=fname)
return send_file(io.BytesIO(slot_data.data), as_attachment=True, attachment_filename=fname)
@app.route("/templates")

View File

@@ -4,7 +4,7 @@ import random
import json
import zipfile
from collections import Counter
from typing import Dict, Optional, Any
from typing import Dict, Optional as TypeOptional
from Utils import __version__
from flask import request, flash, redirect, url_for, session, render_template
@@ -12,10 +12,10 @@ from flask import request, flash, redirect, url_for, session, render_template
from worlds.alttp.EntranceRandomizer import parse_arguments
from Main import main as ERmain
from BaseClasses import seeddigits, get_seed
from Generate import handle_name, PlandoSettings
from Generate import handle_name
import pickle
from .models import Generation, STATE_ERROR, STATE_QUEUED, commit, db_session, Seed, UUID
from .models import *
from WebHostLib import app
from .check import get_yaml_data, roll_options
from .upload import upload_zip_to_db
@@ -30,15 +30,16 @@ def get_meta(options_source: dict) -> dict:
}
plando_options -= {""}
server_options = {
meta = {
"hint_cost": int(options_source.get("hint_cost", 10)),
"forfeit_mode": options_source.get("forfeit_mode", "goal"),
"remaining_mode": options_source.get("remaining_mode", "disabled"),
"collect_mode": options_source.get("collect_mode", "disabled"),
"item_cheat": bool(int(options_source.get("item_cheat", 1))),
"server_password": options_source.get("server_password", None),
"plando_options": list(plando_options)
}
return {"server_options": server_options, "plando_options": list(plando_options)}
return meta
@app.route('/generate', methods=['GET', 'POST'])
@@ -59,13 +60,13 @@ def generate(race=False):
results, gen_options = roll_options(options, meta["plando_options"])
if race:
meta["server_options"]["item_cheat"] = False
meta["server_options"]["remaining_mode"] = "disabled"
meta["item_cheat"] = False
meta["remaining_mode"] = "disabled"
if any(type(result) == str for result in results.values()):
return render_template("checkResult.html", results=results)
elif len(gen_options) > app.config["MAX_ROLL"]:
flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players. "
flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players for now. "
f"If you have a larger group, please generate it yourself and upload it.")
elif len(gen_options) >= app.config["JOB_THRESHOLD"]:
gen = Generation(
@@ -91,35 +92,35 @@ def generate(race=False):
return render_template("generate.html", race=race, version=__version__)
def gen_game(gen_options, meta: Optional[Dict[str, Any]] = None, owner=None, sid=None):
def gen_game(gen_options, meta: TypeOptional[Dict[str, object]] = None, owner=None, sid=None):
if not meta:
meta: Dict[str, Any] = {}
meta.setdefault("server_options", {}).setdefault("hint_cost", 10)
race = meta.setdefault("race", False)
meta: Dict[str, object] = {}
meta.setdefault("hint_cost", 10)
race = meta.get("race", False)
del (meta["race"])
plando_options = meta.get("plando", {"bosses", "items", "connections", "texts"})
del (meta["plando_options"])
try:
target = tempfile.TemporaryDirectory()
playercount = len(gen_options)
seed = get_seed()
random.seed(seed)
if race:
random.seed() # use time-based random source
else:
random.seed(seed)
random.seed() # reset to time-based random source
seedname = "W" + (f"{random.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits))
erargs = parse_arguments(['--multi', str(playercount)])
erargs.seed = seed
erargs.name = {x: "" for x in range(1, playercount + 1)} # only so it can be overwritten in mystery
erargs.name = {x: "" for x in range(1, playercount + 1)} # only so it can be overwrittin in mystery
erargs.spoiler = 0 if race else 2
erargs.race = race
erargs.outputname = seedname
erargs.outputpath = target.name
erargs.teams = 1
erargs.plando_options = PlandoSettings.from_set(meta.setdefault("plando_options",
{"bosses", "items", "connections", "texts"}))
erargs.plando_options = ", ".join(plando_options)
name_counter = Counter()
for player, (playerfile, settings) in enumerate(gen_options.items(), 1):
@@ -135,7 +136,7 @@ def gen_game(gen_options, meta: Optional[Dict[str, Any]] = None, owner=None, sid
erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
if len(set(erargs.name.values())) != len(erargs.name):
raise Exception(f"Names have to be unique. Names: {Counter(erargs.name.values())}")
ERmain(erargs, seed, baked_server_options=meta["server_options"])
ERmain(erargs, seed, baked_server_options=meta)
return upload_to_db(target.name, sid, owner, race)
except BaseException as e:
@@ -147,6 +148,7 @@ def gen_game(gen_options, meta: Optional[Dict[str, Any]] = None, owner=None, sid
meta = json.loads(gen.meta)
meta["error"] = (e.__class__.__name__ + ": " + str(e))
gen.meta = json.dumps(meta)
commit()
raise

View File

@@ -1,173 +0,0 @@
import datetime
import os
import jinja2.exceptions
from flask import request, redirect, url_for, render_template, Response, session, abort, send_from_directory
from .models import count, Seed, commit, Room, db_session, Command, UUID, uuid4
from worlds.AutoWorld import AutoWorldRegister
from . import app, cache
def get_world_theme(game_name: str):
if game_name in AutoWorldRegister.world_types:
return AutoWorldRegister.world_types[game_name].web.theme
return 'grass'
@app.before_request
def register_session():
session.permanent = True # technically 31 days after the last visit
if not session.get("_id", None):
session["_id"] = uuid4() # uniquely identify each session without needing a login
@app.errorhandler(404)
@app.errorhandler(jinja2.exceptions.TemplateNotFound)
def page_not_found(err):
return render_template('404.html'), 404
# Start Playing Page
@app.route('/start-playing')
def start_playing():
return render_template(f"startPlaying.html")
@app.route('/weighted-settings')
def weighted_settings():
return render_template(f"weighted-settings.html")
# Player settings pages
@app.route('/games/<string:game>/player-settings')
def player_settings(game):
return render_template(f"player-settings.html", game=game, theme=get_world_theme(game))
# Game Info Pages
@app.route('/games/<string:game>/info/<string:lang>')
def game_info(game, lang):
return render_template('gameInfo.html', game=game, lang=lang, theme=get_world_theme(game))
# List of supported games
@app.route('/games')
def games():
worlds = {}
for game, world in AutoWorldRegister.world_types.items():
if not world.hidden:
worlds[game] = world
return render_template("supportedGames.html", worlds=worlds)
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
def tutorial(game, file, lang):
return render_template("tutorial.html", game=game, file=file, lang=lang, theme=get_world_theme(game))
@app.route('/tutorial/')
def tutorial_landing():
worlds = {}
for game, world in AutoWorldRegister.world_types.items():
if not world.hidden:
worlds[game] = world
return render_template("tutorialLanding.html")
@app.route('/faq/<string:lang>/')
def faq(lang):
return render_template("faq.html", lang=lang)
@app.route('/glossary/<string:lang>/')
def terms(lang):
return render_template("glossary.html", lang=lang)
@app.route('/seed/<suuid:seed>')
def view_seed(seed: UUID):
seed = Seed.get(id=seed)
if not seed:
abort(404)
return render_template("viewSeed.html", seed=seed, slot_count=count(seed.slots))
@app.route('/new_room/<suuid:seed>')
def new_room(seed: UUID):
seed = Seed.get(id=seed)
if not seed:
abort(404)
room = Room(seed=seed, owner=session["_id"], tracker=uuid4())
commit()
return redirect(url_for("host_room", room=room.id))
def _read_log(path: str):
if os.path.exists(path):
with open(path, encoding="utf-8-sig") as log:
yield from log
else:
yield f"Logfile {path} does not exist. " \
f"Likely a crash during spinup of multiworld instance or it is still spinning up."
@app.route('/log/<suuid:room>')
def display_log(room: UUID):
room = Room.get(id=room)
if room is None:
return abort(404)
if room.owner == session["_id"]:
return Response(_read_log(os.path.join("logs", str(room.id) + ".txt")), mimetype="text/plain;charset=UTF-8")
return "Access Denied", 403
@app.route('/room/<suuid:room>', methods=['GET', 'POST'])
def host_room(room: UUID):
room: Room = Room.get(id=room)
if room is None:
return abort(404)
if request.method == "POST":
if room.owner == session["_id"]:
cmd = request.form["cmd"]
if cmd:
Command(room=room, commandtext=cmd)
commit()
now = datetime.datetime.utcnow()
# indicate that the page should reload to get the assigned port
should_refresh = not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3)
with db_session:
room.last_activity = now # will trigger a spinup, if it's not already running
return render_template("hostRoom.html", room=room, should_refresh=should_refresh)
@app.route('/favicon.ico')
def favicon():
return send_from_directory(os.path.join(app.root_path, 'static/static'),
'favicon.ico', mimetype='image/vnd.microsoft.icon')
@app.route('/discord')
def discord():
return redirect("https://discord.gg/archipelago")
@app.route('/datapackage')
@cache.cached()
def get_datapackage():
"""A pretty print version of /api/datapackage"""
from worlds import network_data_package
import json
return Response(json.dumps(network_data_package, indent=4), mimetype="text/plain")
@app.route('/index')
@app.route('/sitemap')
def get_sitemap():
available_games = []
for game, world in AutoWorldRegister.world_types.items():
if not world.hidden:
available_games.append(game)
return render_template("siteMap.html", games=available_games)

View File

@@ -27,7 +27,7 @@ class Room(db.Entity):
seed = Required('Seed', index=True)
multisave = Optional(buffer, lazy=True)
show_spoiler = Required(int, default=0) # 0 -> never, 1 -> after completion, -> 2 always
timeout = Required(int, default=lambda: 2 * 60 * 60) # seconds since last activity to shutdown
timeout = Required(int, default=lambda: 6 * 60 * 60) # seconds since last activity to shutdown
tracker = Optional(UUID, index=True)
last_port = Optional(int, default=lambda: 0)

View File

@@ -60,7 +60,7 @@ def create():
for game_name, world in AutoWorldRegister.world_types.items():
all_options = {**Options.per_game_common_options, **world.option_definitions}
all_options = {**world.options, **Options.per_game_common_options}
res = Template(open(os.path.join("WebHostLib", "templates", "options.yaml")).read()).render(
options=all_options,
__version__=__version__, game=game_name, yaml_dump=yaml.dump,
@@ -110,7 +110,7 @@ def create():
if option.default == "random":
this_option["defaultValue"] = "random"
elif issubclass(option, Options.Range):
elif hasattr(option, "range_start") and hasattr(option, "range_end"):
game_options[option_name] = {
"type": "range",
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
@@ -121,7 +121,7 @@ def create():
"max": option.range_end,
}
if issubclass(option, Options.SpecialRange):
if hasattr(option, "special_range_names"):
game_options[option_name]["type"] = 'special_range'
game_options[option_name]["value_names"] = {}
for key, val in option.special_range_names.items():
@@ -141,7 +141,7 @@ def create():
"description": option.__doc__ if option.__doc__ else "Please document me!",
}
elif issubclass(option, Options.OptionList) or issubclass(option, Options.OptionSet):
elif hasattr(option, "valid_keys"):
if option.valid_keys:
game_options[option_name] = {
"type": "custom-list",

View File

@@ -1,7 +1,7 @@
flask>=2.1.3
flask>=2.1.2
pony>=0.7.16
waitress>=2.1.1
Flask-Caching>=2.0.1
flask-caching>=1.11.1
Flask-Compress>=1.12
Flask-Limiter>=2.5.0
bokeh>=2.4.3
Flask-Limiter>=2.4.6
bokeh>=2.4.3

View File

@@ -23,7 +23,6 @@ window.addEventListener('load', () => {
games.forEach((game) => {
const gameTitle = document.createElement('h2');
gameTitle.innerText = game.gameTitle;
gameTitle.id = `${encodeURIComponent(game.gameTitle)}`;
tutorialDiv.appendChild(gameTitle);
game.tutorials.forEach((tutorial) => {
@@ -66,15 +65,6 @@ window.addEventListener('load', () => {
showError();
console.error(error);
}
// Check if we are on an anchor when coming in, and scroll to it.
const hash = window.location.hash;
if (hash) {
const offset = 128; // To account for navbar banner at top of page.
window.scrollTo(0, 0);
const rect = document.getElementById(hash.slice(1)).getBoundingClientRect();
window.scrollTo(rect.left, rect.top - offset);
}
};
ajax.open('GET', `${window.location.origin}/static/generated/tutorials.json`, true);
ajax.send();

View File

@@ -1,104 +1,54 @@
from collections import Counter, defaultdict
from colorsys import hsv_to_rgb
from itertools import cycle
from datetime import datetime, timedelta, date
from math import tau
import typing
from bokeh.embed import components
from bokeh.models import HoverTool
from bokeh.palettes import Dark2_8 as palette
from bokeh.plotting import figure, ColumnDataSource
from bokeh.resources import INLINE
from bokeh.colors import RGB
from flask import render_template
from pony.orm import select
from . import app, cache
from .models import Room
PLOT_WIDTH = 600
def get_db_data(known_games: str) -> typing.Tuple[typing.Dict[str, int], typing.Dict[datetime.date, typing.Dict[str, int]]]:
def get_db_data():
games_played = defaultdict(Counter)
total_games = Counter()
cutoff = date.today()-timedelta(days=30)
cutoff = date.today()-timedelta(days=30000)
room: Room
for room in select(room for room in Room if room.creation_time >= cutoff):
for slot in room.seed.slots:
if slot.game in known_games:
total_games[slot.game] += 1
games_played[room.creation_time.date()][slot.game] += 1
total_games[slot.game] += 1
games_played[room.creation_time.date()][slot.game] += 1
return total_games, games_played
def get_color_palette(colors_needed: int) -> typing.List[RGB]:
colors = []
# colors_needed +1 to prevent first and last color being too close to each other
colors_needed += 1
for x in range(0, 361, 360 // colors_needed):
# a bit of noise on value to add some luminosity difference
colors.append(RGB(*(val * 255 for val in hsv_to_rgb(x / 360, 0.8, 0.8 + (x / 1800)))))
# splice colors for maximum hue contrast.
colors = colors[::2] + colors[1::2]
return colors
def create_game_played_figure(all_games_data: typing.Dict[datetime.date, typing.Dict[str, int]],
game: str, color: RGB) -> figure:
occurences = []
days = [day for day, game_data in all_games_data.items() if game_data[game]]
for day in days:
occurences.append(all_games_data[day][game])
data = {
"days": [datetime.combine(day, datetime.min.time()) for day in days],
"played": occurences
}
plot = figure(
title=f"{game} Played Per Day", x_axis_type='datetime', x_axis_label="Date",
y_axis_label="Games Played", sizing_mode="scale_both", width=PLOT_WIDTH, height=500,
toolbar_location=None, tools="",
# setting legend to False seems broken in bokeh currently?
# legend=False
)
hover = HoverTool(tooltips=[("Date:", "@days{%F}"), ("Played:", "@played")], formatters={"@days": "datetime"})
plot.add_tools(hover)
plot.vbar(x="days", top="played", legend_label=game, color=color, source=ColumnDataSource(data=data), width=1)
return plot
@app.route('/stats')
@cache.memoize(timeout=60 * 60) # regen once per hour should be plenty
@cache.memoize(timeout=60*60) # regen once per hour should be plenty
def stats():
from worlds import network_data_package
known_games = set(network_data_package["games"])
plot = figure(title="Games Played Per Day", x_axis_type='datetime', x_axis_label="Date",
y_axis_label="Games Played", sizing_mode="scale_both", width=PLOT_WIDTH, height=500)
y_axis_label="Games Played", sizing_mode="scale_both", width=500, height=500)
total_games, games_played = get_db_data(known_games)
total_games, games_played = get_db_data()
days = sorted(games_played)
color_palette = get_color_palette(len(total_games))
game_to_color: typing.Dict[str, RGB] = {game: color for game, color in zip(total_games, color_palette)}
cyc_palette = cycle(palette)
for game in sorted(total_games):
occurences = []
for day in days:
occurences.append(games_played[day][game])
plot.line([datetime.combine(day, datetime.min.time()) for day in days],
occurences, legend_label=game, line_width=2, color=game_to_color[game])
occurences, legend_label=game, line_width=2, color=next(cyc_palette))
total = sum(total_games.values())
pie = figure(plot_height=350, title=f"Games Played in the Last 30 Days (Total: {total})", toolbar_location=None,
tools="hover", tooltips=[("Game:", "@games"), ("Played:", "@count")],
sizing_mode="scale_both", width=PLOT_WIDTH, height=500, x_range=(-0.5, 1.2))
sizing_mode="scale_both", width=500, height=500)
pie.axis.visible = False
pie.xgrid.visible = False
pie.ygrid.visible = False
data = {
"games": [],
@@ -115,15 +65,12 @@ def stats():
current_angle += angle
data["end_angles"].append(current_angle)
data["colors"] = [game_to_color[game] for game in data["games"]]
pie.wedge(x=0, y=0, radius=0.5,
data["colors"] = [element[1] for element in sorted((game, color) for game, color in
zip(data["games"], cycle(palette)))]
pie.wedge(x=0.5, y=0.5, radius=0.5,
start_angle="start_angles", end_angle="end_angles", fill_color="colors",
source=ColumnDataSource(data=data), legend_field="games")
per_game_charts = [create_game_played_figure(games_played, game, game_to_color[game]) for game in total_games
if total_games[game] > 1]
script, charts = components((plot, pie, *per_game_charts))
script, charts = components((plot, pie))
return render_template("stats.html", js_resources=INLINE.render_js(), css_resources=INLINE.render_css(),
chart_data=script, charts=charts)

View File

@@ -2,7 +2,6 @@
{% import "macros.html" as macros %}
{% block head %}
<title>Multiworld {{ room.id|suuid }}</title>
{% if should_refresh %}<meta http-equiv="refresh" content="2">{% endif %}
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/hostRoom.css") }}"/>
{% endblock %}
@@ -17,9 +16,9 @@
This room has a <a href="{{ url_for("getTracker", tracker=room.tracker) }}">Multiworld Tracker</a> enabled.
<br />
{% endif %}
The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity.
Should you wish to continue later,
anyone can simply refresh this page and the server will resume.<br>
This room will be closed after {{ room.timeout//60//60 }} hours of inactivity. Should you wish to continue
later,
you can simply refresh this page and the server will be started again.<br>
{% if room.last_port %}
You can connect to this room by using <span class="interactive"
data-tooltip="This means address/ip is {{ config['PATCH_TARGET'] }} and port is {{ room.last_port }}.">

View File

@@ -40,12 +40,9 @@
{% elif patch.game == "Super Mario 64" and room.seed.slots|length == 1 %}
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
Download APSM64EX File...</a>
{% elif patch.game | supports_apdeltapatch %}
{% elif patch.game in ["A Link to the Past", "Secret of Evermore", "Super Metroid", "SMZ3"] %}
<a href="{{ url_for("download_patch", patch_id=patch.id, room_id=room.id) }}" download>
Download Patch File...</a>
{% elif patch.game == "Dark Souls III" %}
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
Download JSON File...</a>
{% else %}
No file to download for this game.
{% endif %}

View File

@@ -10,21 +10,15 @@
{% include 'header/oceanHeader.html' %}
<div id="games" class="markdown">
<h1>Currently Supported Games</h1>
{% for game_name in worlds | title_sorted %}
{% set world = worlds[game_name] %}
{% for game_name, world in worlds.items() | sort(attribute=0) %}
<h2>{{ game_name }}</h2>
<p>
{{ world.__doc__ | default("No description provided.", true) }}<br />
<a href="{{ url_for("game_info", game=game_name, lang="en") }}">Game Page</a>
{% if world.web.tutorials %}
<span class="link-spacer">|</span>
<a href="{{ url_for("tutorial_landing") }}#{{ game_name }}">Setup Guides</a>
{% endif %}
{% if world.web.settings_page is string %}
<span class="link-spacer">|</span>
<a href="{{ world.web.settings_page }}">Settings Page</a>
{% elif world.web.settings_page %}
<span class="link-spacer">|</span>
<a href="{{ url_for("player_settings", game=game_name) }}">Settings Page</a>
{% endif %}
{% if world.web.bug_report_page %}

View File

@@ -11,7 +11,7 @@ from worlds.alttp import Items
from WebHostLib import app, cache, Room
from Utils import restricted_loads
from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name
from MultiServer import Context
from MultiServer import get_item_name_from_id, Context
from NetUtils import SlotType
alttp_icons = {
@@ -987,10 +987,10 @@ def getTracker(tracker: UUID):
if game_state == 30:
inventory[team][player][106] = 1 # Triforce
player_big_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1)}
player_small_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1)}
player_big_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1) if playernumber not in groups}
player_small_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1) if playernumber not in groups}
for loc_data in locations.values():
for values in loc_data.values():
for values in loc_data.values():
item_id, item_player, flags = values
if item_id in ids_big_key:
@@ -1021,7 +1021,7 @@ def getTracker(tracker: UUID):
for (team, player), data in multisave.get("video", []):
video[(team, player)] = data
return render_template("tracker.html", inventory=inventory, get_item_name_from_id=lookup_any_item_id_to_name,
return render_template("tracker.html", inventory=inventory, get_item_name_from_id=get_item_name_from_id,
lookup_id_to_name=Items.lookup_id_to_name, player_names=player_names,
tracking_names=tracking_names, tracking_ids=tracking_ids, room=room, icons=alttp_icons,
multi_items=multi_items, checks_done=checks_done, ordered_areas=ordered_areas,

View File

@@ -80,11 +80,6 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
slots.add(Slot(data=zfile.open(file, "r").read(), player_name=slot_name,
player_id=int(slot_id[1:]), game="Ocarina of Time"))
elif file.filename.endswith(".json"):
_, seed_name, slot_id, slot_name = file.filename.split('.')[0].split('-', 3)
slots.add(Slot(data=zfile.open(file, "r").read(), player_name=slot_name,
player_id=int(slot_id[1:]), game="Dark Souls III"))
elif file.filename.endswith(".txt"):
spoiler = zfile.open(file, "r").read().decode("utf-8-sig")

View File

@@ -2,8 +2,8 @@ local socket = require("socket")
local json = require('json')
local math = require('math')
local last_modified_date = '2022-07-24' -- Should be the last modified date
local script_version = 2
local last_modified_date = '2022-05-25' -- Should be the last modified date
local script_version = 1
--------------------------------------------------
-- Heavily modified form of RiptideSage's tracker
@@ -1723,11 +1723,6 @@ function get_death_state()
end
function kill_link()
-- market entrance: 27/28/29
-- outside ToT: 35/36/37.
-- if killed on these scenes the game crashes, so we wait until not on this screen.
local scene = global_context:rawget('cur_scene'):rawget()
if scene == 27 or scene == 28 or scene == 29 or scene == 35 or scene == 36 or scene == 37 then return end
mainmemory.write_u16_be(0x11A600, 0)
end
@@ -1829,15 +1824,13 @@ function main()
elseif (curstate == STATE_UNINITIALIZED) then
if (frame % 60 == 0) then
server:settimeout(2)
print("Attempting to connect")
local client, timeout = server:accept()
if timeout == nil then
print('Initial Connection Made')
curstate = STATE_INITIAL_CONNECTION_MADE
ootSocket = client
ootSocket:settimeout(0)
else
print('Connection failed, ensure OoTClient is running and rerun oot_connector.lua')
return
end
end
end

View File

@@ -1,25 +0,0 @@
# apworld Specification
Archipelago depends on worlds to provide game-specific details like items, locations and output generation.
Those are located in the `worlds/` folder (source) or `<insall dir>/lib/worlds/` (when installed).
See [world api.md](world api.md) for details.
apworld provides a way to package and ship a world that is not part of the main distribution by placing a `*.apworld`
file into the worlds folder.
## File Format
apworld files are zip archives with the case-sensitive file ending `.apworld`.
The zip has to contain a folder with the same name as the zip, case-sensitive, that contains what would normally be in
the world's folder in `worlds/`. I.e. `worlds/ror2.apworld` containing `ror2/__init__.py`.
## Metadata
No metadata is specified yet.
## Extra Data
The zip can contain arbitrary files in addition what was specified above.

BIN
docs/network diagram.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

View File

@@ -8,15 +8,6 @@ flowchart LR
CC[CommonClient.py]
AS <-- WebSockets --> CC
subgraph "Starcraft 2"
SC2[Starcraft 2 Game Client]
SC2C[Starcraft2Client.py]
SC2AI[apsc2 Python Package]
SC2C <--> SC2AI <-- WebSockets --> SC2
end
CC <-- Integrated --> SC2C
%% ChecksFinder
subgraph ChecksFinder
CFC[ChecksFinderClient]
@@ -69,12 +60,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"
@@ -87,16 +72,12 @@ flowchart LR
V6[VVVVVV]
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
end
SOE <--> SNI <-- Various, depending on SNES device --> SOESNES
AS <-- WebSockets --> APCLIENTPP

1
docs/network diagram.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 526 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 92 KiB

View File

@@ -191,23 +191,8 @@ Sent to clients after a client requested this message be sent to them, more info
### 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. |
| 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. |
##### 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 |
| ---- | ----- |
| cmd | `cmd` argument of the faulty packet that could not be parsed correctly. |
| 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 |
| ---- | ---- | ----- |
@@ -501,7 +486,7 @@ Color options:
* green_bg
* yellow_bg
* blue_bg
* magenta_bg
* purple_bg
* cyan_bg
* white_bg

View File

@@ -1,58 +0,0 @@
# Running From Source
If you just want to play and there is a compiled version available on the
[Archipelago releases page](https://github.com/ArchipelagoMW/Archipelago/releases),
use that version. These steps are for developers or platforms without compiled releases available.
## General
What you'll need:
* Python 3.8.7 or newer
* pip (Depending on platform may come included)
* A C compiler
* possibly optional, read OS-specific sections
Then run any of the starting point scripts, like Generate.py, and the included ModuleUpdater should prompt to install or update the
required modules and after pressing enter proceed to install everything automatically.
After this, you should be able to run the programs.
## Windows
Recommended steps
* Download and install a "Windows installer (64-bit)" from the [Python download page](https://www.python.org/downloads)
* Download and install full Visual Studio from
[Visual Studio Downloads](https://visualstudio.microsoft.com/downloads/)
or an older "Build Tools for Visual Studio" from
[Visual Studio Older Downloads](https://visualstudio.microsoft.com/vs/older-downloads/).
* Refer to [Windows Compilers on the python wiki](https://wiki.python.org/moin/WindowsCompilers) for details
* This step is optional. Pre-compiled modules are pinned on
[Discord in #archipelago-dev](https://discord.com/channels/731205301247803413/731214280439103580/905154456377757808)
* It is recommended to use [PyCharm IDE](https://www.jetbrains.com/pycharm/)
* Run Generate.py which will prompt installation of missing modules, press enter to confirm
## macOS
Refer to [Guide to Run Archipelago from Source Code on macOS](../worlds/generic/docs/mac_en.md).
## Optional: A Link to the Past Enemizer
Only required to generate seeds that include A Link to the Past with certain options enabled. You will receive an
error if it is required.
You can get the latest Enemizer release at [Enemizer Github releases](https://github.com/Ijwu/Enemizer/releases).
It should be dropped as "EnemizerCLI" into the root folder of the project. Alternatively, you can point the Enemizer
setting in host.yaml at your Enemizer executable.
## Optional: SNI
SNI is required to use SNIClient. If not integrated into the project, it has to be started manually.
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.

View File

@@ -1,49 +0,0 @@
# Style Guide
## Generic
* This guide can be ignored for data files that are not to be viewed in an editor.
* 120 character per line for all source files.
* Avoid white space errors like trailing spaces.
## Python Code
* We mostly follow [PEP8](https://peps.python.org/pep-0008/). Read below to see the differences.
* 120 characters per line. PyCharm does this automatically, other editors can be configured for it.
* Strings in core code will be `"strings"`. In other words: double quote your strings.
* Strings in worlds should use double quotes as well, but imported code may differ.
* Prefer [format string literals](https://peps.python.org/pep-0498/) over string concatenation,
use single quotes inside them: `f"Like {dct['key']}"`
* Use type annotation where possible.
## Markdown
* We almost follow [Google's styleguide](https://google.github.io/styleguide/docguide/style.html).
Read below for differences.
* For existing documents, try to follow its style or ask to completely reformat it.
* 120 characters per line.
* One space between bullet/number and text.
* No lazy numbering.
## HTML
* Indent with 2 spaces for new code.
* kebab-case for ids and classes.
## CSS
* Indent with 2 spaces for new code.
* `{` on the same line as the selector.
* No space between selector and `{`.
## JS
* Indent with 2 spaces.
* Indent `case` inside `switch ` with 2 spaces.
* Use single quotes.
* Semicolons are required after every statement.

View File

@@ -86,7 +86,7 @@ inside a World object.
Players provide customized settings for their World in the form of yamls.
Those are accessible through `self.world.<option_name>[self.player]`. A dict
of valid options has to be provided in `self.option_definitions`. Options are automatically
of valid options has to be provided in `self.options`. Options are automatically
added to the `World` object for easy access.
### World Options
@@ -236,7 +236,7 @@ class MyGameLocation(Location):
game: str = "My Game"
# override constructor to automatically mark event locations as such
def __init__(self, player: int, name = "", code = None, parent = None):
def __init__(self, player: int, name = '', code = None, parent = None):
super(MyGameLocation, self).__init__(player, name, code, parent)
self.event = code is None
```
@@ -252,7 +252,7 @@ to describe it and a `display_name` property for display on the website and in
spoiler logs.
The actual name as used in the yaml is defined in a `dict[str, Option]`, that is
assigned to the world under `self.option_definitions`.
assigned to the world under `self.options`.
Common option types are `Toggle`, `DefaultOnToggle`, `Choice`, `Range`.
For more see `Options.py` in AP's base directory.
@@ -328,7 +328,7 @@ from .Options import mygame_options # import the options dict
class MyGameWorld(World):
#...
option_definitions = mygame_options # assign the options dict to the world
options = mygame_options # assign the options dict to the world
#...
```
@@ -365,7 +365,7 @@ class MyGameLocation(Location): # or from Locations import MyGameLocation
class MyGameWorld(World):
"""Insert description of the world/game here."""
game: str = "My Game" # name of the game/world
option_definitions = mygame_options # options the player can set
options = mygame_options # options the player can set
topology_present: bool = True # show path to required location checks in spoiler
remote_items: bool = False # True if all items come from the server
remote_start_inventory: bool = False # True if start inventory comes from the server
@@ -487,14 +487,14 @@ def create_items(self) -> None:
for item in map(self.create_item, mygame_items):
if item in exclude:
exclude.remove(item) # this is destructive. create unique list above
self.world.itempool.append(self.create_item("nothing"))
self.world.itempool.append(self.create_item('nothing'))
else:
self.world.itempool.append(item)
# itempool and number of locations should match up.
# If this is not the case we want to fill the itempool with junk.
junk = 0 # calculate this based on player settings
self.world.itempool += [self.create_item("nothing") for _ in range(junk)]
self.world.itempool += [self.create_item('nothing') for _ in range(junk)]
```
#### create_regions
@@ -628,7 +628,7 @@ class MyGameLogic(LogicMixin):
def _mygame_has_key(self, world: MultiWorld, player: int):
# Arguments above are free to choose
# it may make sense to use World as argument instead of MultiWorld
return self.has("key", player) # or whatever
return self.has('key', player) # or whatever
```
```python
# __init__.py

View File

@@ -101,9 +101,7 @@ sm_options:
# Alternatively, a path to a program to open the .sfc file with
rom_start: true
factorio_options:
executable: "factorio/bin/x64/factorio"
# by default, no settings are loaded if this file does not exist. If this file does exist, then it will be used.
# server_settings: "factorio\\data\\server-settings.json"
executable: "factorio\\bin\\x64\\factorio"
minecraft_options:
forge_directory: "Minecraft Forge server"
max_heap_size: "2G"
@@ -129,12 +127,3 @@ smz3_options:
# True for operating system default program
# Alternatively, a path to a program to open the .sfc file with
rom_start: true
dkc3_options:
# File name of the DKC3 US rom
rom_file: "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc"
# Set this to your SNI folder location if you want the MultiClient to attempt an auto start, does nothing if not found
sni: "SNI"
# Set this to false to never autostart a rom (such as after patching)
# True for operating system default program
# Alternatively, a path to a program to open the .sfc file with
rom_start: true

View File

@@ -54,7 +54,6 @@ Name: "custom"; Description: "Custom installation"; Flags: iscustom
Name: "core"; Description: "Core Files"; Types: full hosting playing custom; Flags: fixed
Name: "generator"; Description: "Generator"; Types: full hosting
Name: "generator/sm"; Description: "Super Metroid ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning
Name: "generator/dkc3"; Description: "Donkey Kong Country 3 ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning
Name: "generator/soe"; Description: "Secret of Evermore ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning
Name: "generator/lttp"; Description: "A Link to the Past ROM Setup and Enemizer"; Types: full hosting; ExtraDiskSpaceRequired: 5191680
Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 100663296; Flags: disablenouninstallwarning
@@ -63,7 +62,6 @@ Name: "client"; Description: "Clients"; Types: full playing
Name: "client/sni"; Description: "SNI Client"; Types: full playing
Name: "client/sni/lttp"; Description: "SNI Client - A Link to the Past Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
Name: "client/sni/dkc3"; Description: "SNI Client - Donkey Kong Country 3 Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
Name: "client/factorio"; Description: "Factorio"; Types: full playing
Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278
Name: "client/oot"; Description: "Ocarina of Time"; Types: full playing
@@ -78,7 +76,6 @@ NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-mod
[Files]
Source: "{code:GetROMPath}"; DestDir: "{app}"; DestName: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"; Flags: external; Components: client/sni/lttp or generator/lttp
Source: "{code:GetSMROMPath}"; DestDir: "{app}"; DestName: "Super Metroid (JU).sfc"; Flags: external; Components: client/sni/sm or generator/sm
Source: "{code:GetDKC3ROMPath}"; DestDir: "{app}"; DestName: "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc"; Flags: external; Components: client/sni/dkc3 or generator/dkc3
Source: "{code:GetSoEROMPath}"; DestDir: "{app}"; DestName: "Secret of Evermore (USA).sfc"; Flags: external; Components: generator/soe
Source: "{code:GetOoTROMPath}"; DestDir: "{app}"; DestName: "The Legend of Zelda - Ocarina of Time.z64"; Flags: external; Components: client/oot or generator/oot
Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, Archipelago*.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
@@ -132,7 +129,6 @@ Type: dirifempty; Name: "{app}"
[InstallDelete]
Type: files; Name: "{app}\ArchipelagoLttPClient.exe"
Type: filesandordirs; Name: "{app}\lib\worlds\rogue-legacy*"
[Registry]
@@ -146,11 +142,6 @@ Root: HKCR; Subkey: "{#MyAppName}smpatch"; ValueData: "Archi
Root: HKCR; Subkey: "{#MyAppName}smpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}smpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: ".apdkc3"; ValueData: "{#MyAppName}dkc3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}dkc3patch"; ValueData: "Archipelago Donkey Kong Country 3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}dkc3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}dkc3patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: ".apsmz3"; ValueData: "{#MyAppName}smz3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}smz3patch"; ValueData: "Archipelago SMZ3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}smz3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
@@ -214,9 +205,6 @@ var LttPROMFilePage: TInputFileWizardPage;
var smrom: string;
var SMRomFilePage: TInputFileWizardPage;
var dkc3rom: string;
var DKC3RomFilePage: TInputFileWizardPage;
var soerom: string;
var SoERomFilePage: TInputFileWizardPage;
@@ -306,8 +294,6 @@ begin
Result := not (LttPROMFilePage.Values[0] = '')
else if (assigned(SMROMFilePage)) and (CurPageID = SMROMFilePage.ID) then
Result := not (SMROMFilePage.Values[0] = '')
else if (assigned(DKC3ROMFilePage)) and (CurPageID = DKC3ROMFilePage.ID) then
Result := not (DKC3ROMFilePage.Values[0] = '')
else if (assigned(SoEROMFilePage)) and (CurPageID = SoEROMFilePage.ID) then
Result := not (SoEROMFilePage.Values[0] = '')
else if (assigned(OoTROMFilePage)) and (CurPageID = OoTROMFilePage.ID) then
@@ -348,22 +334,6 @@ begin
Result := '';
end;
function GetDKC3ROMPath(Param: string): string;
begin
if Length(dkc3rom) > 0 then
Result := dkc3rom
else if Assigned(DKC3RomFilePage) then
begin
R := CompareStr(GetSNESMD5OfFile(DKC3ROMFilePage.Values[0]), '120abf304f0c40fe059f6a192ed4f947')
if R <> 0 then
MsgBox('Donkey Kong Country 3 ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
Result := DKC3ROMFilePage.Values[0]
end
else
Result := '';
end;
function GetSoEROMPath(Param: string): string;
begin
if Length(soerom) > 0 then
@@ -408,10 +378,6 @@ begin
if Length(smrom) = 0 then
SMRomFilePage:= AddRomPage('Super Metroid (JU).sfc');
dkc3rom := CheckRom('Donkey Kong Country 3 - Dixie Kong''s Double Trouble! (USA) (En,Fr).sfc', '120abf304f0c40fe059f6a192ed4f947');
if Length(dkc3rom) = 0 then
DKC3RomFilePage:= AddRomPage('Donkey Kong Country 3 - Dixie Kong''s Double Trouble! (USA) (En,Fr).sfc');
soerom := CheckRom('Secret of Evermore (USA).sfc', '6e9c94511d04fac6e0a1e582c170be3a');
if Length(soerom) = 0 then
SoEROMFilePage:= AddRomPage('Secret of Evermore (USA).sfc');
@@ -425,8 +391,6 @@ begin
Result := not (WizardIsComponentSelected('client/sni/lttp') or WizardIsComponentSelected('generator/lttp'));
if (assigned(SMROMFilePage)) and (PageID = SMROMFilePage.ID) then
Result := not (WizardIsComponentSelected('client/sni/sm') or WizardIsComponentSelected('generator/sm'));
if (assigned(DKC3ROMFilePage)) and (PageID = DKC3ROMFilePage.ID) then
Result := not (WizardIsComponentSelected('client/sni/dkc3') or WizardIsComponentSelected('generator/dkc3'));
if (assigned(SoEROMFilePage)) and (PageID = SoEROMFilePage.ID) then
Result := not (WizardIsComponentSelected('generator/soe'));
if (assigned(OoTROMFilePage)) and (PageID = OoTROMFilePage.ID) then

View File

@@ -175,15 +175,12 @@ A Link to the Past:
retro_caves:
on: 0 # Zelda-1 like mode. There are randomly placed take-any caves that contain one Sword and choices of Heart Container/Blue Potion.
off: 50
hints: # On/Full: Put item and entrance placement hints on telepathic tiles and some NPCs, Full removes joke hints.
hints: # Vendors: King Zora and Bottle Merchant say what they're selling.
# On/Full: Put item and entrance placement hints on telepathic tiles and some NPCs, Full removes joke hints.
'on': 50
vendors: 0
'off': 0
full: 0
scams: # If on, these Merchants will no longer tell you what they're selling.
'off': 50
'king_zora': 0
'bottle_merchant': 0
'all': 0
swordless:
on: 0 # Your swords are replaced by rupees. Gameplay changes have been made to accommodate this change
off: 1
@@ -276,7 +273,6 @@ A Link to the Past:
p: 0 # Randomize the prices of the items in shop inventories
u: 0 # Shuffle capacity upgrades into the item pool (and allow them to traverse the multiworld)
w: 0 # Consider witch's hut like any other shop and shuffle/randomize it too
P: 0 # Prices of the items in shop inventories cost hearts, arrow, or bombs instead of rupees
ip: 0 # Shuffle inventories and randomize prices
fpu: 0 # Generate new inventories, randomize prices and shuffle capacity upgrades into item pool
uip: 0 # Shuffle inventories, randomize prices and shuffle capacity upgrades into the item pool

View File

@@ -1,4 +1,4 @@
colorama>=0.4.5
colorama>=0.4.4
websockets>=10.3
PyYAML>=6.0
jellyfish>=0.9.0

View File

@@ -16,7 +16,7 @@ class TestDungeon(unittest.TestCase):
def setUp(self):
self.world = MultiWorld(1)
args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args)
self.world.set_default_common_options()

View File

@@ -49,7 +49,8 @@ class PlayerDefinition(object):
region_name = "player" + str(self.id) + region_tag
region = Region("player" + str(self.id) + region_tag, RegionType.Generic,
"Region Hint", self.id, self.world)
self.locations += generate_locations(size, self.id, None, region, region_tag)
self.locations += generate_locations(size,
self.id, None, region, region_tag)
entrance = Entrance(self.id, region_name + "_entrance", parent)
parent.exits.append(entrance)

View File

@@ -12,7 +12,7 @@ def setup_default_world(world_type) -> MultiWorld:
world.player_name = {1: "Tester"}
world.set_seed()
args = Namespace()
for name, option in world_type.option_definitions.items():
for name, option in world_type.options.items():
setattr(args, name, {1: option.from_any(option.default)})
world.set_options(args)
world.set_default_common_options()

View File

@@ -16,7 +16,7 @@ class TestInverted(TestBase):
def setUp(self):
self.world = MultiWorld(1)
args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args)
self.world.set_default_common_options()

View File

@@ -17,7 +17,7 @@ class TestInvertedBombRules(unittest.TestCase):
self.world = MultiWorld(1)
self.world.mode[1] = "inverted"
args = Namespace
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args)
self.world.set_default_common_options()

View File

@@ -17,7 +17,7 @@ class TestInvertedMinor(TestBase):
def setUp(self):
self.world = MultiWorld(1)
args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args)
self.world.set_default_common_options()

View File

@@ -18,7 +18,7 @@ class TestInvertedOWG(TestBase):
def setUp(self):
self.world = MultiWorld(1)
args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args)
self.world.set_default_common_options()

View File

@@ -17,7 +17,7 @@ class TestMinor(TestBase):
def setUp(self):
self.world = MultiWorld(1)
args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args)
self.world.set_default_common_options()

View File

@@ -18,7 +18,7 @@ class TestVanillaOWG(TestBase):
def setUp(self):
self.world = MultiWorld(1)
args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args)
self.world.set_default_common_options()

View File

@@ -1,44 +0,0 @@
# Tests for SI prefix in Utils.py
import unittest
from decimal import Decimal
from Utils import format_SI_prefix
class TestGenerateMain(unittest.TestCase):
"""This tests SI prefix formatting in Utils.py"""
def assertEqual(self, first, second, msg=None):
# we strip spaces everywhere because that is an undefined implementation detail
super().assertEqual(first.replace(" ", ""), second.replace(" ", ""), msg)
def test_rounding(self):
# we don't care if float(999.995) would fail due to error in precision
self.assertEqual(format_SI_prefix(999.999), "1.00k")
self.assertEqual(format_SI_prefix(1000.001), "1.00k")
self.assertEqual(format_SI_prefix(Decimal("999.995")), "1.00k")
self.assertEqual(format_SI_prefix(Decimal("1000.004")), "1.00k")
def test_letters(self):
self.assertEqual(format_SI_prefix(0e0), "0.00")
self.assertEqual(format_SI_prefix(1e3), "1.00k")
self.assertEqual(format_SI_prefix(2e6), "2.00M")
self.assertEqual(format_SI_prefix(3e9), "3.00G")
self.assertEqual(format_SI_prefix(4e12), "4.00T")
self.assertEqual(format_SI_prefix(5e15), "5.00P")
self.assertEqual(format_SI_prefix(6e18), "6.00E")
self.assertEqual(format_SI_prefix(7e21), "7.00Z")
self.assertEqual(format_SI_prefix(8e24), "8.00Y")
def test_multiple_letters(self):
self.assertEqual(format_SI_prefix(9e27), "9.00kY")
def test_custom_power(self):
self.assertEqual(format_SI_prefix(1023.99, 1024), "1023.99")
self.assertEqual(format_SI_prefix(1034.24, 1024), "1.01k")
def test_custom_labels(self):
labels = ("E", "da", "h", "k")
self.assertEqual(format_SI_prefix(1, 10, labels), "1.00E")
self.assertEqual(format_SI_prefix(10, 10, labels), "1.00da")
self.assertEqual(format_SI_prefix(100, 10, labels), "1.00h")
self.assertEqual(format_SI_prefix(1000, 10, labels), "1.00k")

View File

View File

@@ -16,7 +16,7 @@ class TestVanilla(TestBase):
def setUp(self):
self.world = MultiWorld(1)
args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args)
self.world.set_default_common_options()

View File

@@ -2,14 +2,10 @@ from __future__ import annotations
import logging
import sys
import pathlib
from typing import Dict, FrozenSet, Set, Tuple, List, Optional, TextIO, Any, Callable, Union, TYPE_CHECKING
from typing import Dict, FrozenSet, Set, Tuple, List, Optional, TextIO, Any, Callable, Union, NamedTuple
from BaseClasses import MultiWorld, Item, CollectionState, Location, Tutorial
from Options import Option
from BaseClasses import CollectionState
if TYPE_CHECKING:
from BaseClasses import MultiWorld, Item, Location, Tutorial
class AutoWorldRegister(type):
@@ -45,18 +41,14 @@ class AutoWorldRegister(type):
# construct class
new_class = super().__new__(mcs, name, bases, dct)
if "game" in dct:
if dct["game"] in AutoWorldRegister.world_types:
raise RuntimeError(f"""Game {dct["game"]} already registered.""")
AutoWorldRegister.world_types[dct["game"]] = new_class
new_class.__file__ = sys.modules[new_class.__module__].__file__
if ".apworld" in new_class.__file__:
new_class.zip_path = pathlib.Path(new_class.__file__).parents[1]
return new_class
class AutoLogicRegister(type):
def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoLogicRegister:
new_class = super().__new__(mcs, name, bases, dct)
def __new__(cls, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoLogicRegister:
new_class = super().__new__(cls, name, bases, dct)
function: Callable[..., Any]
for item_name, function in dct.items():
if item_name == "copy_mixin":
@@ -70,12 +62,12 @@ class AutoLogicRegister(type):
return new_class
def call_single(world: "MultiWorld", method_name: str, player: int, *args: Any) -> Any:
def call_single(world: MultiWorld, method_name: str, player: int, *args: Any) -> Any:
method = getattr(world.worlds[player], method_name)
return method(*args)
def call_all(world: "MultiWorld", method_name: str, *args: Any) -> None:
def call_all(world: MultiWorld, method_name: str, *args: Any) -> None:
world_types: Set[AutoWorldRegister] = set()
for player in world.player_ids:
world_types.add(world.worlds[player].__class__)
@@ -87,7 +79,7 @@ def call_all(world: "MultiWorld", method_name: str, *args: Any) -> None:
stage_callable(world, *args)
def call_stage(world: "MultiWorld", method_name: str, *args: Any) -> None:
def call_stage(world: MultiWorld, method_name: str, *args: Any) -> None:
world_types = {world.worlds[player].__class__ for player in world.player_ids}
for world_type in world_types:
stage_callable = getattr(world_type, f"stage_{method_name}", None)
@@ -105,7 +97,7 @@ class WebWorld:
# 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"]
tutorials: List[Tutorial]
# Choose a theme for your /game/* pages
# Available: dirt, grass, grassFlowers, ice, jungle, ocean, partyTime, stone
@@ -119,7 +111,7 @@ 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
options: 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
@@ -167,11 +159,8 @@ class World(metaclass=AutoWorldRegister):
# Hide World Type from various views. Does not remove functionality.
hidden: bool = False
# see WebWorld for options
web: WebWorld = WebWorld()
# autoset on creation:
world: "MultiWorld"
world: MultiWorld
player: int
# automatically generated
@@ -181,10 +170,9 @@ class World(metaclass=AutoWorldRegister):
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
web: WebWorld = WebWorld()
def __init__(self, world: "MultiWorld", player: int):
def __init__(self, world: MultiWorld, player: int):
self.world = world
self.player = player
@@ -219,12 +207,12 @@ class World(metaclass=AutoWorldRegister):
@classmethod
def fill_hook(cls,
progitempool: List["Item"],
nonexcludeditempool: List["Item"],
localrestitempool: Dict[int, List["Item"]],
nonlocalrestitempool: Dict[int, List["Item"]],
restitempool: List["Item"],
fill_locations: List["Location"]) -> None:
progitempool: List[Item],
nonexcludeditempool: List[Item],
localrestitempool: Dict[int, List[Item]],
nonlocalrestitempool: Dict[int, List[Item]],
restitempool: List[Item],
fill_locations: List[Location]) -> None:
"""Special method that gets called as part of distribute_items_restrictive (main fill).
This gets called once per present world type."""
pass
@@ -262,7 +250,7 @@ class World(metaclass=AutoWorldRegister):
# end of ordered Main.py calls
def create_item(self, name: str) -> "Item":
def create_item(self, name: str) -> Item:
"""Create an item for this world type and player.
Warning: this may be called with self.world = None, for example by MultiServer"""
raise NotImplementedError
@@ -273,7 +261,7 @@ class World(metaclass=AutoWorldRegister):
return self.world.random.choice(tuple(self.item_name_to_id.keys()))
# decent place to implement progressive items, in most cases can stay as-is
def collect_item(self, state: "CollectionState", item: "Item", remove: bool = False) -> Optional[str]:
def collect_item(self, state: CollectionState, item: Item, remove: bool = False) -> Optional[str]:
"""Collect an item name into state. For speed reasons items that aren't logically useful get skipped.
Collect None to skip item.
:param state: CollectionState to collect into
@@ -284,18 +272,18 @@ class World(metaclass=AutoWorldRegister):
return None
# called to create all_state, return Items that are created during pre_fill
def get_pre_fill_items(self) -> List["Item"]:
def get_pre_fill_items(self) -> List[Item]:
return []
# following methods should not need to be overridden.
def collect(self, state: "CollectionState", item: "Item") -> bool:
def collect(self, state: CollectionState, item: Item) -> bool:
name = self.collect_item(state, item)
if name:
state.prog_items[name, self.player] += 1
return True
return False
def remove(self, state: "CollectionState", item: "Item") -> bool:
def remove(self, state: CollectionState, item: Item) -> bool:
name = self.collect_item(state, item, True)
if name:
state.prog_items[name, self.player] -= 1
@@ -304,7 +292,7 @@ class World(metaclass=AutoWorldRegister):
return True
return False
def create_filler(self) -> "Item":
def create_filler(self) -> Item:
return self.create_item(self.get_filler_item_name())

View File

@@ -1,56 +1,29 @@
import importlib
import zipimport
import os
import typing
folder = os.path.dirname(__file__)
__all__ = {
"lookup_any_item_id_to_name",
"lookup_any_location_id_to_name",
"network_data_package",
"AutoWorldRegister",
"world_sources",
"folder",
}
if typing.TYPE_CHECKING:
from .AutoWorld import World
class WorldSource(typing.NamedTuple):
path: str # typically relative path from this module
is_zip: bool = False
# find potential world containers, currently folders and zip-importable .apworld's
world_sources: typing.List[WorldSource] = []
file: os.DirEntry # for me (Berserker) at least, PyCharm doesn't seem to infer the type correctly
for file in os.scandir(folder):
if not file.name.startswith("_"): # prevent explicitly loading __pycache__ and allow _* names for non-world folders
if file.is_dir():
world_sources.append(WorldSource(file.name))
elif file.is_file() and file.name.endswith(".apworld"):
world_sources.append(WorldSource(file.name, is_zip=True))
__all__ = {"lookup_any_item_id_to_name",
"lookup_any_location_id_to_name",
"network_data_package",
"AutoWorldRegister"}
# import all submodules to trigger AutoWorldRegister
world_sources.sort()
for world_source in world_sources:
if world_source.is_zip:
importer = zipimport.zipimporter(os.path.join(folder, world_source.path))
importer.load_module(world_source.path.split(".", 1)[0])
else:
importlib.import_module(f".{world_source.path}", "worlds")
world_folders = []
for file in os.scandir(os.path.dirname(__file__)):
if file.is_dir():
world_folders.append(file.name)
world_folders.sort()
for world in world_folders:
if not world.startswith("_"): # prevent explicitly loading __pycache__ and allow _* names for non-world folders
importlib.import_module(f".{world}", "worlds")
from .AutoWorld import AutoWorldRegister
lookup_any_item_id_to_name = {}
lookup_any_location_id_to_name = {}
games = {}
from .AutoWorld import AutoWorldRegister
for world_name, world in AutoWorldRegister.world_types.items():
games[world_name] = {
"item_name_to_id": world.item_name_to_id,
"item_name_to_id" : world.item_name_to_id,
"location_name_to_id": world.location_name_to_id,
"version": world.data_version,
# seems clients don't actually want this. Keeping it here in case someone changes their mind.
@@ -68,6 +41,5 @@ network_data_package = {
if any(not world.data_version for world in AutoWorldRegister.world_types.values()):
network_data_package["version"] = 0
import logging
logging.warning(f"Datapackage is in custom mode. Custom Worlds: "
f"{[world for world in AutoWorldRegister.world_types.values() if not world.data_version]}")

View File

@@ -15,6 +15,7 @@ def create_dungeons(world, player):
dungeon_items, player)
for item in dungeon.all_items:
item.dungeon = dungeon
item.world = world
dungeon.boss = BossFactory(default_boss, player) if default_boss else None
for region in dungeon.regions:
world.get_region(region, player).dungeon = dungeon

View File

@@ -51,11 +51,6 @@ class ItemData(typing.NamedTuple):
flute_boy_credit: typing.Optional[str]
hint_text: typing.Optional[str]
def as_init_dict(self) -> typing.Dict[str, typing.Any]:
return {key: getattr(self, key) for key in
('classification', 'type', 'item_code', 'pedestal_hint', 'hint_text')}
# Format: Name: (Advancement, Type, ItemCode, Pedestal Hint Text, Pedestal Credit Text, Sick Kid Credit Text, Zora Credit Text, Witch Credit Text, Flute Boy Credit Text, Hint Text)
item_table = {'Bow': ItemData(IC.progression, None, 0x0B, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'the Bow'),
'Progressive Bow': ItemData(IC.progression, None, 0x64, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'),
@@ -223,7 +218,7 @@ item_table = {'Bow': ItemData(IC.progression, None, 0x0B, 'You have\nchosen the\
'Open Floodgate': ItemData(IC.progression, 'Event', None, None, None, None, None, None, None, None),
}
item_init_table = {name: data.as_init_dict() for name, data in item_table.items()}
as_dict_item_table = {name: data._asdict() for name, data in item_table.items()}
progression_mapping = {
"Golden Sword": ("Progressive Sword", 4),

View File

@@ -1,6 +1,5 @@
import typing
from BaseClasses import MultiWorld
from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink
@@ -28,35 +27,6 @@ class Goal(Choice):
option_hand_in = 2
class OpenPyramid(Choice):
"""Determines whether the hole at the top of pyramid is open.
Goal will open the pyramid if the goal requires you to kill Ganon, without needing to kill Agahnim 2.
Auto is the same as goal except if Ganon's dropdown is in another location, the hole will be closed."""
display_name = "Open Pyramid Hole"
option_closed = 0
option_open = 1
option_goal = 2
option_auto = 3
default = option_goal
alias_true = option_open
alias_false = option_closed
alias_yes = option_open
alias_no = option_closed
def to_bool(self, world: MultiWorld, player: int) -> bool:
if self.value == self.option_goal:
return world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'}
elif self.value == self.option_auto:
return world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'} \
and (world.shuffle[player] in {'vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed'} or not
world.shuffle_ganon)
elif self.value == self.option_open:
return True
else:
return False
class DungeonItem(Choice):
value: int
option_original_dungeon = 0
@@ -215,11 +185,9 @@ class Scams(Choice):
option_all = 3
alias_false = 0
@property
def gives_king_zora_hint(self):
return self.value in {0, 2}
@property
def gives_bottle_merchant_hint(self):
return self.value in {0, 1}
@@ -363,7 +331,6 @@ class AllowCollect(Toggle):
alttp_options: typing.Dict[str, type(Option)] = {
"crystals_needed_for_gt": CrystalsTower,
"crystals_needed_for_ganon": CrystalsGanon,
"open_pyramid": OpenPyramid,
"bigkey_shuffle": bigkey_shuffle,
"smallkey_shuffle": smallkey_shuffle,
"compass_shuffle": compass_shuffle,

View File

@@ -1247,7 +1247,7 @@ def patch_rom(world, rom, player, enemized):
rom.write_bytes(0x50563, [0x3F, 0x14]) # disable below ganon chest
rom.write_byte(0x50599, 0x00) # disable below ganon chest
rom.write_bytes(0xE9A5, [0x7E, 0x00, 0x24]) # disable below ganon chest
rom.write_byte(0x18008B, 0x01 if world.open_pyramid[player].to_bool(world, player) else 0x00) # pre-open Pyramid Hole
rom.write_byte(0x18008B, 0x01 if world.open_pyramid[player] else 0x00) # pre-open Pyramid Hole
rom.write_byte(0x18008C, 0x01 if world.crystals_needed_for_gt[
player] == 0 else 0x00) # GT pre-opened if crystal requirement is 0
rom.write_byte(0xF5D73, 0xF0) # bees are catchable
@@ -2091,9 +2091,7 @@ def write_string_to_rom(rom, target, string):
def write_strings(rom, world, player):
from . import ALTTPWorld
local_random = world.slot_seeds[player]
w: ALTTPWorld = world.worlds[player]
tt = TextTable()
tt.removeUnwantedText()
@@ -2422,8 +2420,7 @@ def write_strings(rom, world, player):
pedestal_text = 'Some Hot Air' if pedestalitem is None else hint_text(pedestalitem,
True) if pedestalitem.pedestal_hint_text is not None else 'Unknown Item'
tt['mastersword_pedestal_translated'] = pedestal_text
pedestal_credit_text = 'and the Hot Air' if pedestalitem is None else \
w.pedestal_credit_texts.get(pedestalitem.code, 'and the Unknown Item')
pedestal_credit_text = 'and the Hot Air' if pedestalitem is None else pedestalitem.pedestal_credit_text if pedestalitem.pedestal_credit_text is not None else 'and the Unknown Item'
etheritem = world.get_location('Ether Tablet', player).item
ether_text = 'Some Hot Air' if etheritem is None else hint_text(etheritem,
@@ -2451,24 +2448,20 @@ def write_strings(rom, world, player):
credits = Credits()
sickkiditem = world.get_location('Sick Kid', player).item
sickkiditem_text = local_random.choice(SickKid_texts) \
if sickkiditem is None or sickkiditem.code not in w.sickkid_credit_texts \
else w.sickkid_credit_texts[sickkiditem.code]
sickkiditem_text = local_random.choice(
SickKid_texts) if sickkiditem is None or sickkiditem.sickkid_credit_text is None else sickkiditem.sickkid_credit_text
zoraitem = world.get_location('King Zora', player).item
zoraitem_text = local_random.choice(Zora_texts) \
if zoraitem is None or zoraitem.code not in w.zora_credit_texts \
else w.zora_credit_texts[zoraitem.code]
zoraitem_text = local_random.choice(
Zora_texts) if zoraitem is None or zoraitem.zora_credit_text is None else zoraitem.zora_credit_text
magicshopitem = world.get_location('Potion Shop', player).item
magicshopitem_text = local_random.choice(MagicShop_texts) \
if magicshopitem is None or magicshopitem.code not in w.magicshop_credit_texts \
else w.magicshop_credit_texts[magicshopitem.code]
magicshopitem_text = local_random.choice(
MagicShop_texts) if magicshopitem is None or magicshopitem.magicshop_credit_text is None else magicshopitem.magicshop_credit_text
fluteboyitem = world.get_location('Flute Spot', player).item
fluteboyitem_text = local_random.choice(FluteBoy_texts) \
if fluteboyitem is None or fluteboyitem.code not in w.fluteboy_credit_texts \
else w.fluteboy_credit_texts[fluteboyitem.code]
fluteboyitem_text = local_random.choice(
FluteBoy_texts) if fluteboyitem is None or fluteboyitem.fluteboy_credit_text is None else fluteboyitem.fluteboy_credit_text
credits.update_credits_line('castle', 0, local_random.choice(KingsReturn_texts))
credits.update_credits_line('sanctuary', 0, local_random.choice(Sanctuary_texts))

View File

@@ -935,6 +935,7 @@ def set_trock_key_rules(world, player):
else:
# A key is required in the Big Key Chest to prevent a possible softlock. Place an extra key to ensure 100% locations still works
item = ItemFactory('Small Key (Turtle Rock)', player)
item.world = world
location = world.get_location('Turtle Rock - Big Key Chest', player)
location.place_locked_item(item)
location.event = True

View File

@@ -207,10 +207,10 @@ def ShopSlotFill(world):
shops_per_sphere.append(current_shops_slots)
candidates_per_sphere.append(current_candidates)
for location in sphere:
if isinstance(location, ALttPLocation) and location.shop_slot is not None:
if location.shop_slot is not None:
if not location.shop_slot_disabled:
current_shops_slots.append(location)
elif not location.locked and location.item.name not in blacklist_words:
elif not location.locked and not location.item.name in blacklist_words:
current_candidates.append(location)
if cumu_weights:
x = cumu_weights[-1]
@@ -335,6 +335,7 @@ def create_shops(world, player: int):
else:
loc.item = ItemFactory(GetBeemizerItem(world, player, 'Nothing'), player)
loc.shop_slot_disabled = True
loc.item.world = world
shop.region.locations.append(loc)
world.clear_location_cache()
@@ -459,11 +460,10 @@ def shuffle_shops(world, items, player: int):
f"Not all upgrades put into Player{player}' item pool. Putting remaining items in Capacity Upgrade shop instead.")
bombupgrades = sum(1 for item in new_items if 'Bomb Upgrade' in item)
arrowupgrades = sum(1 for item in new_items if 'Arrow Upgrade' in item)
slots = iter(range(2))
if bombupgrades:
capacityshop.add_inventory(next(slots), 'Bomb Upgrade (+5)', 100, bombupgrades)
capacityshop.add_inventory(1, 'Bomb Upgrade (+5)', 100, bombupgrades)
if arrowupgrades:
capacityshop.add_inventory(next(slots), 'Arrow Upgrade (+5)', 100, arrowupgrades)
capacityshop.add_inventory(1, 'Arrow Upgrade (+5)', 100, arrowupgrades)
else:
for item in new_items:
world.push_precollected(ItemFactory(item, player))

View File

@@ -6,33 +6,31 @@ from BaseClasses import Location, Item, ItemClassification
class ALttPLocation(Location):
game: str = "A Link to the Past"
crystal: bool
player_address: Optional[int]
_hint_text: Optional[str]
shop_slot: Optional[int] = None
"""If given as integer, shop_slot is the shop's inventory index."""
shop_slot_disabled: bool = False
def __init__(self, player: int, name: str, address: Optional[int] = None, crystal: bool = False,
hint_text: Optional[str] = None, parent=None, player_address: Optional[int] = None):
def __init__(self, player: int, name: str = '', address=None, crystal: bool = False,
hint_text: Optional[str] = None, parent=None,
player_address=None):
super(ALttPLocation, self).__init__(player, name, address, parent)
self.crystal = crystal
self.player_address = player_address
self._hint_text = hint_text
self._hint_text: str = hint_text
class ALttPItem(Item):
game: str = "A Link to the Past"
type: Optional[str]
_pedestal_hint_text: Optional[str]
_hint_text: Optional[str]
dungeon = None
def __init__(self, name, player, classification=ItemClassification.filler, type=None, item_code=None,
pedestal_hint=None, hint_text=None):
def __init__(self, name, player, classification=ItemClassification.filler, type=None, item_code=None, pedestal_hint=None,
pedestal_credit=None, sick_kid_credit=None, zora_credit=None, witch_credit=None,
flute_boy_credit=None, hint_text=None):
super(ALttPItem, self).__init__(name, classification, item_code, player)
self.type = type
self._pedestal_hint_text = pedestal_hint
self.pedestal_credit_text = pedestal_credit
self.sickkid_credit_text = sick_kid_credit
self.zora_credit_text = zora_credit
self.magicshop_credit_text = witch_credit
self.fluteboy_credit_text = flute_boy_credit
self._hint_text = hint_text
@property

View File

@@ -1,23 +1,26 @@
import random
import logging
import os
import random
import threading
import typing
from BaseClasses import Item, CollectionState, Tutorial
from .Dungeons import create_dungeons
from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect
from .InvertedRegions import create_inverted_regions, mark_dark_world_regions
from .ItemPool import generate_itempool, difficulties
from .Items import item_init_table, item_name_groups, item_table, GetBeemizerItem
from .Options import alttp_options, smallkey_shuffle
from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions
from .Rom import LocalRom, patch_rom, patch_race_rom, check_enemizer, patch_enemizer, apply_rom_settings, \
get_hash_string, get_base_rom_path, LttPDeltaPatch
from .Rules import set_rules
from .Shops import create_shops, ShopSlotFill
from .SubClasses import ALttPItem
from ..AutoWorld import World, WebWorld, LogicMixin
from .Options import alttp_options, smallkey_shuffle
from .Items import as_dict_item_table, item_name_groups, item_table, GetBeemizerItem
from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions
from .Rules import set_rules
from .ItemPool import generate_itempool, difficulties
from .Shops import create_shops, ShopSlotFill
from .Dungeons import create_dungeons
from .Rom import LocalRom, patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, get_hash_string, \
get_base_rom_path, LttPDeltaPatch
import Patch
from itertools import chain
from .InvertedRegions import create_inverted_regions, mark_dark_world_regions
from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect
lttp_logger = logging.getLogger("A Link to the Past")
@@ -107,7 +110,7 @@ class ALTTPWorld(World):
Ganon!
"""
game: str = "A Link to the Past"
option_definitions = alttp_options
options = alttp_options
topology_present = True
item_name_groups = item_name_groups
hint_blacklist = {"Triforce"}
@@ -121,17 +124,6 @@ class ALTTPWorld(World):
required_client_version = (0, 3, 2)
web = ALTTPWeb()
pedestal_credit_texts: typing.Dict[int, str] = \
{data.item_code: data.pedestal_credit for data in item_table.values() if data.pedestal_credit}
sickkid_credit_texts: typing.Dict[int, str] = \
{data.item_code: data.sick_kid_credit for data in item_table.values() if data.sick_kid_credit}
zora_credit_texts: typing.Dict[int, str] = \
{data.item_code: data.zora_credit for data in item_table.values() if data.zora_credit}
magicshop_credit_texts: typing.Dict[int, str] = \
{data.item_code: data.witch_credit for data in item_table.values() if data.witch_credit}
fluteboy_credit_texts: typing.Dict[int, str] = \
{data.item_code: data.flute_boy_credit for data in item_table.values() if data.flute_boy_credit}
set_rules = set_rules
create_items = generate_itempool
@@ -153,9 +145,6 @@ class ALTTPWorld(World):
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))
@@ -187,6 +176,17 @@ class ALTTPWorld(World):
def create_regions(self):
player = self.player
world = self.world
if world.open_pyramid[player] == 'goal':
world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt',
'localganontriforcehunt', 'ganonpedestal'}
elif world.open_pyramid[player] == 'auto':
world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt',
'localganontriforcehunt', 'ganonpedestal'} and \
(world.shuffle[player] in {'vanilla', 'dungeonssimple', 'dungeonsfull',
'dungeonscrossed'} or not world.shuffle_ganon)
else:
world.open_pyramid[player] = {'on': True, 'off': False, 'yes': True, 'no': False}.get(
world.open_pyramid[player], 'auto')
world.triforce_pieces_available[player] = max(world.triforce_pieces_available[player],
world.triforce_pieces_required[player])
@@ -341,19 +341,14 @@ class ALTTPWorld(World):
def stage_post_fill(cls, world):
ShopSlotFill(world)
def use_enemizer(self):
world = self.world
player = self.player
return (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player]
or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default'
or world.pot_shuffle[player] or world.bush_shuffle[player]
or world.killable_thieves[player])
def generate_output(self, output_directory: str):
world = self.world
player = self.player
try:
use_enemizer = self.use_enemizer()
use_enemizer = (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player]
or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default'
or world.pot_shuffle[player] or world.bush_shuffle[player]
or world.killable_thieves[player])
rom = LocalRom(get_base_rom_path())
@@ -416,7 +411,7 @@ class ALTTPWorld(World):
multidata["connect_names"][new_name] = multidata["connect_names"][self.world.player_name[self.player]]
def create_item(self, name: str) -> Item:
return ALttPItem(name, self.player, **item_init_table[name])
return ALttPItem(name, self.player, **as_dict_item_table[name])
@classmethod
def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool,

View File

@@ -144,8 +144,7 @@ Sólo hay que segiur estos pasos una vez.
2. Ve a Ajustes --> Interfaz de usario. Configura "Mostrar ajustes avanzados" en ON.
3. Ve a Ajustes --> Red. Configura "Comandos de red" en ON. (Se encuentra bajo Request Device 16.) Deja en 55355 (el
default) el Puerto de comandos de red.
![Captura de pantalla del ajuste Comandos de red](/static/generated/docs/A%20Link%20to%20the%20Past/retroarch-network-commands-en.png)
![Captura de pantalla del ajuste Comandos de red](/static/assets/tutorial/retroarch-network-commands-en.png)
4. Ve a Menú principal --> Actualizador en línea --> Descargador de núcleos. Desplázate y selecciona "Nintendo - SNES /
SFC (bsnes-mercury Performance)".

View File

@@ -299,5 +299,4 @@ item_table = (
'A Shrubbery',
'Roomba with a Knife',
'Wet Cat',
'The missing moderator, Frostwares',
)

View File

@@ -25,7 +25,7 @@ class ArchipIDLEWorld(World):
"""
game = "ArchipIDLE"
topology_present = False
data_version = 4
data_version = 3
hidden = (datetime.now().month != 4) # ArchipIDLE is only visible during April
web = ArchipIDLEWebWorld()
@@ -47,12 +47,13 @@ class ArchipIDLEWorld(World):
item_pool = []
for i in range(100):
item = ArchipIDLEItem(
item = Item(
item_table_copy[i],
ItemClassification.progression if i < 20 else ItemClassification.filler,
self.item_name_to_id[item_table_copy[i]],
self.player
)
item.game = 'ArchipIDLE'
item_pool.append(item)
self.world.itempool += item_pool
@@ -92,10 +93,6 @@ def create_region(world: MultiWorld, player: int, name: str, locations=None, exi
return region
class ArchipIDLEItem(Item):
game = "ArchipIDLE"
class ArchipIDLELocation(Location):
game: str = "ArchipIDLE"

View File

@@ -27,7 +27,7 @@ class ChecksFinderWorld(World):
with the mines! You win when you get all your items and beat the board!
"""
game: str = "ChecksFinder"
option_definitions = checksfinder_options
options = checksfinder_options
topology_present = True
web = ChecksFinderWeb()

View File

@@ -1,41 +0,0 @@
import typing
from Options import Toggle, Option
class AutoEquipOption(Toggle):
"""Automatically equips any received armor or left/right weapons."""
display_name = "Auto-equip"
class LockEquipOption(Toggle):
"""Lock the equipment slots so you cannot change your armor or your left/right weapons. Works great with the
Auto-equip option."""
display_name = "Lock Equipement Slots"
class NoWeaponRequirementsOption(Toggle):
"""Disable the weapon requirements by removing any movement or damage penalties.
Permitting you to use any weapon early"""
display_name = "No Weapon Requirements"
class RandomizeWeaponsLevelOption(Toggle):
"""Enable this option to upgrade 33% ( based on the probability chance ) of the pool of weapons to a random value
between +1 and +5/+10"""
display_name = "Randomize weapons level"
class LateBasinOfVowsOption(Toggle):
"""Force the Basin of Vows to be located as a reward of defeating Pontiff Sulyvahn. It permits to ease the
progression by preventing having to kill the Dancer of the Boreal Valley as the first boss"""
display_name = "Late Basin of Vows"
dark_souls_options: typing.Dict[str, type(Option)] = {
"auto_equip": AutoEquipOption,
"lock_equip": LockEquipOption,
"no_weapon_requirements": NoWeaponRequirementsOption,
"randomize_weapons_level": RandomizeWeaponsLevelOption,
"late_basin_of_vows": LateBasinOfVowsOption,
}

View File

@@ -1,299 +0,0 @@
# world/dark_souls_3/__init__.py
import json
import os
from .Options import dark_souls_options
from .data.items_data import weapons_upgrade_5_table, weapons_upgrade_10_table, item_dictionary_table, key_items_list
from .data.locations_data import location_dictionary_table, cemetery_of_ash_table, fire_link_shrine_table, \
high_wall_of_lothric, \
undead_settlement_table, road_of_sacrifice_table, consumed_king_garden_table, cathedral_of_the_deep_table, \
farron_keep_table, catacombs_of_carthus_table, smouldering_lake_table, irithyll_of_the_boreal_valley_table, \
irithyll_dungeon_table, profaned_capital_table, anor_londo_table, lothric_castle_table, grand_archives_table, \
untended_graves_table, archdragon_peak_table, firelink_shrine_bell_tower_table
from ..AutoWorld import World, WebWorld
from BaseClasses import MultiWorld, Location, Region, Item, RegionType, Entrance, Tutorial, ItemClassification
from ..generic.Rules import set_rule
class DarkSouls3Web(WebWorld):
bug_report_page = "https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/issues"
setup_en = Tutorial(
"Multiworld Setup Tutorial",
"A guide to setting up the Archipelago Dark Souls III randomizer on your computer.",
"English",
"setup_en.md",
"setup/en",
["Marech"]
)
setup_fr = Tutorial(
setup_en.tutorial_name,
setup_en.description,
"Français",
"setup_fr.md",
"setup/fr",
["Marech"]
)
tutorials = [setup_en, setup_fr]
class DarkSouls3World(World):
"""
Dark souls III is an Action role-playing game and is part of the Souls series developed by FromSoftware.
Played in a third-person perspective, players have access to various weapons, armour, magic, and consumables that
they can use to fight their enemies.
"""
game: str = "Dark Souls III"
option_definitions = dark_souls_options
topology_present: bool = True
remote_items: bool = False
remote_start_inventory: bool = False
web = DarkSouls3Web()
data_version = 2
base_id = 100000
item_name_to_id = {name: id for id, name in enumerate(item_dictionary_table, base_id)}
location_name_to_id = {name: id for id, name in enumerate(location_dictionary_table, base_id)}
def __init__(self, world: MultiWorld, player: int):
super().__init__(world, player)
self.locked_items = []
self.locked_locations = []
self.main_path_locations = []
def create_item(self, name: str) -> Item:
data = self.item_name_to_id[name]
if name in key_items_list:
item_classification = ItemClassification.progression
elif name in weapons_upgrade_5_table or name in weapons_upgrade_10_table:
item_classification = ItemClassification.useful
else:
item_classification = ItemClassification.filler
return DarkSouls3Item(name, item_classification, data, self.player)
def create_regions(self):
menu_region = Region("Menu", RegionType.Generic, "Menu", self.player)
self.world.regions.append(menu_region)
# Create all Vanilla regions of Dark Souls III
cemetery_of_ash_region = self.create_region("Cemetery Of Ash", cemetery_of_ash_table)
firelink_shrine_region = self.create_region("Firelink Shrine", fire_link_shrine_table)
firelink_shrine_bell_tower_region = self.create_region("Firelink Shrine Bell Tower",
firelink_shrine_bell_tower_table)
high_wall_of_lothric_region = self.create_region("High Wall of Lothric", high_wall_of_lothric)
undead_settlement_region = self.create_region("Undead Settlement", undead_settlement_table)
road_of_sacrifices_region = self.create_region("Road of Sacrifices", road_of_sacrifice_table)
consumed_king_garden_region = self.create_region("Consumed King's Garden", consumed_king_garden_table)
cathedral_of_the_deep_region = self.create_region("Cathedral of the Deep", cathedral_of_the_deep_table)
farron_keep_region = self.create_region("Farron Keep", farron_keep_table)
catacombs_of_carthus_region = self.create_region("Catacombs of Carthus", catacombs_of_carthus_table)
smouldering_lake_region = self.create_region("Smouldering Lake", smouldering_lake_table)
irithyll_of_the_boreal_valley_region = self.create_region("Irithyll of the Boreal Valley",
irithyll_of_the_boreal_valley_table)
irithyll_dungeon_region = self.create_region("Irithyll Dungeon", irithyll_dungeon_table)
profaned_capital_region = self.create_region("Profaned Capital", profaned_capital_table)
anor_londo_region = self.create_region("Anor Londo", anor_londo_table)
lothric_castle_region = self.create_region("Lothric Castle", lothric_castle_table)
grand_archives_region = self.create_region("Grand Archives", grand_archives_table)
untended_graves_region = self.create_region("Untended Graves", untended_graves_table)
archdragon_peak_region = self.create_region("Archdragon Peak", archdragon_peak_table)
kiln_of_the_first_flame_region = self.create_region("Kiln Of The First Flame", None)
# Create the entrance to connect those regions
menu_region.exits.append(Entrance(self.player, "New Game", menu_region))
self.world.get_entrance("New Game", self.player).connect(cemetery_of_ash_region)
cemetery_of_ash_region.exits.append(Entrance(self.player, "Goto Firelink Shrine", cemetery_of_ash_region))
self.world.get_entrance("Goto Firelink Shrine", self.player).connect(firelink_shrine_region)
firelink_shrine_region.exits.append(Entrance(self.player, "Goto High Wall of Lothric",
firelink_shrine_region))
firelink_shrine_region.exits.append(Entrance(self.player, "Goto Kiln Of The First Flame",
firelink_shrine_region))
firelink_shrine_region.exits.append(Entrance(self.player, "Goto Bell Tower",
firelink_shrine_region))
self.world.get_entrance("Goto High Wall of Lothric", self.player).connect(high_wall_of_lothric_region)
self.world.get_entrance("Goto Kiln Of The First Flame", self.player).connect(kiln_of_the_first_flame_region)
self.world.get_entrance("Goto Bell Tower", self.player).connect(firelink_shrine_bell_tower_region)
high_wall_of_lothric_region.exits.append(Entrance(self.player, "Goto Undead Settlement",
high_wall_of_lothric_region))
high_wall_of_lothric_region.exits.append(Entrance(self.player, "Goto Lothric Castle",
high_wall_of_lothric_region))
self.world.get_entrance("Goto Undead Settlement", self.player).connect(undead_settlement_region)
self.world.get_entrance("Goto Lothric Castle", self.player).connect(lothric_castle_region)
undead_settlement_region.exits.append(Entrance(self.player, "Goto Road Of Sacrifices",
undead_settlement_region))
self.world.get_entrance("Goto Road Of Sacrifices", self.player).connect(road_of_sacrifices_region)
road_of_sacrifices_region.exits.append(Entrance(self.player, "Goto Cathedral", road_of_sacrifices_region))
road_of_sacrifices_region.exits.append(Entrance(self.player, "Goto Farron keep", road_of_sacrifices_region))
self.world.get_entrance("Goto Cathedral", self.player).connect(cathedral_of_the_deep_region)
self.world.get_entrance("Goto Farron keep", self.player).connect(farron_keep_region)
farron_keep_region.exits.append(Entrance(self.player, "Goto Carthus catacombs", farron_keep_region))
self.world.get_entrance("Goto Carthus catacombs", self.player).connect(catacombs_of_carthus_region)
catacombs_of_carthus_region.exits.append(Entrance(self.player, "Goto Irithyll of the boreal",
catacombs_of_carthus_region))
catacombs_of_carthus_region.exits.append(Entrance(self.player, "Goto Smouldering Lake",
catacombs_of_carthus_region))
self.world.get_entrance("Goto Irithyll of the boreal", self.player).\
connect(irithyll_of_the_boreal_valley_region)
self.world.get_entrance("Goto Smouldering Lake", self.player).connect(smouldering_lake_region)
irithyll_of_the_boreal_valley_region.exits.append(Entrance(self.player, "Goto Irithyll dungeon",
irithyll_of_the_boreal_valley_region))
irithyll_of_the_boreal_valley_region.exits.append(Entrance(self.player, "Goto Anor Londo",
irithyll_of_the_boreal_valley_region))
self.world.get_entrance("Goto Irithyll dungeon", self.player).connect(irithyll_dungeon_region)
self.world.get_entrance("Goto Anor Londo", self.player).connect(anor_londo_region)
irithyll_dungeon_region.exits.append(Entrance(self.player, "Goto Archdragon peak", irithyll_dungeon_region))
irithyll_dungeon_region.exits.append(Entrance(self.player, "Goto Profaned capital", irithyll_dungeon_region))
self.world.get_entrance("Goto Archdragon peak", self.player).connect(archdragon_peak_region)
self.world.get_entrance("Goto Profaned capital", self.player).connect(profaned_capital_region)
lothric_castle_region.exits.append(Entrance(self.player, "Goto Consumed King Garden", lothric_castle_region))
lothric_castle_region.exits.append(Entrance(self.player, "Goto Grand Archives", lothric_castle_region))
self.world.get_entrance("Goto Consumed King Garden", self.player).connect(consumed_king_garden_region)
self.world.get_entrance("Goto Grand Archives", self.player).connect(grand_archives_region)
consumed_king_garden_region.exits.append(Entrance(self.player, "Goto Untended Graves",
consumed_king_garden_region))
self.world.get_entrance("Goto Untended Graves", self.player).connect(untended_graves_region)
# For each region, add the associated locations retrieved from the corresponding location_table
def create_region(self, region_name, location_table) -> Region:
new_region = Region(region_name, RegionType.Generic, region_name, self.player, self.world)
if location_table:
for name, address in location_table.items():
location = DarkSouls3Location(self.player, name, self.location_name_to_id[name], new_region)
new_region.locations.append(location)
self.world.regions.append(new_region)
return new_region
def create_items(self):
for name, address in self.item_name_to_id.items():
# Specific items will be included in the item pool under certain conditions. See generate_basic
if name != "Basin of Vows":
self.world.itempool += [self.create_item(name)]
def generate_early(self):
pass
def set_rules(self) -> None:
# Define the access rules to the entrances
set_rule(self.world.get_entrance("Goto Bell Tower", self.player),
lambda state: state.has("Tower Key", self.player))
set_rule(self.world.get_entrance("Goto Undead Settlement", self.player),
lambda state: state.has("Small Lothric Banner", self.player))
set_rule(self.world.get_entrance("Goto Lothric Castle", self.player),
lambda state: state.has("Basin of Vows", self.player))
set_rule(self.world.get_entrance("Goto Irithyll of the boreal", self.player),
lambda state: state.has("Small Doll", self.player))
set_rule(self.world.get_entrance("Goto Archdragon peak", self.player),
lambda state: state.can_reach("CKG: Soul of Consumed Oceiros", "Location", self.player))
set_rule(self.world.get_entrance("Goto Profaned capital", self.player),
lambda state: state.has("Storm Ruler", self.player))
set_rule(self.world.get_entrance("Goto Grand Archives", self.player),
lambda state: state.has("Grand Archives Key", self.player))
set_rule(self.world.get_entrance("Goto Kiln Of The First Flame", self.player),
lambda state: state.has("Cinders of a Lord - Abyss Watcher", self.player) and
state.has("Cinders of a Lord - Yhorm the Giant", self.player) and
state.has("Cinders of a Lord - Aldrich", self.player) and
state.has("Cinders of a Lord - Lothric Prince", self.player))
# Define the access rules to some specific locations
set_rule(self.world.get_location("HWL: Soul of the Dancer", self.player),
lambda state: state.has("Basin of Vows", self.player))
set_rule(self.world.get_location("HWL: Greirat's Ashes", self.player),
lambda state: state.has("Cell Key", self.player))
set_rule(self.world.get_location("ID: Bellowing Dragoncrest Ring", self.player),
lambda state: state.has("Jailbreaker's Key", self.player))
set_rule(self.world.get_location("ID: Prisoner Chief's Ashes", self.player),
lambda state: state.has("Jailer's Key Ring", self.player))
set_rule(self.world.get_location("ID: Covetous Gold Serpent Ring", self.player),
lambda state: state.has("Old Cell Key", self.player))
black_hand_gotthard_corpse_rule = lambda state: \
(state.can_reach("AL: Cinders of a Lord - Aldrich", "Location", self.player) and
state.can_reach("PC: Cinders of a Lord - Yhorm the Giant", "Location", self.player))
set_rule(self.world.get_location("LC: Grand Archives Key", self.player), black_hand_gotthard_corpse_rule)
set_rule(self.world.get_location("LC: Gotthard Twinswords", self.player), black_hand_gotthard_corpse_rule)
self.world.completion_condition[self.player] = lambda state: \
state.has("Cinders of a Lord - Abyss Watcher", self.player) and \
state.has("Cinders of a Lord - Yhorm the Giant", self.player) and \
state.has("Cinders of a Lord - Aldrich", self.player) and \
state.has("Cinders of a Lord - Lothric Prince", self.player)
def generate_basic(self):
# Depending on the specified option, add the Basin of Vows to a specific location or to the item pool
item = self.create_item("Basin of Vows")
if self.world.late_basin_of_vows[self.player]:
self.world.get_location("IBV: Soul of Pontiff Sulyvahn", self.player).place_locked_item(item)
else:
self.world.itempool += [item]
# Fill item pool with additional items
item_pool_len = self.item_name_to_id.__len__()
total_required_locations = self.location_name_to_id.__len__()
for i in range(item_pool_len, total_required_locations):
self.world.itempool += [self.create_item("Soul of an Intrepid Hero")]
def generate_output(self, output_directory: str):
# Depending on the specified option, modify items hexadecimal value to add an upgrade level
item_dictionary = item_dictionary_table.copy()
if self.world.randomize_weapons_level[self.player]:
# Randomize some weapons upgrades
for name in weapons_upgrade_5_table.keys():
if self.world.random.randint(0, 100) < 33:
value = self.world.random.randint(1, 5)
item_dictionary[name] += value
for name in weapons_upgrade_10_table.keys():
if self.world.random.randint(0, 100) < 33:
value = self.world.random.randint(1, 10)
item_dictionary[name] += value
# Create the mandatory lists to generate the player's output file
items_id = []
items_address = []
locations_id = []
locations_address = []
locations_target = []
for location in self.world.get_filled_locations():
if location.item.player == self.player:
items_id.append(location.item.code)
items_address.append(item_dictionary[location.item.name])
if location.player == self.player:
locations_address.append(location_dictionary_table[location.name])
locations_id.append(location.address)
if location.item.player == self.player:
locations_target.append(item_dictionary[location.item.name])
else:
locations_target.append(0)
data = {
"options": {
"auto_equip": self.world.auto_equip[self.player].value,
"lock_equip": self.world.lock_equip[self.player].value,
"no_weapon_requirements": self.world.no_weapon_requirements[self.player].value,
},
"seed": self.world.seed_name, # to verify the server's multiworld
"slot": self.world.player_name[self.player], # to connect to server
"base_id": self.base_id, # to merge location and items lists
"locationsId": locations_id,
"locationsAddress": locations_address,
"locationsTarget": locations_target,
"itemsId": items_id,
"itemsAddress": items_address
}
# generate the file
filename = f"AP-{self.world.seed_name}-P{self.player}-{self.world.player_name[self.player]}.json"
with open(os.path.join(output_directory, filename), 'w') as outfile:
json.dump(data, outfile)
class DarkSouls3Location(Location):
game: str = "Dark Souls III"
class DarkSouls3Item(Item):
game: str = "Dark Souls III"

View File

@@ -1,383 +0,0 @@
"""
Tools used to create this list :
List of all items https://docs.google.com/spreadsheets/d/1nK2g7g6XJ-qphFAk1tjP3jZtlXWDQY-ItKLa_sniawo/edit#gid=1551945791
Regular expression parser https://regex101.com/r/XdtiLR/2
List of locations https://darksouls3.wiki.fextralife.com/Locations
"""
weapons_upgrade_5_table = {
"Irithyll Straight Sword": 0x0020A760,
"Chaos Blade": 0x004C9960,
"Dragonrider Bow": 0x00D6B0F0,
"White Hair Talisman": 0x00CAF120,
"Izalith Staff": 0x00C96A80,
"Fume Ultra Greatsword": 0x0060E4B0,
"Black Knight Sword": 0x005F5E10,
"Yorshka's Spear": 0x008C3A70,
"Smough's Great Hammer": 0x007E30B0,
"Dragonslayer Greatbow": 0x00CF8500,
"Golden Ritual Spear": 0x00C83200,
"Eleonora": 0x006CCB90,
"Witch's Locks": 0x00B7B740,
"Crystal Chime": 0x00CA2DD0,
"Black Knight Glaive": 0x009AE070,
"Dragonslayer Spear": 0x008CAFA0,
"Caitha's Chime": 0x00CA06C0,
"Sunlight Straight Sword": 0x00203230,
"Firelink Greatsword": 0x0060BDA0,
"Hollowslayer Greatsword": 0x00604870,
"Arstor's Spear": 0x008BEC50,
"Vordt's Great Hammer": 0x007CD120,
"Crystal Sage's Rapier": 0x002E6300,
"Farron Greatsword": 0x005E9AC0,
"Wolf Knight's Greatsword": 0x00602160,
"Dancer's Enchanted Swords": 0x00F4C040,
"Wolnir's Holy Sword": 0x005FFA50,
"Demon's Greataxe": 0x006CA480,
"Demon's Fist": 0x00A84DF0,
"Old King's Great Hammer": 0x007CF830,
"Greatsword of Judgment": 0x005E2590,
"Profaned Greatsword": 0x005E4CA0,
"Yhorm's Great Machete": 0x005F0FF0,
"Cleric's Candlestick": 0x0020F580,
"Dragonslayer Greataxe": 0x006C7D70,
"Moonlight Greatsword": 0x00606F80,
"Gundyr's Halberd": 0x009A1D20,
"Lothric's Holy Sword": 0x005FD340,
"Lorian's Greatsword": 0x005F8520,
"Twin Princes' Greatsword": 0x005FAC30,
"Storm Curved Sword": 0x003E4180,
"Dragonslayer Swordspear": 0x008BC540,
"Sage's Crystal Staff": 0x00C8CE40,
}
weapons_upgrade_10_table = {
"Broken Straight Sword": 0x001EF9B0,
"Deep Battle Axe": 0x0006AFA54,
"Club": 0x007A1200,
"Claymore": 0x005BDBA0,
"Longbow": 0x00D689E0,
"Mail Breaker": 0x002DEDD0,
"Broadsword": 0x001ED2A0,
"Astora's Straight Sword": 0x002191C0,
"Rapier": 0x002E14E0,
"Lucerne": 0x0098BD90,
"Whip": 0x00B71B00,
"Reinforced Club": 0x007A8730,
"Caestus": 0x00A7FFD0,
"Partizan": 0x0089C970,
"Red Hilted Halberd": 0x009AB960,
"Saint's Talisman": 0x00CACA10,
"Large Club": 0x007AFC60,
"Brigand Twindaggers": 0x00F50E60,
"Butcher Knife": 0x006BE130,
"Brigand Axe": 0x006B1DE0,
"Heretic's Staff": 0x00C8F550,
"Great Club": 0x007B4A80,
"Exile Greatsword": 0x005DD770,
"Sellsword Twinblades": 0x00F42400,
"Notched Whip": 0x00B7DE50,
"Astora Greatsword": 0x005C9EF0,
"Executioner's Greatsword": 0x0021DFE0,
"Saint-tree Bellvine": 0x00C9DFB0,
"Saint Bident": 0x008C1360,
"Drang Hammers": 0x00F61FD0,
"Arbalest": 0x00D662D0,
"Sunlight Talisman": 0x00CA54E0,
"Greatsword": 0x005C50D0,
"Black Bow of Pharis": 0x00D7E970,
"Great Axe": 0x006B9310,
"Black Blade": 0x004CC070,
"Blacksmith Hammer": 0x007E57C0,
"Witchtree Branch": 0x00C94370,
"Painting Guardian's Curved Sword": 0x003E6890,
"Pickaxe": 0x007DE290,
"Court Sorcerer's Staff": 0x00C91C60,
"Avelyn": 0x00D6FF10,
"Onikiri and Ubadachi": 0x00F58390,
"Ricard's Rapier": 0x002E3BF0,
"Drakeblood Greatsword": 0x00609690,
"Greatlance": 0x008A8CC0,
"Sniper Crossbow": 0x00D83790,
"Claw": 0x00A7D8C0,
"Drang Twinspears": 0x00F5AAA0,
}
shields_table = {
"East-West Shield": 0x0142B930,
"Silver Eagle Kite Shield": 0x014418C0,
"Small Leather Shield": 0x01315410,
"Blue Wooden Shield": 0x0143F1B0,
"Plank Shield": 0x01346150,
"Caduceus Round Shield": 0x01341330,
"Wargod Wooden Shield": 0x0144DC10,
"Grass Crest Shield": 0x01437C80,
"Golden Falcon Shield": 0x01354BB0,
"Twin Dragon Greatshield": 0x01513820,
"Spider Shield": 0x01435570,
"Crest Shield": 0x01430750,
"Curse Ward Greatshield": 0x01518640,
"Stone Parma": 0x01443FD0,
"Dragon Crest Shield": 0x01432E60,
"Shield of Want": 0x0144B500,
"Black Iron Greatshield": 0x0150EA00,
"Greatshield of Glory": 0x01515F30,
"Sacred Bloom Shield": 0x013572C0,
"Golden Wing Crest Shield": 0x0143CAA0,
"Ancient Dragon Greatshield": 0x013599D0,
"Spirit Tree Crest Shield": 0x014466E0,
}
goods_table = {
"Soul of an Intrepid Hero": 0x4000019D,
"Soul of the Nameless King": 0x400002D2,
"Soul of Champion Gundyr": 0x400002C8,
"Soul of the Twin Princes": 0x400002DB,
"Soul of Consumed Oceiros": 0x400002CE,
"Soul of Aldrich": 0x400002D5,
"Soul of Yhorm the Giant": 0x400002DC,
"Soul of Pontiff Sulyvahn": 0x400002D4,
"Soul of the Old Demon King": 0x400002D0,
"Soul of High Lord Wolnir": 0x400002D6,
"Soul of the Blood of the Wolf": 0x400002CD,
"Soul of the Deacons of the Deep": 0x400002D9,
"Soul of a Crystal Sage": 0x400002CB,
"Soul of Boreal Valley Vordt": 0x400002CF,
"Soul of a Stray Demon": 0x400002E7,
"Soul of a Demon": 0x400002E3,
}
armor_table = {
"Fire Keeper Robe": 0x140D9CE8,
"Fire Keeper Gloves": 0x140DA0D0,
"Fire Keeper Skirt": 0x140DA4B8,
"Deserter Trousers": 0x126265B8,
"Cleric Hat": 0x11D905C0,
"Cleric Blue Robe": 0x11D909A8,
"Cleric Gloves": 0x11D90D90,
"Cleric Trousers": 0x11D91178,
"Northern Helm": 0x116E3600,
"Northern Armor": 0x116E39E8,
"Northern Gloves": 0x116E3DD0,
"Northern Trousers": 0x116E41B8,
"Loincloth": 0x148F57D8,
"Brigand Hood": 0x148009E0,
"Brigand Armor": 0x14800DC8,
"Brigand Gauntlets": 0x148011B0,
"Brigand Trousers": 0x14801598,
"Sorcerer Hood": 0x11C9C380,
"Sorcerer Robe": 0x11C9C768,
"Sorcerer Gloves": 0x11C9CB50,
"Sorcerer Trousers": 0x11C9CF38,
"Fallen Knight Helm": 0x1121EAC0,
"Fallen Knight Armor": 0x1121EEA8,
"Fallen Knight Gauntlets": 0x1121F290,
"Fallen Knight Trousers": 0x1121F678,
"Conjurator Hood": 0x149E8E60,
"Conjurator Robe": 0x149E9248,
"Conjurator Manchettes": 0x149E9630,
"Conjurator Boots": 0x149E9A18,
"Sellsword Helm": 0x11481060,
"Sellsword Armor": 0x11481448,
"Sellsword Gauntlet": 0x11481830,
"Sellsword Trousers": 0x11481C18,
"Herald Helm": 0x114FB180,
"Herald Armor": 0x114FB568,
"Herald Gloves": 0x114FB950,
"Herald Trousers": 0x114FBD38,
"Maiden Hood": 0x14BD12E0,
"Maiden Robe": 0x14BD16C8,
"Maiden Gloves": 0x14BD1AB0,
"Maiden Skirt": 0x14BD1E98,
"Drang Armor": 0x154E0C28,
"Drang Gauntlets": 0x154E1010,
"Drang Shoes": 0x154E13F8,
"Archdeacon White Crown": 0x13EF1480,
"Archdeacon Holy Garb": 0x13EF1868,
"Archdeacon Skirt": 0x13EF2038,
"Antiquated Dress": 0x15D76068,
"Antiquated Gloves": 0x15D76450,
"Antiquated Skirt": 0x15D76838,
"Ragged Mask": 0x148F4C20,
"Crown of Dusk": 0x15D75C80,
"Pharis's Hat": 0x1487AB00,
"Old Sage's Blindfold": 0x11945BA0,
"Painting Guardian Hood": 0x156C8CC0,
"Painting Guardian Gown": 0x156C90A8,
"Painting Guardian Gloves": 0x156C9490,
"Painting Guardian Waistcloth": 0x156C9878,
"Brass Helm": 0x1501BD00,
"Brass Armor": 0x1501C0E8,
"Brass Gauntlets": 0x1501C4D0,
"Brass Leggings": 0x1501C8B8,
"Old Sorcerer Hat": 0x1496ED40,
"Old Sorcerer Coat": 0x1496F128,
"Old Sorcerer Gauntlets": 0x1496F510,
"Old Sorcerer Boots": 0x1496F8F8,
"Court Sorcerer Hood": 0x11BA8140,
"Court Sorcerer Robe": 0x11BA8528,
"Court Sorcerer Gloves": 0x11BA8910,
"Court Sorcerer Trousers": 0x11BA8CF8,
"Dragonslayer Helm": 0x158B1140,
"Dragonslayer Armor": 0x158B1528,
"Dragonslayer Gauntlets": 0x158B1910,
"Dragonslayer Leggings": 0x158B1CF8,
"Hood of Prayer": 0x13AA6A60,
"Robe of Prayer": 0x13AA6E48,
"Skirt of Prayer": 0x13AA7618,
"Winged Knight Helm": 0x12EBAE40,
"Winged Knight Armor": 0x12EBB228,
"Winged Knight Gauntlets": 0x12EBB610,
"Winged Knight Leggings": 0x12EBB9F8,
"Shadow Mask": 0x14D3F640,
"Shadow Garb": 0x14D3FA28,
"Shadow Gauntlets": 0x14D3FE10,
"Shadow Leggings": 0x14D401F8,
}
rings_table = {
"Estus Ring": 0x200050DC,
"Covetous Silver Serpent Ring": 0x20004FB0,
"Fire Clutch Ring": 0x2000501E,
"Flame Stoneplate Ring": 0x20004E52,
"Flynn's Ring": 0x2000503C,
"Chloranthy Ring": 0x20004E2A,
"Morne's Ring": 0x20004F1A,
"Sage Ring": 0x20004F38,
"Aldrich's Sapphire": 0x20005096,
"Lloyd's Sword Ring": 0x200050B4,
"Poisonbite Ring": 0x20004E8E,
"Deep Ring": 0x20004F60,
"Lingering Dragoncrest Ring": 0x20004F2E,
"Carthus Milkring": 0x20004FE2,
"Witch's Ring": 0x20004F11,
"Carthus Bloodring": 0x200050FA,
"Speckled Stoneplate Ring": 0x20004E7A,
"Magic Clutch Ring": 0x2000500A,
"Ring of the Sun's First Born": 0x20004F1B,
"Pontiff's Right Eye": 0x2000510E, "Leo Ring": 0x20004EE8,
"Dark Stoneplate Ring": 0x20004E70,
"Reversal Ring": 0x20005104,
"Ring of Favor": 0x20004E3E,
"Bellowing Dragoncrest Ring": 0x20004F07,
"Covetous Gold Serpent Ring": 0x20004FA6,
"Dusk Crown Ring": 0x20004F4C,
"Dark Clutch Ring": 0x20005028,
"Cursebite Ring": 0x20004E98,
"Sun Princess Ring": 0x20004FBA,
"Aldrich's Ruby": 0x2000508C,
"Scholar Ring": 0x20004EB6,
"Fleshbite Ring": 0x20004EA2,
"Hunter's Ring": 0x20004FF6,
"Ashen Estus Ring": 0x200050E6,
"Hornet Ring": 0x20004F9C,
"Lightning Clutch Ring": 0x20005014,
"Ring of Steel Protection": 0x20004E48,
"Calamity Ring": 0x20005078,
"Thunder Stoneplate Ring": 0x20004E5C,
"Knight's Ring": 0x20004FEC,
"Red Tearstone Ring": 0x20004ECA,
"Dragonscale Ring": 0x2000515E,
"Knight Slayer's Ring": 0x20005000,
}
spells_table = {
"Seek Guidance": 0x40360420,
"Lightning Spear": 0x40362B30,
"Atonement": 0x4039ADA0,
"Great Magic Weapon": 0x40140118,
"Iron Flesh": 0x40251430,
"Lightning Stake": 0x40389C30,
"Toxic Mist": 0x4024F108,
"Sacred Flame": 0x40284880,
"Dorhys' Gnawing": 0x40363EB8,
"Great Heal": 0x40356FB0,
"Lightning Blade": 0x4036C770,
"Profaned Flame": 0x402575D8,
"Wrath of the Gods": 0x4035E0F8,
"Power Within": 0x40253B40,
"Soul Stream": 0x4018B820,
"Divine Pillars of Light": 0x4038C340,
"Great Magic Barrier": 0x40365628,
"Great Magic Shield": 0x40144F38,
}
misc_items_table = {
"Tower Key": 0x400007DF,
"Grave Key": 0x400007D9,
"Cell Key": 0x400007DA,
"Small Lothric Banner": 0x40000836,
"Mortician's Ashes": 0x4000083B,
"Braille Divine Tome of Carim": 0x40000847, # Shop
"Great Swamp Pyromancy Tome": 0x4000084F, # Shop
"Farron Coal ": 0x40000837, # Shop
"Paladin's Ashes": 0x4000083D, #Shop
"Deep Braille Divine Tome": 0x40000860, # Shop
"Small Doll": 0x400007D5,
"Golden Scroll": 0x4000085C,
"Sage's Coal": 0x40000838, # Shop #Unique
"Sage's Scroll": 0x40000854,
"Dreamchaser's Ashes": 0x4000083C, # Shop #Unique
"Cinders of a Lord - Abyss Watcher": 0x4000084B,
"Cinders of a Lord - Yhorm the Giant": 0x4000084D,
"Cinders of a Lord - Aldrich": 0x4000084C,
"Grand Archives Key": 0x400007DE,
"Basin of Vows": 0x40000845,
"Cinders of a Lord - Lothric Prince": 0x4000084E,
"Carthus Pyromancy Tome": 0x40000850,
"Grave Warden's Ashes": 0x4000083E,
"Grave Warden Pyromancy Tome": 0x40000853,
"Quelana Pyromancy Tome": 0x40000852,
"Izalith Pyromancy Tome": 0x40000851,
"Greirat's Ashes": 0x4000083F,
"Excrement-covered Ashes": 0x40000862,
"Easterner's Ashes": 0x40000868,
"Prisoner Chief's Ashes": 0x40000863,
"Jailbreaker's Key": 0x400007D7,
"Dragon Torso Stone": 0x4000017A,
"Profaned Coal": 0x4000083A,
"Xanthous Ashes": 0x40000864,
"Old Cell Key": 0x400007DC,
"Jailer's Key Ring": 0x400007D8,
"Logan's Scroll": 0x40000855,
"Storm Ruler": 0x006132D0,
"Giant's Coal": 0x40000839,
"Coiled Sword Fragment": 0x4000015F,
"Dragon Chaser's Ashes": 0x40000867,
"Twinkling Dragon Torso Stone": 0x40000184,
"Braille Divine Tome of Lothric": 0x40000848,
}
key_items_list = {
"Small Lothric Banner",
"Basin of Vows",
"Small Doll",
"Storm Ruler",
"Grand Archives Key",
"Cinders of a Lord - Abyss Watcher",
"Cinders of a Lord - Yhorm the Giant",
"Cinders of a Lord - Aldrich",
"Cinders of a Lord - Lothric Prince",
"Mortician's Ashes",
"Cell Key",
"Tower Key",
"Jailbreaker's Key",
"Prisoner Chief's Ashes",
"Old Cell Key",
"Jailer's Key Ring",
}
item_dictionary_table = {**weapons_upgrade_5_table, **weapons_upgrade_10_table, **shields_table, **armor_table, **rings_table, **spells_table, **misc_items_table, **goods_table}

View File

@@ -1,434 +0,0 @@
"""
Tools used to create this list :
List of all items https://docs.google.com/spreadsheets/d/1nK2g7g6XJ-qphFAk1tjP3jZtlXWDQY-ItKLa_sniawo/edit#gid=1551945791
Regular expression parser https://regex101.com/r/XdtiLR/2
List of locations https://darksouls3.wiki.fextralife.com/Locations
"""
cemetery_of_ash_table = {
}
fire_link_shrine_table = {
# "FS: Coiled Sword": 0x40000859, You can still light the Firelink Shrine fire whether you have it or not, useless
"FS: Broken Straight Sword": 0x001EF9B0,
"FS: East-West Shield": 0x0142B930,
"FS: Uchigatana": 0x004C4B40,
"FS: Master's Attire": 0x148F5008,
"FS: Master's Gloves": 0x148F53F0,
}
firelink_shrine_bell_tower_table = {
"FSBT: Covetous Silver Serpent Ring": 0x20004FB0,
"FSBT: Fire Keeper Robe": 0x140D9CE8,
"FSBT: Fire Keeper Gloves": 0x140DA0D0,
"FSBT: Fire Keeper Skirt": 0x140DA4B8,
"FSBT: Estus Ring": 0x200050DC,
"FSBT: Fire Keeper Soul": 0x40000186
}
high_wall_of_lothric = {
"HWL: Deep Battle Axe": 0x0006AFA54,
"HWL: Club": 0x007A1200,
"HWL: Claymore": 0x005BDBA0,
"HWL: Binoculars": 0x40000173,
"HWL: Longbow": 0x00D689E0,
"HWL: Mail Breaker": 0x002DEDD0,
"HWL: Broadsword": 0x001ED2A0,
"HWL: Silver Eagle Kite Shield": 0x014418C0,
"HWL: Astora's Straight Sword": 0x002191C0,
"HWL: Cell Key": 0x400007DA,
"HWL: Rapier": 0x002E14E0,
"HWL: Lucerne": 0x0098BD90,
"HWL: Small Lothric Banner": 0x40000836,
"HWL: Basin of Vows": 0x40000845,
"HWL: Soul of Boreal Valley Vordt": 0x400002CF,
"HWL: Soul of the Dancer": 0x400002CA,
"HWL: Way of Blue Covenant": 0x2000274C,
"HWL: Greirat's Ashes": 0x4000083F,
}
undead_settlement_table = {
"US: Small Leather Shield": 0x01315410,
"US: Whip": 0x00B71B00,
"US: Reinforced Club": 0x007A8730,
"US: Blue Wooden Shield": 0x0143F1B0,
"US: Cleric Hat": 0x11D905C0,
"US: Cleric Blue Robe": 0x11D909A8,
"US: Cleric Gloves": 0x11D90D90,
"US: Cleric Trousers": 0x11D91178,
"US: Mortician's Ashes": 0x4000083B,
"US: Caestus": 0x00A7FFD0,
"US: Plank Shield": 0x01346150,
"US: Flame Stoneplate Ring": 0x20004E52,
"US: Caduceus Round Shield": 0x01341330,
"US: Fire Clutch Ring": 0x2000501E,
"US: Partizan": 0x0089C970,
"US: Bloodbite Ring": 0x20004E84,
"US: Red Hilted Halberd": 0x009AB960,
"US: Saint's Talisman": 0x00CACA10,
"US: Irithyll Straight Sword": 0x0020A760,
"US: Large Club": 0x007AFC60,
"US: Northern Helm": 0x116E3600,
"US: Northern Armor": 0x116E39E8,
"US: Northern Gloves": 0x116E3DD0,
"US: Northern Trousers": 0x116E41B8,
"US: Flynn's Ring": 0x2000503C,
"US: Mirrah Vest": 0x15204568,
"US: Mirrah Gloves": 0x15204950,
"US: Mirrah Trousers": 0x15204D38,
"US: Chloranthy Ring": 0x20004E2A,
"US: Loincloth": 0x148F57D8,
"US: Wargod Wooden Shield": 0x0144DC10,
"US: Loretta's Bone": 0x40000846,
"US: Hand Axe": 0x006ACFC0,
"US: Great Scythe": 0x00989680,
"US: Soul of the Rotted Greatwood": 0x400002D7,
"US: Hawk Ring": 0x20004F92,
"US: Warrior of Sunlight Covenant": 0x20002738,
}
road_of_sacrifice_table = {
"RS: Brigand Twindaggers": 0x00F50E60,
"RS: Brigand Hood": 0x148009E0,
"RS: Brigand Armor": 0x14800DC8,
"RS: Brigand Gauntlets": 0x148011B0,
"RS: Brigand Trousers": 0x14801598,
"RS: Butcher Knife": 0x006BE130,
"RS: Brigand Axe": 0x006B1DE0,
"RS: Braille Divine Tome of Carim": 0x40000847,
"RS: Morne's Ring": 0x20004F1A,
"RS: Twin Dragon Greatshield": 0x01513820,
"RS: Heretic's Staff": 0x00C8F550,
"RS: Sorcerer Hood": 0x11C9C380,
"RS: Sorcerer Robe": 0x11C9C768,
"RS: Sorcerer Gloves": 0x11C9CB50,
"RS: Sorcerer Trousers": 0x11C9CF38,
"RS: Sage Ring": 0x20004F38,
"RS: Fallen Knight Helm": 0x1121EAC0,
"RS: Fallen Knight Armor": 0x1121EEA8,
"RS: Fallen Knight Gauntlets": 0x1121F290,
"RS: Fallen Knight Trousers": 0x1121F678,
"RS: Conjurator Hood": 0x149E8E60,
"RS: Conjurator Robe": 0x149E9248,
"RS: Conjurator Manchettes": 0x149E9630,
"RS: Conjurator Boots": 0x149E9A18,
"RS: Great Swamp Pyromancy Tome": 0x4000084F,
"RS: Great Club": 0x007B4A80,
"RS: Exile Greatsword": 0x005DD770,
"RS: Farron Coal ": 0x40000837,
"RS: Sellsword Twinblades": 0x00F42400,
"RS: Sellsword Helm": 0x11481060,
"RS: Sellsword Armor": 0x11481448,
"RS: Sellsword Gauntlet": 0x11481830,
"RS: Sellsword Trousers": 0x11481C18,
"RS: Golden Falcon Shield": 0x01354BB0,
"RS: Herald Helm": 0x114FB180,
"RS: Herald Armor": 0x114FB568,
"RS: Herald Gloves": 0x114FB950,
"RS: Herald Trousers": 0x114FBD38,
"RS: Grass Crest Shield": 0x01437C80,
"RS: Soul of a Crystal Sage": 0x400002CB,
"RS: Great Swamp Ring": 0x20004F10,
}
cathedral_of_the_deep_table = {
"CD: Paladin's Ashes": 0x4000083D,
"CD: Spider Shield": 0x01435570,
"CD: Crest Shield": 0x01430750,
"CD: Notched Whip": 0x00B7DE50,
"CD: Astora Greatsword": 0x005C9EF0,
"CD: Executioner's Greatsword": 0x0021DFE0,
"CD: Curse Ward Greatshield": 0x01518640,
"CD: Saint-tree Bellvine": 0x00C9DFB0,
"CD: Poisonbite Ring": 0x20004E8E,
"CD: Lloyd's Sword Ring": 0x200050B4,
"CD: Seek Guidance": 0x40360420,
"CD: Aldrich's Sapphire": 0x20005096,
"CD: Deep Braille Divine Tome": 0x40000860,
"CD: Saint Bident": 0x008C1360,
"CD: Maiden Hood": 0x14BD12E0,
"CD: Maiden Robe": 0x14BD16C8,
"CD: Maiden Gloves": 0x14BD1AB0,
"CD: Maiden Skirt": 0x14BD1E98,
"CD: Drang Armor": 0x154E0C28,
"CD: Drang Gauntlets": 0x154E1010,
"CD: Drang Shoes": 0x154E13F8,
"CD: Drang Hammers": 0x00F61FD0,
"CD: Deep Ring": 0x20004F60,
"CD: Archdeacon White Crown": 0x13EF1480,
"CD: Archdeacon Holy Garb": 0x13EF1868,
"CD: Archdeacon Skirt": 0x13EF2038,
"CD: Arbalest": 0x00D662D0,
"CD: Small Doll": 0x400007D5,
"CD: Soul of the Deacons of the Deep": 0x400002D9,
"CD: Rosaria's Fingers Covenant": 0x20002760,
}
farron_keep_table = {
"FK: Ragged Mask": 0x148F4C20,
"FK: Iron Flesh": 0x40251430,
"FK: Golden Scroll": 0x4000085C,
"FK: Antiquated Dress": 0x15D76068,
"FK: Antiquated Gloves": 0x15D76450,
"FK: Antiquated Skirt": 0x15D76838,
"FK: Nameless Knight Helm": 0x143B5FC0,
"FK: Nameless Knight Armor": 0x143B63A8,
"FK: Nameless Knight Gauntlets": 0x143B6790,
"FK: Nameless Knight Leggings": 0x143B6B78,
"FK: Sunlight Talisman": 0x00CA54E0,
"FK: Wolf's Blood Swordgrass": 0x4000016E,
"FK: Greatsword": 0x005C50D0,
"FK: Sage's Coal": 0x40000838,
"FK: Stone Parma": 0x01443FD0,
"FK: Sage's Scroll": 0x40000854,
"FK: Crown of Dusk": 0x15D75C80,
"FK: Lingering Dragoncrest Ring": 0x20004F2E,
"FK: Pharis's Hat": 0x1487AB00,
"FK: Black Bow of Pharis": 0x00D7E970,
"FK: Dreamchaser's Ashes": 0x4000083C,
"FK: Great Axe": 0x006B9310,
"FK: Dragon Crest Shield": 0x01432E60,
"FK: Lightning Spear": 0x40362B30,
"FK: Atonement": 0x4039ADA0,
"FK: Great Magic Weapon": 0x40140118,
"FK: Cinders of a Lord - Abyss Watcher": 0x4000084B,
"FK: Soul of the Blood of the Wolf": 0x400002CD,
"FK: Soul of a Stray Demon": 0x400002E7,
"FK: Watchdogs of Farron Covenant": 0x20002724,
}
catacombs_of_carthus_table = {
"CC: Carthus Pyromancy Tome": 0x40000850,
"CC: Carthus Milkring": 0x20004FE2,
"CC: Grave Warden's Ashes": 0x4000083E,
"CC: Carthus Bloodring": 0x200050FA,
"CC: Grave Warden Pyromancy Tome": 0x40000853,
"CC: Old Sage's Blindfold": 0x11945BA0,
"CC: Witch's Ring": 0x20004F11,
"CC: Black Blade": 0x004CC070,
"CC: Soul of High Lord Wolnir": 0x400002D6,
"CC: Soul of a Demon": 0x400002E3,
}
smouldering_lake_table = {
"SL: Shield of Want": 0x0144B500,
"SL: Speckled Stoneplate Ring": 0x20004E7A,
"SL: Dragonrider Bow": 0x00D6B0F0,
"SL: Lightning Stake": 0x40389C30,
"SL: Izalith Pyromancy Tome": 0x40000851,
"SL: Black Knight Sword": 0x005F5E10,
"SL: Quelana Pyromancy Tome": 0x40000852,
"SL: Toxic Mist": 0x4024F108,
"SL: White Hair Talisman": 0x00CAF120,
"SL: Izalith Staff": 0x00C96A80,
"SL: Sacred Flame": 0x40284880,
"SL: Fume Ultra Greatsword": 0x0060E4B0,
"SL: Black Iron Greatshield": 0x0150EA00,
"SL: Soul of the Old Demon King": 0x400002D0,
"SL: Knight Slayer's Ring": 0x20005000,
}
irithyll_of_the_boreal_valley_table = {
"IBV: Dorhys' Gnawing": 0x40363EB8,
"IBV: Witchtree Branch": 0x00C94370,
"IBV: Magic Clutch Ring": 0x2000500A,
"IBV: Ring of the Sun's First Born": 0x20004F1B,
"IBV: Roster of Knights": 0x4000006C,
"IBV: Pontiff's Right Eye": 0x2000510E,
"IBV: Yorshka's Spear": 0x008C3A70,
"IBV: Great Heal": 0x40356FB0,
"IBV: Smough's Great Hammer": 0x007E30B0,
"IBV: Leo Ring": 0x20004EE8,
"IBV: Excrement-covered Ashes": 0x40000862,
"IBV: Dark Stoneplate Ring": 0x20004E70,
"IBV: Easterner's Ashes": 0x40000868,
"IBV: Painting Guardian's Curved Sword": 0x003E6890,
"IBV: Painting Guardian Hood": 0x156C8CC0,
"IBV: Painting Guardian Gown": 0x156C90A8,
"IBV: Painting Guardian Gloves": 0x156C9490,
"IBV: Painting Guardian Waistcloth": 0x156C9878,
"IBV: Dragonslayer Greatbow": 0x00CF8500,
"IBV: Reversal Ring": 0x20005104,
"IBV: Brass Helm": 0x1501BD00,
"IBV: Brass Armor": 0x1501C0E8,
"IBV: Brass Gauntlets": 0x1501C4D0,
"IBV: Brass Leggings": 0x1501C8B8,
"IBV: Ring of Favor": 0x20004E3E,
"IBV: Golden Ritual Spear": 0x00C83200,
"IBV: Soul of Pontiff Sulyvahn": 0x400002D4,
"IBV: Aldrich Faithful Covenant": 0x2000272E,
"IBV: Drang Twinspears": 0x00F5AAA0,
}
irithyll_dungeon_table = {
"ID: Bellowing Dragoncrest Ring": 0x20004F07,
"ID: Jailbreaker's Key": 0x400007D7,
"ID: Prisoner Chief's Ashes": 0x40000863,
"ID: Old Sorcerer Hat": 0x1496ED40,
"ID: Old Sorcerer Coat": 0x1496F128,
"ID: Old Sorcerer Gauntlets": 0x1496F510,
"ID: Old Sorcerer Boots": 0x1496F8F8,
"ID: Great Magic Shield": 0x40144F38,
"ID: Dragon Torso Stone": 0x4000017A,
"ID: Lightning Blade": 0x4036C770,
"ID: Profaned Coal": 0x4000083A,
"ID: Xanthous Ashes": 0x40000864,
"ID: Old Cell Key": 0x400007DC,
"ID: Pickaxe": 0x007DE290,
"ID: Profaned Flame": 0x402575D8,
"ID: Covetous Gold Serpent Ring": 0x20004FA6,
"ID: Jailer's Key Ring": 0x400007D8,
"ID: Dusk Crown Ring": 0x20004F4C,
"ID: Dark Clutch Ring": 0x20005028,
}
profaned_capital_table = {
"PC: Cursebite Ring": 0x20004E98,
"PC: Court Sorcerer Hood": 0x11BA8140,
"PC: Court Sorcerer Robe": 0x11BA8528,
"PC: Court Sorcerer Gloves": 0x11BA8910,
"PC: Court Sorcerer Trousers": 0x11BA8CF8,
"PC: Wrath of the Gods": 0x4035E0F8,
"PC: Logan's Scroll": 0x40000855,
"PC: Eleonora": 0x006CCB90,
"PC: Court Sorcerer's Staff": 0x00C91C60,
"PC: Greatshield of Glory": 0x01515F30,
"PC: Storm Ruler": 0x006132D0,
"PC: Cinders of a Lord - Yhorm the Giant": 0x4000084D,
"PC: Soul of Yhorm the Giant": 0x400002DC,
}
anor_londo_table = {
"AL: Giant's Coal": 0x40000839,
"AL: Sun Princess Ring": 0x20004FBA,
"AL: Aldrich's Ruby": 0x2000508C,
"AL: Cinders of a Lord - Aldrich": 0x4000084C,
"AL: Soul of Aldrich": 0x400002D5,
}
lothric_castle_table = {
"LC: Hood of Prayer": 0x13AA6A60,
"LC: Robe of Prayer": 0x13AA6E48,
"LC: Skirt of Prayer": 0x13AA7618,
"LC: Sacred Bloom Shield": 0x013572C0,
"LC: Winged Knight Helm": 0x12EBAE40,
"LC: Winged Knight Armor": 0x12EBB228,
"LC: Winged Knight Gauntlets": 0x12EBB610,
"LC: Winged Knight Leggings": 0x12EBB9F8,
"LC: Greatlance": 0x008A8CC0,
"LC: Sniper Crossbow": 0x00D83790,
"LC: Spirit Tree Crest Shield": 0x014466E0,
"LC: Red Tearstone Ring": 0x20004ECA,
"LC: Caitha's Chime": 0x00CA06C0,
"LC: Braille Divine Tome of Lothric": 0x40000848,
"LC: Knight's Ring": 0x20004FEC,
"LC: Sunlight Straight Sword": 0x00203230,
"LC: Soul of Dragonslayer Armour": 0x400002D1,
# The Black Hand Gotthard corpse appears when you have defeated Yhorm and Aldrich and triggered the cutscene
"LC: Grand Archives Key": 0x400007DE, # On Black Hand Gotthard corpse
"LC: Gotthard Twinswords": 0x00F53570 # On Black Hand Gotthard corpse
}
consumed_king_garden_table = {
"CKG: Dragonscale Ring": 0x2000515E,
"CKG: Shadow Mask": 0x14D3F640,
"CKG: Shadow Garb": 0x14D3FA28,
"CKG: Shadow Gauntlets": 0x14D3FE10,
"CKG: Shadow Leggings": 0x14D401F8,
"CKG: Claw": 0x00A7D8C0,
"CKG: Soul of Consumed Oceiros": 0x400002CE,
# "CKG: Path of the Dragon Gesture": 0x40002346, I can't technically randomize it as it is a gesture and not an item
}
grand_archives_table = {
"GA: Avelyn": 0x00D6FF10,
"GA: Witch's Locks": 0x00B7B740,
"GA: Power Within": 0x40253B40,
"GA: Scholar Ring": 0x20004EB6,
"GA: Soul Stream": 0x4018B820,
"GA: Fleshbite Ring": 0x20004EA2,
"GA: Crystal Chime": 0x00CA2DD0,
"GA: Golden Wing Crest Shield": 0x0143CAA0,
"GA: Onikiri and Ubadachi": 0x00F58390,
"GA: Hunter's Ring": 0x20004FF6,
"GA: Divine Pillars of Light": 0x4038C340,
"GA: Cinders of a Lord - Lothric Prince": 0x4000084E,
"GA: Soul of the Twin Princes": 0x400002DB,
"GA: Sage's Crystal Staff": 0x00C8CE40,
}
untended_graves_table = {
"UG: Ashen Estus Ring": 0x200050E6,
"UG: Black Knight Glaive": 0x009AE070,
"UG: Hornet Ring": 0x20004F9C,
"UG: Chaos Blade": 0x004C9960,
"UG: Blacksmith Hammer": 0x007E57C0,
"UG: Eyes of a Fire Keeper": 0x4000085A,
"UG: Coiled Sword Fragment": 0x4000015F,
"UG: Soul of Champion Gundyr": 0x400002C8,
}
archdragon_peak_table = {
"AP: Lightning Clutch Ring": 0x20005014,
"AP: Ancient Dragon Greatshield": 0x013599D0,
"AP: Ring of Steel Protection": 0x20004E48,
"AP: Calamity Ring": 0x20005078,
"AP: Drakeblood Greatsword": 0x00609690,
"AP: Dragonslayer Spear": 0x008CAFA0,
"AP: Thunder Stoneplate Ring": 0x20004E5C,
"AP: Great Magic Barrier": 0x40365628,
"AP: Dragon Chaser's Ashes": 0x40000867,
"AP: Twinkling Dragon Torso Stone": 0x40000184,
"AP: Dragonslayer Helm": 0x158B1140,
"AP: Dragonslayer Armor": 0x158B1528,
"AP: Dragonslayer Gauntlets": 0x158B1910,
"AP: Dragonslayer Leggings": 0x158B1CF8,
"AP: Ricard's Rapier": 0x002E3BF0,
"AP: Soul of the Nameless King": 0x400002D2,
"AP: Dragon Tooth": 0x007E09A0,
"AP: Havel's Greatshield": 0x013376F0,
}
location_dictionary_table = {**cemetery_of_ash_table, **fire_link_shrine_table, **firelink_shrine_bell_tower_table, **high_wall_of_lothric, **undead_settlement_table, **road_of_sacrifice_table,
**cathedral_of_the_deep_table, **farron_keep_table, **catacombs_of_carthus_table, **smouldering_lake_table, **irithyll_of_the_boreal_valley_table,
**irithyll_dungeon_table, **profaned_capital_table, **anor_londo_table, **lothric_castle_table, **consumed_king_garden_table,
**grand_archives_table, **untended_graves_table, **archdragon_peak_table}

View File

@@ -1,25 +0,0 @@
# Dark Souls III
## Where is the settings page?
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
In Dark Souls III, all unique items you can earn from a static corpse, a chest or the death of a Boss/NPC are randomized.
This exclude the upgrade materials such as the titanite shards, the estus shards and the consumables which remain at
the same location. I also added an option available from the settings page to randomize the level of the generated
weapons(from +0 to +10/+5)
To beat the game you need to collect the 4 "Cinders of a Lord" randomized in the multiworld
and kill the final boss "Soul of Cinder"
## What Dark Souls III items can appear in other players' worlds?
Every unique items from Dark Souls III can appear in other player's worlds, such as a piece of armor, an upgraded weapon
or a key item.
## What does another world's item look like in Dark Souls III?
In Dark Souls III, items which need to be sent to other worlds appear as a Prism Stone.

View File

@@ -1,37 +0,0 @@
# Dark Souls III Randomizer Setup Guide
## Required Software
- [Dark Souls III](https://store.steampowered.com/app/374320/DARK_SOULS_III/)
- [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases)
## General Concept
The Dark Souls III AP Client is a dinput8.dll triggered when launching Dark Souls III. This .dll file will launch a command
prompt where you can read information about your run and write any command to interact with the Archipelago server.
The randomization is performed by the AP.json file, an output file generated by the Archipelago server.
## Installation Procedures
<span style="color:tomato">
**This mod can ban you permanently from the FromSoftware servers if used online.**
</span>
This client has only been tested with the Official Steam version of the game (v1.15/1.35) not matter which DLCs are installed.
Get the dinput8.dll from the [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases).
Then you need to add the two following files at the root folder of your game (e.g. "SteamLibrary\steamapps\common\DARK SOULS III\Game"):
- **dinput8.dll**
- **AP.json** : The .json file downloaded from the multiworld room or provided by the host, named AP-{ROOM_ID}.json, has to be renamed to AP.json.
## Joining a MultiWorld Game
1. Run DarkSoulsIII.exe or run the game through Steam
2. Type in "/connect {SERVER_IP}:{SERVER_PORT}" in the "Windows Command Prompt" that opened
3. Once connected, create a new game, choose a class and wait for the others before starting
4. You can quit and launch at anytime during a game
## Where do I get a config file?
The [Player Settings](/games/Dark%20Souls%20III/player-settings) page on the website allows you to
configure your personal settings and export them into a config file

View File

@@ -1,38 +0,0 @@
# Guide d'installation de Dark Souls III Randomizer
## Logiciels requis
- [Dark Souls III](https://store.steampowered.com/app/374320/DARK_SOULS_III/)
- [Client AP de Dark Souls III](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases)
## Concept général
Le client Archipelago de Dark Souls III est un fichier dinput8.dll. Cette .dll va lancer une invite de commande Windows
permettant de lire des informations de la partie et écrire des commandes pour intéragir avec le serveur Archipelago.
Le mélange des objets est réalisé par le fichier AP.json, un fichier généré par le serveur Archipelago.
## Procédures d'installation
<span style="color:tomato">
**Il y a des risques de bannissement permanent des serveurs FromSoftware si ce mod est utilisé en ligne.**
</span>
Ce client a été testé sur la version Steam officielle du jeu (v1.15/1.35), peu importe les DLCs actuellement installés.
Télécharger le fichier dinput8.dll disponible dans le [Client AP de Dark Souls III](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases).
Vous devez ensuite ajouter les deux fichiers suivants à la racine du jeu
(ex: "SteamLibrary\steamapps\common\DARK SOULS III\Game"):
- **dinput8.dll**
- **AP.json** : Le fichier .json téléchargé depuis la <em>room</em> ou donné par l'hôte de la partie, nommé AP-{ROOM_ID}.json, doit être renommé en AP.json.
## Rejoindre une partie Multiworld
1. Lancer DarkSoulsIII.exe ou lancer le jeu depuis Steam
2. Ecrire "/connect {SERVER_IP}:{SERVER_PORT}" dans l'invite de commande Windows ouverte au lancement du jeu
3. Une fois connecté, créez une nouvelle partie, choisissez une classe et attendez que les autres soient prêts avant de lancer
4. Vous pouvez quitter et lancer le jeu n'importe quand pendant une partie
## Où trouver le fichier de configuration ?
La [Page de configuration](/games/Dark%20Souls%20III/player-settings) sur le site vous permez de configurer vos
paramètres et de les exporter sous la forme d'un fichier.

View File

@@ -1,221 +0,0 @@
import logging
import asyncio
from NetUtils import ClientStatus, color
from SNIClient import Context, snes_buffered_write, snes_flush_writes, snes_read
from Patch import GAME_DKC3
snes_logger = logging.getLogger("SNES")
# DKC3 - DKC3_TODO: Check these values
ROM_START = 0x000000
WRAM_START = 0xF50000
WRAM_SIZE = 0x20000
SRAM_START = 0xE00000
SAVEDATA_START = WRAM_START + 0xF000
SAVEDATA_SIZE = 0x500
DKC3_ROMNAME_START = 0x00FFC0
DKC3_ROMHASH_START = 0x7FC0
ROMNAME_SIZE = 0x15
ROMHASH_SIZE = 0x15
DKC3_RECV_PROGRESS_ADDR = WRAM_START + 0x632 # DKC3_TODO: Find a permanent home for this
DKC3_FILE_NAME_ADDR = WRAM_START + 0x5D9
DEATH_LINK_ACTIVE_ADDR = DKC3_ROMNAME_START + 0x15 # DKC3_TODO: Find a permanent home for this
async def deathlink_kill_player(ctx: Context):
pass
#if ctx.game == GAME_DKC3:
# DKC3_TODO: Handle Receiving Deathlink
async def dkc3_rom_init(ctx: Context):
if not ctx.rom:
ctx.finished_game = False
ctx.death_link_allow_survive = False
game_name = await snes_read(ctx, DKC3_ROMNAME_START, 0x15)
if game_name is None or game_name != b"DONKEY KONG COUNTRY 3":
return False
else:
ctx.game = GAME_DKC3
ctx.items_handling = 0b111 # remote items
rom = await snes_read(ctx, DKC3_ROMHASH_START, ROMHASH_SIZE)
if rom is None or rom == bytes([0] * ROMHASH_SIZE):
return False
ctx.rom = rom
#death_link = await snes_read(ctx, DEATH_LINK_ACTIVE_ADDR, 1)
## DKC3_TODO: Handle Deathlink
#if death_link:
# ctx.allow_collect = bool(death_link[0] & 0b100)
# await ctx.update_death_link(bool(death_link[0] & 0b1))
return True
async def dkc3_game_watcher(ctx: Context):
if ctx.game == GAME_DKC3:
# DKC3_TODO: Handle Deathlink
save_file_name = await snes_read(ctx, DKC3_FILE_NAME_ADDR, 0x5)
if save_file_name is None or save_file_name[0] == 0x00:
# We haven't loaded a save file
return
new_checks = []
from worlds.dkc3.Rom import location_rom_data, item_rom_data
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)
masked_data = data[0] & (1 << loc_data[1])
bit_set = (masked_data != 0)
invert_bit = ((len(loc_data) >= 3) and loc_data[2])
if bit_set != invert_bit:
# DKC3_TODO: Handle non-included checks
new_checks.append(loc_id)
verify_save_file_name = await snes_read(ctx, DKC3_FILE_NAME_ADDR, 0x5)
if verify_save_file_name is None or verify_save_file_name[0] == 0x00 or verify_save_file_name != save_file_name:
# We have somehow exited the save file (or worse)
return
rom = await snes_read(ctx, DKC3_ROMHASH_START, ROMHASH_SIZE)
if rom != ctx.rom:
ctx.rom = None
# We have somehow loaded a different ROM
return
for new_check_id in new_checks:
ctx.locations_checked.add(new_check_id)
location = ctx.location_names[new_check_id]
snes_logger.info(
f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}])
# DKC3_TODO: Make this actually visually display new things received (ASM Hook required)
recv_count = await snes_read(ctx, DKC3_RECV_PROGRESS_ADDR, 1)
recv_index = recv_count[0]
if recv_index < len(ctx.items_received):
item = ctx.items_received[recv_index]
recv_index += 1
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
color(ctx.item_names[item.item], 'red', 'bold'),
color(ctx.player_names[item.player], 'yellow'),
ctx.location_names[item.location], recv_index, len(ctx.items_received)))
snes_buffered_write(ctx, DKC3_RECV_PROGRESS_ADDR, bytes([recv_index]))
if item.item in item_rom_data:
item_count = await snes_read(ctx, WRAM_START + item_rom_data[item.item][0], 0x1)
new_item_count = item_count[0] + 1
for address in item_rom_data[item.item]:
snes_buffered_write(ctx, WRAM_START + address, bytes([new_item_count]))
# Handle Coin Displays
current_level = await snes_read(ctx, WRAM_START + 0x5E3, 0x5)
overworld_locked = ((await snes_read(ctx, WRAM_START + 0x5FC, 0x1))[0] == 0x01)
if item.item == 0xDC3002 and not overworld_locked and (current_level[0] == 0x0A and current_level[2] == 0x00 and current_level[4] == 0x03):
# Bazaar and Barter
item_count = await snes_read(ctx, WRAM_START + 0xB02, 0x1)
new_item_count = item_count[0] + 1
snes_buffered_write(ctx, WRAM_START + 0xB02, bytes([new_item_count]))
elif item.item == 0xDC3002 and not overworld_locked and current_level[0] == 0x04:
# Swanky
item_count = await snes_read(ctx, WRAM_START + 0xA26, 0x1)
new_item_count = item_count[0] + 1
snes_buffered_write(ctx, WRAM_START + 0xA26, bytes([new_item_count]))
elif item.item == 0xDC3003 and not overworld_locked and (current_level[0] == 0x0A and current_level[2] == 0x08 and current_level[4] == 0x01):
# Boomer
item_count = await snes_read(ctx, WRAM_START + 0xB02, 0x1)
new_item_count = item_count[0] + 1
snes_buffered_write(ctx, WRAM_START + 0xB02, bytes([new_item_count]))
else:
# Handle Patch and Skis
if item.item == 0xDC3007:
num_upgrades = 1
inventory = await snes_read(ctx, WRAM_START + 0x605, 0xF)
if (inventory[0] & 0x02):
num_upgrades = 3
elif (inventory[13] & 0x08) or (inventory[0] & 0x01):
num_upgrades = 2
if num_upgrades == 1:
snes_buffered_write(ctx, WRAM_START + 0x605, bytes([inventory[0] | 0x01]))
if inventory[4] == 0:
snes_buffered_write(ctx, WRAM_START + 0x609, bytes([0x01]))
elif inventory[6] == 0:
snes_buffered_write(ctx, WRAM_START + 0x60B, bytes([0x01]))
elif inventory[8] == 0:
snes_buffered_write(ctx, WRAM_START + 0x60D, bytes([0x01]))
elif inventory[10] == 0:
snes_buffered_write(ctx, WRAM_START + 0x60F, bytes([0x01]))
cove_mekanos_progress = await snes_read(ctx, WRAM_START + 0x691, 0x2)
snes_buffered_write(ctx, WRAM_START + 0x691, bytes([cove_mekanos_progress[0] | 0x01]))
snes_buffered_write(ctx, WRAM_START + 0x692, bytes([cove_mekanos_progress[1] | 0x01]))
elif num_upgrades == 2:
snes_buffered_write(ctx, WRAM_START + 0x605, bytes([inventory[0] | 0x02]))
if inventory[4] == 0:
snes_buffered_write(ctx, WRAM_START + 0x609, bytes([0x02]))
elif inventory[6] == 0:
snes_buffered_write(ctx, WRAM_START + 0x60B, bytes([0x02]))
elif inventory[8] == 0:
snes_buffered_write(ctx, WRAM_START + 0x60D, bytes([0x02]))
elif inventory[10] == 0:
snes_buffered_write(ctx, WRAM_START + 0x60F, bytes([0x02]))
elif num_upgrades == 3:
snes_buffered_write(ctx, WRAM_START + 0x606, bytes([inventory[1] | 0x20]))
k3_ridge_progress = await snes_read(ctx, WRAM_START + 0x693, 0x2)
snes_buffered_write(ctx, WRAM_START + 0x693, bytes([k3_ridge_progress[0] | 0x01]))
snes_buffered_write(ctx, WRAM_START + 0x694, bytes([k3_ridge_progress[1] | 0x01]))
elif item.item == 0xDC3000:
# Handle Victory
if not ctx.finished_game:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
else:
print("Item Not Recognized: ", item.item)
pass
await snes_flush_writes(ctx)
# 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)
# Calculate Boomer Cost Text
boomer_cost_text = await snes_read(ctx, WRAM_START + 0xAAFD, 2)
if boomer_cost_text[0] == 0x31 and boomer_cost_text[1] == 0x35:
boomer_cost = await snes_read(ctx, ROM_START + 0x349857, 1)
boomer_cost_tens = int(boomer_cost[0]) // 10
boomer_cost_ones = int(boomer_cost[0]) % 10
snes_buffered_write(ctx, WRAM_START + 0xAAFD, bytes([0x30 + boomer_cost_tens, 0x30 + boomer_cost_ones]))
await snes_flush_writes(ctx)
boomer_final_cost_text = await snes_read(ctx, WRAM_START + 0xAB9B, 2)
if boomer_final_cost_text[0] == 0x32 and boomer_final_cost_text[1] == 0x35:
boomer_cost = await snes_read(ctx, ROM_START + 0x349857, 1)
boomer_cost_tens = boomer_cost[0] // 10
boomer_cost_ones = boomer_cost[0] % 10
snes_buffered_write(ctx, WRAM_START + 0xAB9B, bytes([0x30 + boomer_cost_tens, 0x30 + boomer_cost_ones]))
await snes_flush_writes(ctx)

View File

@@ -1,52 +0,0 @@
import typing
from BaseClasses import Item, ItemClassification
from .Names import ItemName
class ItemData(typing.NamedTuple):
code: typing.Optional[int]
progression: bool
quantity: int = 1
event: bool = False
class DKC3Item(Item):
game: str = "Donkey Kong Country 3"
# Separate tables for each type of item.
junk_table = {
ItemName.one_up_balloon: ItemData(0xDC3001, False),
ItemName.bear_coin: ItemData(0xDC3002, False),
}
collectable_table = {
ItemName.bonus_coin: ItemData(0xDC3003, True),
ItemName.dk_coin: ItemData(0xDC3004, True),
ItemName.banana_bird: ItemData(0xDC3005, True),
ItemName.krematoa_cog: ItemData(0xDC3006, True),
ItemName.progressive_boat: ItemData(0xDC3007, True),
}
inventory_table = {
ItemName.present: ItemData(0xDC3008, True),
ItemName.bowling_ball: ItemData(0xDC3009, True),
ItemName.shell: ItemData(0xDC300A, True),
ItemName.mirror: ItemData(0xDC300B, True),
ItemName.flower: ItemData(0xDC300C, True),
ItemName.wrench: ItemData(0xDC300D, True),
}
event_table = {
ItemName.victory: ItemData(0xDC3000, True),
}
# Complete item table.
item_table = {
**junk_table,
**collectable_table,
**event_table,
}
lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_table.items() if data.code}

View File

@@ -1,115 +0,0 @@
from .Names import LocationName
class DKC3Level():
nameIDAddress: int
levelIDAddress: int
nameID: int
levelID: int
def __init__(self, nameIDAddress: int, levelIDAddress: int, nameID: int, levelID: int):
self.nameIDAddress = nameIDAddress
self.levelIDAddress = levelIDAddress
self.nameID = nameID
self.levelID = levelID
level_dict = {
LocationName.lakeside_limbo_region: DKC3Level(0x34D19C, 0x34D19D, 0x01, 0x25),
LocationName.doorstop_dash_region: DKC3Level(0x34D1A7, 0x34D1A8, 0x02, 0x28),
LocationName.tidal_trouble_region: DKC3Level(0x34D1BD, 0x34D1BE, 0x04, 0x27),
LocationName.skiddas_row_region: DKC3Level(0x34D1C8, 0x34D1C9, 0x05, 0x2B),
LocationName.murky_mill_region: DKC3Level(0x34D1D3, 0x34D1D4, 0x0D, 0x2A),
LocationName.barrel_shield_bust_up_region: DKC3Level(0x34D217, 0x34D218, 0x0B, 0x30),
LocationName.riverside_race_region: DKC3Level(0x34D22D, 0x34D22E, 0x0C, 0x32),
LocationName.squeals_on_wheels_region: DKC3Level(0x34D238, 0x34D239, 0x06, 0x29),
LocationName.springin_spiders_region: DKC3Level(0x34D24E, 0x34D24F, 0x0E, 0x2F),
LocationName.bobbing_barrel_brawl_region: DKC3Level(0x34D264, 0x34D265, 0x37, 0x34),
LocationName.bazzas_blockade_region: DKC3Level(0x34D29D, 0x34D29E, 0x14, 0x35),
LocationName.rocket_barrel_ride_region: DKC3Level(0x34D2A8, 0x34D2A9, 0x15, 0x38),
LocationName.kreeping_klasps_region: DKC3Level(0x34D2BE, 0x34D2BF, 0x16, 0x26),
LocationName.tracker_barrel_trek_region: DKC3Level(0x34D2D4, 0x34D2D5, 0x17, 0x39),
LocationName.fish_food_frenzy_region: DKC3Level(0x34D2DF, 0x34D2E0, 0x18, 0x36),
LocationName.fire_ball_frenzy_region: DKC3Level(0x34D30D, 0x34D30E, 0x1B, 0x3B),
LocationName.demolition_drain_pipe_region: DKC3Level(0x34D323, 0x34D324, 0x1D, 0x40),
LocationName.ripsaw_rage_region: DKC3Level(0x34D339, 0x34D33A, 0x1E, 0x2E),
LocationName.blazing_bazookas_region: DKC3Level(0x34D34F, 0x34D350, 0x1F, 0x3C),
LocationName.low_g_labyrinth_region: DKC3Level(0x34D35A, 0x34D35B, 0x20, 0x3E),
LocationName.krevice_kreepers_region: DKC3Level(0x34D388, 0x34D389, 0x23, 0x41),
LocationName.tearaway_toboggan_region: DKC3Level(0x34D393, 0x34D394, 0x24, 0x2D),
LocationName.barrel_drop_bounce_region: DKC3Level(0x34D39E, 0x34D39F, 0x25, 0x3A),
LocationName.krack_shot_kroc_region: DKC3Level(0x34D3A9, 0x34D3AA, 0x26, 0x3D),
LocationName.lemguin_lunge_region: DKC3Level(0x34D3B4, 0x34D3B5, 0x27, 0x2C),
LocationName.buzzer_barrage_region: DKC3Level(0x34D40E, 0x34D40F, 0x2B, 0x44),
LocationName.kong_fused_cliffs_region: DKC3Level(0x34D424, 0x34D425, 0x2D, 0x42),
LocationName.floodlit_fish_region: DKC3Level(0x34D42F, 0x34D430, 0x2E, 0x37),
LocationName.pothole_panic_region: DKC3Level(0x34D43A, 0x34D43B, 0x2F, 0x45),
LocationName.ropey_rumpus_region: DKC3Level(0x34D450, 0x34D451, 0x30, 0x43),
LocationName.konveyor_rope_clash_region: DKC3Level(0x34D489, 0x34D48A, 0x38, 0x48),
LocationName.creepy_caverns_region: DKC3Level(0x34D49F, 0x34D4A0, 0x36, 0x46),
LocationName.lightning_lookout_region: DKC3Level(0x34D4AA, 0x34D4AB, 0x10, 0x33),
LocationName.koindozer_klamber_region: DKC3Level(0x34D4C0, 0x34D4C1, 0x34, 0x47),
LocationName.poisonous_pipeline_region: DKC3Level(0x34D4D6, 0x34D4D7, 0x39, 0x3F),
LocationName.stampede_sprint_region: DKC3Level(0x34D51A, 0x34D51B, 0x3D, 0x49),
LocationName.criss_cross_cliffs_region: DKC3Level(0x34D525, 0x34D526, 0x3E, 0x4A),
LocationName.tyrant_twin_tussle_region: DKC3Level(0x34D530, 0x34D531, 0x3F, 0x4B),
LocationName.swoopy_salvo_region: DKC3Level(0x34D53B, 0x34D53C, 0x40, 0x31),
#LocationName.rocket_rush_region: DKC3Level(0x34D546, 0x34D547, 0x05, 0x4C), # Rocket Rush is not getting shuffled
}
level_list = [
LocationName.lakeside_limbo_region,
LocationName.doorstop_dash_region,
LocationName.tidal_trouble_region,
LocationName.skiddas_row_region,
LocationName.murky_mill_region,
LocationName.barrel_shield_bust_up_region,
LocationName.riverside_race_region,
LocationName.squeals_on_wheels_region,
LocationName.springin_spiders_region,
LocationName.bobbing_barrel_brawl_region,
LocationName.bazzas_blockade_region,
LocationName.rocket_barrel_ride_region,
LocationName.kreeping_klasps_region,
LocationName.tracker_barrel_trek_region,
LocationName.fish_food_frenzy_region,
LocationName.fire_ball_frenzy_region,
LocationName.demolition_drain_pipe_region,
LocationName.ripsaw_rage_region,
LocationName.blazing_bazookas_region,
LocationName.low_g_labyrinth_region,
LocationName.krevice_kreepers_region,
LocationName.tearaway_toboggan_region,
LocationName.barrel_drop_bounce_region,
LocationName.krack_shot_kroc_region,
LocationName.lemguin_lunge_region,
LocationName.buzzer_barrage_region,
LocationName.kong_fused_cliffs_region,
LocationName.floodlit_fish_region,
LocationName.pothole_panic_region,
LocationName.ropey_rumpus_region,
LocationName.konveyor_rope_clash_region,
LocationName.creepy_caverns_region,
LocationName.lightning_lookout_region,
LocationName.koindozer_klamber_region,
LocationName.poisonous_pipeline_region,
LocationName.stampede_sprint_region,
LocationName.criss_cross_cliffs_region,
LocationName.tyrant_twin_tussle_region,
LocationName.swoopy_salvo_region,
#LocationName.rocket_rush_region,
]

View File

@@ -1,283 +0,0 @@
import typing
from BaseClasses import Location
from .Names import LocationName
class DKC3Location(Location):
game: str = "Donkey Kong Country 3"
progress_byte: int = 0x000000
progress_bit: int = 0
inverted_bit: bool = False
def __init__(self, player: int, name: str = '', address: int = None, parent=None, prog_byte: int = None, prog_bit: int = None, invert: bool = False):
super().__init__(player, name, address, parent)
self.progress_byte = prog_byte
self.progress_bit = prog_bit
self.inverted_bit = invert
level_location_table = {
LocationName.lakeside_limbo_flag: 0xDC3000,
LocationName.lakeside_limbo_bonus_1: 0xDC3001,
LocationName.lakeside_limbo_bonus_2: 0xDC3002,
LocationName.lakeside_limbo_dk: 0xDC3003,
LocationName.doorstop_dash_flag: 0xDC3004,
LocationName.doorstop_dash_bonus_1: 0xDC3005,
LocationName.doorstop_dash_bonus_2: 0xDC3006,
LocationName.doorstop_dash_dk: 0xDC3007,
LocationName.tidal_trouble_flag: 0xDC3008,
LocationName.tidal_trouble_bonus_1: 0xDC3009,
LocationName.tidal_trouble_bonus_2: 0xDC300A,
LocationName.tidal_trouble_dk: 0xDC300B,
LocationName.skiddas_row_flag: 0xDC300C,
LocationName.skiddas_row_bonus_1: 0xDC300D,
LocationName.skiddas_row_bonus_2: 0xDC300E,
LocationName.skiddas_row_dk: 0xDC300F,
LocationName.murky_mill_flag: 0xDC3010,
LocationName.murky_mill_bonus_1: 0xDC3011,
LocationName.murky_mill_bonus_2: 0xDC3012,
LocationName.murky_mill_dk: 0xDC3013,
LocationName.barrel_shield_bust_up_flag: 0xDC3014,
LocationName.barrel_shield_bust_up_bonus_1: 0xDC3015,
LocationName.barrel_shield_bust_up_bonus_2: 0xDC3016,
LocationName.barrel_shield_bust_up_dk: 0xDC3017,
LocationName.riverside_race_flag: 0xDC3018,
LocationName.riverside_race_bonus_1: 0xDC3019,
LocationName.riverside_race_bonus_2: 0xDC301A,
LocationName.riverside_race_dk: 0xDC301B,
LocationName.squeals_on_wheels_flag: 0xDC301C,
LocationName.squeals_on_wheels_bonus_1: 0xDC301D,
LocationName.squeals_on_wheels_bonus_2: 0xDC301E,
LocationName.squeals_on_wheels_dk: 0xDC301F,
LocationName.springin_spiders_flag: 0xDC3020,
LocationName.springin_spiders_bonus_1: 0xDC3021,
LocationName.springin_spiders_bonus_2: 0xDC3022,
LocationName.springin_spiders_dk: 0xDC3023,
LocationName.bobbing_barrel_brawl_flag: 0xDC3024,
LocationName.bobbing_barrel_brawl_bonus_1: 0xDC3025,
LocationName.bobbing_barrel_brawl_bonus_2: 0xDC3026,
LocationName.bobbing_barrel_brawl_dk: 0xDC3027,
LocationName.bazzas_blockade_flag: 0xDC3028,
LocationName.bazzas_blockade_bonus_1: 0xDC3029,
LocationName.bazzas_blockade_bonus_2: 0xDC302A,
LocationName.bazzas_blockade_dk: 0xDC302B,
LocationName.rocket_barrel_ride_flag: 0xDC302C,
LocationName.rocket_barrel_ride_bonus_1: 0xDC302D,
LocationName.rocket_barrel_ride_bonus_2: 0xDC302E,
LocationName.rocket_barrel_ride_dk: 0xDC302F,
LocationName.kreeping_klasps_flag: 0xDC3030,
LocationName.kreeping_klasps_bonus_1: 0xDC3031,
LocationName.kreeping_klasps_bonus_2: 0xDC3032,
LocationName.kreeping_klasps_dk: 0xDC3033,
LocationName.tracker_barrel_trek_flag: 0xDC3034,
LocationName.tracker_barrel_trek_bonus_1: 0xDC3035,
LocationName.tracker_barrel_trek_bonus_2: 0xDC3036,
LocationName.tracker_barrel_trek_dk: 0xDC3037,
LocationName.fish_food_frenzy_flag: 0xDC3038,
LocationName.fish_food_frenzy_bonus_1: 0xDC3039,
LocationName.fish_food_frenzy_bonus_2: 0xDC303A,
LocationName.fish_food_frenzy_dk: 0xDC303B,
LocationName.fire_ball_frenzy_flag: 0xDC303C,
LocationName.fire_ball_frenzy_bonus_1: 0xDC303D,
LocationName.fire_ball_frenzy_bonus_2: 0xDC303E,
LocationName.fire_ball_frenzy_dk: 0xDC303F,
LocationName.demolition_drain_pipe_flag: 0xDC3040,
LocationName.demolition_drain_pipe_bonus_1: 0xDC3041,
LocationName.demolition_drain_pipe_bonus_2: 0xDC3042,
LocationName.demolition_drain_pipe_dk: 0xDC3043,
LocationName.ripsaw_rage_flag: 0xDC3044,
LocationName.ripsaw_rage_bonus_1: 0xDC3045,
LocationName.ripsaw_rage_bonus_2: 0xDC3046,
LocationName.ripsaw_rage_dk: 0xDC3047,
LocationName.blazing_bazookas_flag: 0xDC3048,
LocationName.blazing_bazookas_bonus_1: 0xDC3049,
LocationName.blazing_bazookas_bonus_2: 0xDC304A,
LocationName.blazing_bazookas_dk: 0xDC304B,
LocationName.low_g_labyrinth_flag: 0xDC304C,
LocationName.low_g_labyrinth_bonus_1: 0xDC304D,
LocationName.low_g_labyrinth_bonus_2: 0xDC304E,
LocationName.low_g_labyrinth_dk: 0xDC304F,
LocationName.krevice_kreepers_flag: 0xDC3050,
LocationName.krevice_kreepers_bonus_1: 0xDC3051,
LocationName.krevice_kreepers_bonus_2: 0xDC3052,
LocationName.krevice_kreepers_dk: 0xDC3053,
LocationName.tearaway_toboggan_flag: 0xDC3054,
LocationName.tearaway_toboggan_bonus_1: 0xDC3055,
LocationName.tearaway_toboggan_bonus_2: 0xDC3056,
LocationName.tearaway_toboggan_dk: 0xDC3057,
LocationName.barrel_drop_bounce_flag: 0xDC3058,
LocationName.barrel_drop_bounce_bonus_1: 0xDC3059,
LocationName.barrel_drop_bounce_bonus_2: 0xDC305A,
LocationName.barrel_drop_bounce_dk: 0xDC305B,
LocationName.krack_shot_kroc_flag: 0xDC305C,
LocationName.krack_shot_kroc_bonus_1: 0xDC305D,
LocationName.krack_shot_kroc_bonus_2: 0xDC305E,
LocationName.krack_shot_kroc_dk: 0xDC305F,
LocationName.lemguin_lunge_flag: 0xDC3060,
LocationName.lemguin_lunge_bonus_1: 0xDC3061,
LocationName.lemguin_lunge_bonus_2: 0xDC3062,
LocationName.lemguin_lunge_dk: 0xDC3063,
LocationName.buzzer_barrage_flag: 0xDC3064,
LocationName.buzzer_barrage_bonus_1: 0xDC3065,
LocationName.buzzer_barrage_bonus_2: 0xDC3066,
LocationName.buzzer_barrage_dk: 0xDC3067,
LocationName.kong_fused_cliffs_flag: 0xDC3068,
LocationName.kong_fused_cliffs_bonus_1: 0xDC3069,
LocationName.kong_fused_cliffs_bonus_2: 0xDC306A,
LocationName.kong_fused_cliffs_dk: 0xDC306B,
LocationName.floodlit_fish_flag: 0xDC306C,
LocationName.floodlit_fish_bonus_1: 0xDC306D,
LocationName.floodlit_fish_bonus_2: 0xDC306E,
LocationName.floodlit_fish_dk: 0xDC306F,
LocationName.pothole_panic_flag: 0xDC3070,
LocationName.pothole_panic_bonus_1: 0xDC3071,
LocationName.pothole_panic_bonus_2: 0xDC3072,
LocationName.pothole_panic_dk: 0xDC3073,
LocationName.ropey_rumpus_flag: 0xDC3074,
LocationName.ropey_rumpus_bonus_1: 0xDC3075,
LocationName.ropey_rumpus_bonus_2: 0xDC3076,
LocationName.ropey_rumpus_dk: 0xDC3077,
LocationName.konveyor_rope_clash_flag: 0xDC3078,
LocationName.konveyor_rope_clash_bonus_1: 0xDC3079,
LocationName.konveyor_rope_clash_bonus_2: 0xDC307A,
LocationName.konveyor_rope_clash_dk: 0xDC307B,
LocationName.creepy_caverns_flag: 0xDC307C,
LocationName.creepy_caverns_bonus_1: 0xDC307D,
LocationName.creepy_caverns_bonus_2: 0xDC307E,
LocationName.creepy_caverns_dk: 0xDC307F,
LocationName.lightning_lookout_flag: 0xDC3080,
LocationName.lightning_lookout_bonus_1: 0xDC3081,
LocationName.lightning_lookout_bonus_2: 0xDC3082,
LocationName.lightning_lookout_dk: 0xDC3083,
LocationName.koindozer_klamber_flag: 0xDC3084,
LocationName.koindozer_klamber_bonus_1: 0xDC3085,
LocationName.koindozer_klamber_bonus_2: 0xDC3086,
LocationName.koindozer_klamber_dk: 0xDC3087,
LocationName.poisonous_pipeline_flag: 0xDC3088,
LocationName.poisonous_pipeline_bonus_1: 0xDC3089,
LocationName.poisonous_pipeline_bonus_2: 0xDC308A,
LocationName.poisonous_pipeline_dk: 0xDC308B,
LocationName.stampede_sprint_flag: 0xDC308C,
LocationName.stampede_sprint_bonus_1: 0xDC308D,
LocationName.stampede_sprint_bonus_2: 0xDC308E,
LocationName.stampede_sprint_bonus_3: 0xDC308F,
LocationName.stampede_sprint_dk: 0xDC3090,
LocationName.criss_cross_cliffs_flag: 0xDC3091,
LocationName.criss_cross_cliffs_bonus_1: 0xDC3092,
LocationName.criss_cross_cliffs_bonus_2: 0xDC3093,
LocationName.criss_cross_cliffs_dk: 0xDC3094,
LocationName.tyrant_twin_tussle_flag: 0xDC3095,
LocationName.tyrant_twin_tussle_bonus_1: 0xDC3096,
LocationName.tyrant_twin_tussle_bonus_2: 0xDC3097,
LocationName.tyrant_twin_tussle_bonus_3: 0xDC3098,
LocationName.tyrant_twin_tussle_dk: 0xDC3099,
LocationName.swoopy_salvo_flag: 0xDC309A,
LocationName.swoopy_salvo_bonus_1: 0xDC309B,
LocationName.swoopy_salvo_bonus_2: 0xDC309C,
LocationName.swoopy_salvo_bonus_3: 0xDC309D,
LocationName.swoopy_salvo_dk: 0xDC309E,
LocationName.rocket_rush_flag: 0xDC309F,
LocationName.rocket_rush_dk: 0xDC30A0,
}
boss_location_table = {
LocationName.belchas_barn: 0xDC30A1,
LocationName.arichs_ambush: 0xDC30A2,
LocationName.squirts_showdown: 0xDC30A3,
LocationName.kaos_karnage: 0xDC30A4,
LocationName.bleaks_house: 0xDC30A5,
LocationName.barboss_barrier: 0xDC30A6,
LocationName.kastle_kaos: 0xDC30A7,
LocationName.knautilus: 0xDC30A8,
}
secret_cave_location_table = {
LocationName.belchas_burrow: 0xDC30A9,
LocationName.kong_cave: 0xDC30AA,
LocationName.undercover_cove: 0xDC30AB,
LocationName.ks_cache: 0xDC30AC,
LocationName.hill_top_hoard: 0xDC30AD,
LocationName.bounty_beach: 0xDC30AE,
LocationName.smugglers_cove: 0xDC30AF,
LocationName.arichs_hoard: 0xDC30B0,
LocationName.bounty_bay: 0xDC30B1,
LocationName.sky_high_secret: 0xDC30B2,
LocationName.glacial_grotto: 0xDC30B3,
LocationName.cifftop_cache: 0xDC30B4,
LocationName.sewer_stockpile: 0xDC30B5,
LocationName.banana_bird_mother: 0xDC30B6,
}
brothers_bear_location_table = {
LocationName.bazaars_general_store_1: 0xDC30B7,
LocationName.bazaars_general_store_2: 0xDC30B8,
LocationName.brambles_bungalow: 0xDC30B9,
LocationName.flower_spot: 0xDC30BA,
LocationName.barters_swap_shop: 0xDC30BB,
LocationName.barnacles_island: 0xDC30BC,
LocationName.blues_beach_hut: 0xDC30BD,
LocationName.blizzards_basecamp: 0xDC30BE,
}
all_locations = {
**level_location_table,
**boss_location_table,
**secret_cave_location_table,
**brothers_bear_location_table,
}
location_table = {}
def setup_locations(world, player: int):
location_table = {**level_location_table, **boss_location_table, **secret_cave_location_table}
if False:#world.include_trade_sequence[player].value:
location_table.update({**brothers_bear_location_table})
return location_table
lookup_id_to_name: typing.Dict[int, str] = {id: name for name, _ in all_locations.items()}

View File

@@ -1,21 +0,0 @@
# Junk Definitions
one_up_balloon = "1-Up Balloon"
bear_coin = "Bear Coin"
# Collectable Definitions
bonus_coin = "Bonus Coin"
dk_coin = "DK Coin"
banana_bird = "Banana Bird"
krematoa_cog = "Krematoa Cog"
# Inventory Definitions
progressive_boat = "Progressive Boat Upgrade"
present = "Present"
bowling_ball = "Bowling Ball"
shell = "Shell"
mirror = "Mirror"
flower = "Flupperius Petallus Pongus"
wrench = "No. 6 Wrench"
# Other Definitions
victory = "Donkey Kong"

View File

@@ -1,336 +0,0 @@
# Level Definitions
lakeside_limbo_flag = "Lakeside Limbo - Flag"
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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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"
low_g_labyrinth_flag = "Low-G Labyrinth - Flag"
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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_bonus_1 = "Swoopy Salvo - Bonus 1"
swoopy_salvo_bonus_2 = "Swoopy Salvo - Bonus 2"
swoopy_salvo_bonus_3 = "Swoopy Salvo - Bonus 3"
swoopy_salvo_dk = "Swoopy Salvo - DK Coin"
rocket_rush_flag = "Rocket Rush - Flag"
rocket_rush_dk = "Rocket Rush - DK Coin"
# Boss Definitions
belchas_barn = "Belcha's Barn"
arichs_ambush = "Arich's Ambush"
squirts_showdown = "Squirt's Showdown"
kaos_karnage = "KAOS Karnage"
bleaks_house = "Bleak's House"
barboss_barrier = "Barbos's Barrier"
kastle_kaos = "Kastle KAOS"
knautilus = "Knautilus"
# Banana Bird Cave Definitions
belchas_burrow = "Belcha's Burrow"
kong_cave = "Kong Cave"
undercover_cove = "Undercover Cove"
ks_cache = "K's Cache"
hill_top_hoard = "Hill-Top Hoard"
bounty_beach = "Bounty Beach"
smugglers_cove = "Smuggler's Cove"
arichs_hoard = "Arich's Hoard"
bounty_bay = "Bounty Bay"
sky_high_secret = "Sky-High Secret"
glacial_grotto = "Glacial Grotto"
cifftop_cache = "Clifftop Cache"
sewer_stockpile = "Sewer Stockpile"
banana_bird_mother = "Banana Bird Mother"
# Brothers Bear Definitions
bazaars_general_store_1 = "Bazaar's General Store - 1"
bazaars_general_store_2 = "Bazaar's General Store - 2"
brambles_bungalow = "Bramble's Bungalow"
flower_spot = "Flower Spot"
barters_swap_shop = "Barter's Swap Shop"
barnacles_island = "Barnacle's Island"
blues_beach_hut = "Blue's Beach Hut"
blizzards_basecamp = "Bizzard's Basecamp"
# Region Definitions
menu_region = "Menu"
overworld_1_region = "Overworld 1"
overworld_2_region = "Overworld 2"
overworld_3_region = "Overworld 3"
overworld_4_region = "Overworld 4"
bazaar_region = "Bazaar's General Store Region"
bramble_region = "Bramble's Bungalow Region"
flower_spot_region = "Flower Spot Region"
barter_region = "Barter's Swap Shop Region"
barnacle_region = "Barnacle's Island Region"
blue_region = "Blue's Beach Hut Region"
blizzard_region = "Bizzard's Basecamp Region"
lake_orangatanga_region = "Lake_Orangatanga"
kremwood_forest_region = "Kremwood Forest"
cotton_top_cove_region = "Cotton-Top Cove"
mekanos_region = "Mekanos"
k3_region = "K3"
razor_ridge_region = "Razor Ridge"
kaos_kore_region = "KAOS Kore"
krematoa_region = "Krematoa"
belchas_barn_region = "Belcha's Barn Region"
arichs_ambush_region = "Arich's Ambush Region"
squirts_showdown_region = "Squirt's Showdown Region"
kaos_karnage_region = "KAOS Karnage Region"
bleaks_house_region = "Bleak's House Region"
barboss_barrier_region = "Barbos's Barrier Region"
kastle_kaos_region = "Kastle KAOS Region"
knautilus_region = "Knautilus Region"
belchas_burrow_region = "Belcha's Burrow Region"
kong_cave_region = "Kong Cave Region"
undercover_cove_region = "Undercover Cove Region"
ks_cache_region = "K's Cache Region"
hill_top_hoard_region = "Hill-Top Hoard Region"
bounty_beach_region = "Bounty Beach Region"
smugglers_cove_region = "Smuggler's Cove Region"
arichs_hoard_region = "Arich's Hoard Region"
bounty_bay_region = "Bounty Bay Region"
sky_high_secret_region = "Sky-High Secret Region"
glacial_grotto_region = "Glacial Grotto Region"
cifftop_cache_region = "Clifftop Cache Region"
sewer_stockpile_region = "Sewer Stockpile Region"
lakeside_limbo_region = "Lakeside Limbo"
doorstop_dash_region = "Doorstop Dash"
tidal_trouble_region = "Tidal Trouble"
skiddas_row_region = "Skidda's Row"
murky_mill_region = "Murky Mill"
barrel_shield_bust_up_region = "Barrel Shield Bust-Up"
riverside_race_region = "Riverside Race"
squeals_on_wheels_region = "Squeals On Wheels"
springin_spiders_region = "Springin' Spiders"
bobbing_barrel_brawl_region = "Bobbing Barrel Brawl"
bazzas_blockade_region = "Bazza's Blockade"
rocket_barrel_ride_region = "Rocket Barrel Ride"
kreeping_klasps_region = "Kreeping Klasps"
tracker_barrel_trek_region = "Tracker Barrel Trek"
fish_food_frenzy_region = "Fish Food Frenzy"
fire_ball_frenzy_region = "Fire-Ball Frenzy"
demolition_drain_pipe_region = "Demolition Drain-Pipe"
ripsaw_rage_region = "Ripsaw Rage"
blazing_bazookas_region = "Blazing Bazukas"
low_g_labyrinth_region = "Low-G Labyrinth"
krevice_kreepers_region = "Krevice Kreepers"
tearaway_toboggan_region = "Tearaway Toboggan"
barrel_drop_bounce_region = "Barrel Drop Bounce"
krack_shot_kroc_region = "Krack-Shot Kroc"
lemguin_lunge_region = "Lemguin Lunge"
buzzer_barrage_region = "Buzzer Barrage"
kong_fused_cliffs_region = "Kong-Fused Cliffs"
floodlit_fish_region = "Floodlit Fish"
pothole_panic_region = "Pothole Panic"
ropey_rumpus_region = "Ropey Rumpus"
konveyor_rope_clash_region = "Konveyor Rope Klash"
creepy_caverns_region = "Creepy Caverns"
lightning_lookout_region = "Lightning Lookout"
koindozer_klamber_region = "Koindozer Klamber"
poisonous_pipeline_region = "Poisonous Pipeline"
stampede_sprint_region = "Stampede Sprint"
criss_cross_cliffs_region = "Criss Kross Cliffs"
tyrant_twin_tussle_region = "Tyrant Twin Tussle"
swoopy_salvo_region = "Swoopy Salvo"
rocket_rush_region = "Rocket Rush"

View File

@@ -1,132 +0,0 @@
import typing
from Options import Choice, Range, Option, Toggle, DeathLink, DefaultOnToggle, OptionList
class Goal(Choice):
"""
Determines the goal of the seed
Knautilus: Reach the Knautilus and defeat Baron K. Roolenstein
Banana Bird Hunt: Find a certain number of Banana Birds and rescue their mother
"""
display_name = "Goal"
option_knautilus = 0
option_banana_bird_hunt = 1
default = 0
class IncludeTradeSequence(Toggle):
"""
Allows logic to place items at the various steps of the trade sequence
"""
display_name = "Include Trade Sequence"
class DKCoinsForGyrocopter(Range):
"""
How many DK Coins are needed to unlock the Gyrocopter
Note: Achieving this number before unlocking the Turbo Ski will cause the game to grant you a
one-time upgrade to the next non-unlocked boat, until you return to Funky. Logic does not assume
that you will use this.
"""
display_name = "DK Coins for Gyrocopter"
range_start = 10
range_end = 41
default = 30
class KrematoaBonusCoinCost(Range):
"""
How many Bonus Coins are needed to unlock each level in Krematoa
"""
display_name = "Krematoa Bonus Coins Cost"
range_start = 1
range_end = 17
default = 15
class PercentageOfExtraBonusCoins(Range):
"""
What Percentage of unneeded Bonus Coins are included in the item pool
"""
display_name = "Percentage of Extra Bonus Coins"
range_start = 0
range_end = 100
default = 100
class NumberOfBananaBirds(Range):
"""
How many Banana Birds are put into the item pool
"""
display_name = "Number of Banana Birds"
range_start = 5
range_end = 15
default = 15
class PercentageOfBananaBirds(Range):
"""
What Percentage of Banana Birds in the item pool are required for Banana Bird Hunt
"""
display_name = "Percentage of Banana Birds"
range_start = 20
range_end = 100
default = 100
class LevelShuffle(Toggle):
"""
Whether levels are shuffled
"""
display_name = "Level Shuffle"
class MusicShuffle(Toggle):
"""
Whether music is shuffled
"""
display_name = "Music Shuffle"
class KongPaletteSwap(Choice):
"""
Which Palette to use for the Kongs
"""
display_name = "Kong Palette Swap"
option_default = 0
option_purple = 1
option_spooky = 2
option_dark = 3
option_chocolate = 4
option_shadow = 5
option_red_gold = 6
option_gbc = 7
option_halloween = 8
default = 0
class StartingLifeCount(Range):
"""
How many extra lives to start the game with
"""
display_name = "Starting Life Count"
range_start = 1
range_end = 99
default = 5
dkc3_options: typing.Dict[str, type(Option)] = {
#"death_link": DeathLink, # Disabled
"goal": Goal,
#"include_trade_sequence": IncludeTradeSequence, # Disabled
"dk_coins_for_gyrocopter": DKCoinsForGyrocopter,
"krematoa_bonus_coin_cost": KrematoaBonusCoinCost,
"percentage_of_extra_bonus_coins": PercentageOfExtraBonusCoins,
"number_of_banana_birds": NumberOfBananaBirds,
"percentage_of_banana_birds": PercentageOfBananaBirds,
"level_shuffle": LevelShuffle,
"music_shuffle": MusicShuffle,
"kong_palette_swap": KongPaletteSwap,
"starting_life_count": StartingLifeCount,
}

View File

@@ -1,883 +0,0 @@
import typing
from BaseClasses import MultiWorld, Region, Entrance
from .Items import DKC3Item
from .Locations import DKC3Location
from .Names import LocationName, ItemName
def create_regions(world, player: int, active_locations):
menu_region = create_region(world, player, active_locations, 'Menu', None, None)
overworld_1_region_locations = {}
if world.goal[player] != "knautilus":
overworld_1_region_locations.update({LocationName.banana_bird_mother: []})
overworld_1_region = create_region(world, player, active_locations, LocationName.overworld_1_region,
overworld_1_region_locations, None)
overworld_2_region_locations = {}
overworld_2_region = create_region(world, player, active_locations, LocationName.overworld_2_region,
overworld_2_region_locations, None)
overworld_3_region_locations = {}
overworld_3_region = create_region(world, player, active_locations, LocationName.overworld_3_region,
overworld_3_region_locations, None)
overworld_4_region_locations = {}
overworld_4_region = create_region(world, player, active_locations, LocationName.overworld_4_region,
overworld_4_region_locations, None)
lake_orangatanga_region = create_region(world, player, active_locations, LocationName.lake_orangatanga_region, None, None)
kremwood_forest_region = create_region(world, player, active_locations, LocationName.kremwood_forest_region, None, None)
cotton_top_cove_region = create_region(world, player, active_locations, LocationName.cotton_top_cove_region, None, None)
mekanos_region = create_region(world, player, active_locations, LocationName.mekanos_region, None, None)
k3_region = create_region(world, player, active_locations, LocationName.k3_region, None, None)
razor_ridge_region = create_region(world, player, active_locations, LocationName.razor_ridge_region, None, None)
kaos_kore_region = create_region(world, player, active_locations, LocationName.kaos_kore_region, None, None)
krematoa_region = create_region(world, player, active_locations, LocationName.krematoa_region, None, None)
lakeside_limbo_region_locations = {
LocationName.lakeside_limbo_flag : [0x657, 1],
LocationName.lakeside_limbo_bonus_1 : [0x657, 2],
LocationName.lakeside_limbo_bonus_2 : [0x657, 3],
LocationName.lakeside_limbo_dk : [0x657, 5],
}
lakeside_limbo_region = create_region(world, player, active_locations, LocationName.lakeside_limbo_region,
lakeside_limbo_region_locations, None)
doorstop_dash_region_locations = {
LocationName.doorstop_dash_flag : [0x65A, 1],
LocationName.doorstop_dash_bonus_1 : [0x65A, 2],
LocationName.doorstop_dash_bonus_2 : [0x65A, 3],
LocationName.doorstop_dash_dk : [0x65A, 5],
}
doorstop_dash_region = create_region(world, player, active_locations, LocationName.doorstop_dash_region,
doorstop_dash_region_locations, None)
tidal_trouble_region_locations = {
LocationName.tidal_trouble_flag : [0x659, 1],
LocationName.tidal_trouble_bonus_1 : [0x659, 2],
LocationName.tidal_trouble_bonus_2 : [0x659, 3],
LocationName.tidal_trouble_dk : [0x659, 5],
}
tidal_trouble_region = create_region(world, player, active_locations, LocationName.tidal_trouble_region,
tidal_trouble_region_locations, None)
skiddas_row_region_locations = {
LocationName.skiddas_row_flag : [0x65D, 1],
LocationName.skiddas_row_bonus_1 : [0x65D, 2],
LocationName.skiddas_row_bonus_2 : [0x65D, 3],
LocationName.skiddas_row_dk : [0x65D, 5],
}
skiddas_row_region = create_region(world, player, active_locations, LocationName.skiddas_row_region,
skiddas_row_region_locations, None)
murky_mill_region_locations = {
LocationName.murky_mill_flag : [0x65C, 1],
LocationName.murky_mill_bonus_1 : [0x65C, 2],
LocationName.murky_mill_bonus_2 : [0x65C, 3],
LocationName.murky_mill_dk : [0x65C, 5],
}
murky_mill_region = create_region(world, player, active_locations, LocationName.murky_mill_region,
murky_mill_region_locations, None)
barrel_shield_bust_up_region_locations = {
LocationName.barrel_shield_bust_up_flag : [0x662, 1],
LocationName.barrel_shield_bust_up_bonus_1 : [0x662, 2],
LocationName.barrel_shield_bust_up_bonus_2 : [0x662, 3],
LocationName.barrel_shield_bust_up_dk : [0x662, 5],
}
barrel_shield_bust_up_region = create_region(world, player, active_locations, LocationName.barrel_shield_bust_up_region,
barrel_shield_bust_up_region_locations, None)
riverside_race_region_locations = {
LocationName.riverside_race_flag : [0x664, 1],
LocationName.riverside_race_bonus_1 : [0x664, 2],
LocationName.riverside_race_bonus_2 : [0x664, 3],
LocationName.riverside_race_dk : [0x664, 5],
}
riverside_race_region = create_region(world, player, active_locations, LocationName.riverside_race_region,
riverside_race_region_locations, None)
squeals_on_wheels_region_locations = {
LocationName.squeals_on_wheels_flag : [0x65B, 1],
LocationName.squeals_on_wheels_bonus_1 : [0x65B, 2],
LocationName.squeals_on_wheels_bonus_2 : [0x65B, 3],
LocationName.squeals_on_wheels_dk : [0x65B, 5],
}
squeals_on_wheels_region = create_region(world, player, active_locations, LocationName.squeals_on_wheels_region,
squeals_on_wheels_region_locations, None)
springin_spiders_region_locations = {
LocationName.springin_spiders_flag : [0x661, 1],
LocationName.springin_spiders_bonus_1 : [0x661, 2],
LocationName.springin_spiders_bonus_2 : [0x661, 3],
LocationName.springin_spiders_dk : [0x661, 5],
}
springin_spiders_region = create_region(world, player, active_locations, LocationName.springin_spiders_region,
springin_spiders_region_locations, None)
bobbing_barrel_brawl_region_locations = {
LocationName.bobbing_barrel_brawl_flag : [0x666, 1],
LocationName.bobbing_barrel_brawl_bonus_1 : [0x666, 2],
LocationName.bobbing_barrel_brawl_bonus_2 : [0x666, 3],
LocationName.bobbing_barrel_brawl_dk : [0x666, 5],
}
bobbing_barrel_brawl_region = create_region(world, player, active_locations, LocationName.bobbing_barrel_brawl_region,
bobbing_barrel_brawl_region_locations, None)
bazzas_blockade_region_locations = {
LocationName.bazzas_blockade_flag : [0x667, 1],
LocationName.bazzas_blockade_bonus_1 : [0x667, 2],
LocationName.bazzas_blockade_bonus_2 : [0x667, 3],
LocationName.bazzas_blockade_dk : [0x667, 5],
}
bazzas_blockade_region = create_region(world, player, active_locations, LocationName.bazzas_blockade_region,
bazzas_blockade_region_locations, None)
rocket_barrel_ride_region_locations = {
LocationName.rocket_barrel_ride_flag : [0x66A, 1],
LocationName.rocket_barrel_ride_bonus_1 : [0x66A, 2],
LocationName.rocket_barrel_ride_bonus_2 : [0x66A, 3],
LocationName.rocket_barrel_ride_dk : [0x66A, 5],
}
rocket_barrel_ride_region = create_region(world, player, active_locations, LocationName.rocket_barrel_ride_region,
rocket_barrel_ride_region_locations, None)
kreeping_klasps_region_locations = {
LocationName.kreeping_klasps_flag : [0x658, 1],
LocationName.kreeping_klasps_bonus_1 : [0x658, 2],
LocationName.kreeping_klasps_bonus_2 : [0x658, 3],
LocationName.kreeping_klasps_dk : [0x658, 5],
}
kreeping_klasps_region = create_region(world, player, active_locations, LocationName.kreeping_klasps_region,
kreeping_klasps_region_locations, None)
tracker_barrel_trek_region_locations = {
LocationName.tracker_barrel_trek_flag : [0x66B, 1],
LocationName.tracker_barrel_trek_bonus_1 : [0x66B, 2],
LocationName.tracker_barrel_trek_bonus_2 : [0x66B, 3],
LocationName.tracker_barrel_trek_dk : [0x66B, 5],
}
tracker_barrel_trek_region = create_region(world, player, active_locations, LocationName.tracker_barrel_trek_region,
tracker_barrel_trek_region_locations, None)
fish_food_frenzy_region_locations = {
LocationName.fish_food_frenzy_flag : [0x668, 1],
LocationName.fish_food_frenzy_bonus_1 : [0x668, 2],
LocationName.fish_food_frenzy_bonus_2 : [0x668, 3],
LocationName.fish_food_frenzy_dk : [0x668, 5],
}
fish_food_frenzy_region = create_region(world, player, active_locations, LocationName.fish_food_frenzy_region,
fish_food_frenzy_region_locations, None)
fire_ball_frenzy_region_locations = {
LocationName.fire_ball_frenzy_flag : [0x66D, 1],
LocationName.fire_ball_frenzy_bonus_1 : [0x66D, 2],
LocationName.fire_ball_frenzy_bonus_2 : [0x66D, 3],
LocationName.fire_ball_frenzy_dk : [0x66D, 5],
}
fire_ball_frenzy_region = create_region(world, player, active_locations, LocationName.fire_ball_frenzy_region,
fire_ball_frenzy_region_locations, None)
demolition_drain_pipe_region_locations = {
LocationName.demolition_drain_pipe_flag : [0x672, 1],
LocationName.demolition_drain_pipe_bonus_1 : [0x672, 2],
LocationName.demolition_drain_pipe_bonus_2 : [0x672, 3],
LocationName.demolition_drain_pipe_dk : [0x672, 5],
}
demolition_drain_pipe_region = create_region(world, player, active_locations, LocationName.demolition_drain_pipe_region,
demolition_drain_pipe_region_locations, None)
ripsaw_rage_region_locations = {
LocationName.ripsaw_rage_flag : [0x660, 1],
LocationName.ripsaw_rage_bonus_1 : [0x660, 2],
LocationName.ripsaw_rage_bonus_2 : [0x660, 3],
LocationName.ripsaw_rage_dk : [0x660, 5],
}
ripsaw_rage_region = create_region(world, player, active_locations, LocationName.ripsaw_rage_region,
ripsaw_rage_region_locations, None)
blazing_bazookas_region_locations = {
LocationName.blazing_bazookas_flag : [0x66E, 1],
LocationName.blazing_bazookas_bonus_1 : [0x66E, 2],
LocationName.blazing_bazookas_bonus_2 : [0x66E, 3],
LocationName.blazing_bazookas_dk : [0x66E, 5],
}
blazing_bazookas_region = create_region(world, player, active_locations, LocationName.blazing_bazookas_region,
blazing_bazookas_region_locations, None)
low_g_labyrinth_region_locations = {
LocationName.low_g_labyrinth_flag : [0x670, 1],
LocationName.low_g_labyrinth_bonus_1 : [0x670, 2],
LocationName.low_g_labyrinth_bonus_2 : [0x670, 3],
LocationName.low_g_labyrinth_dk : [0x670, 5],
}
low_g_labyrinth_region = create_region(world, player, active_locations, LocationName.low_g_labyrinth_region,
low_g_labyrinth_region_locations, None)
krevice_kreepers_region_locations = {
LocationName.krevice_kreepers_flag : [0x673, 1],
LocationName.krevice_kreepers_bonus_1 : [0x673, 2],
LocationName.krevice_kreepers_bonus_2 : [0x673, 3],
LocationName.krevice_kreepers_dk : [0x673, 5],
}
krevice_kreepers_region = create_region(world, player, active_locations, LocationName.krevice_kreepers_region,
krevice_kreepers_region_locations, None)
tearaway_toboggan_region_locations = {
LocationName.tearaway_toboggan_flag : [0x65F, 1],
LocationName.tearaway_toboggan_bonus_1 : [0x65F, 2],
LocationName.tearaway_toboggan_bonus_2 : [0x65F, 3],
LocationName.tearaway_toboggan_dk : [0x65F, 5],
}
tearaway_toboggan_region = create_region(world, player, active_locations, LocationName.tearaway_toboggan_region,
tearaway_toboggan_region_locations, None)
barrel_drop_bounce_region_locations = {
LocationName.barrel_drop_bounce_flag : [0x66C, 1],
LocationName.barrel_drop_bounce_bonus_1 : [0x66C, 2],
LocationName.barrel_drop_bounce_bonus_2 : [0x66C, 3],
LocationName.barrel_drop_bounce_dk : [0x66C, 5],
}
barrel_drop_bounce_region = create_region(world, player, active_locations, LocationName.barrel_drop_bounce_region,
barrel_drop_bounce_region_locations, None)
krack_shot_kroc_region_locations = {
LocationName.krack_shot_kroc_flag : [0x66F, 1],
LocationName.krack_shot_kroc_bonus_1 : [0x66F, 2],
LocationName.krack_shot_kroc_bonus_2 : [0x66F, 3],
LocationName.krack_shot_kroc_dk : [0x66F, 5],
}
krack_shot_kroc_region = create_region(world, player, active_locations, LocationName.krack_shot_kroc_region,
krack_shot_kroc_region_locations, None)
lemguin_lunge_region_locations = {
LocationName.lemguin_lunge_flag : [0x65E, 1],
LocationName.lemguin_lunge_bonus_1 : [0x65E, 2],
LocationName.lemguin_lunge_bonus_2 : [0x65E, 3],
LocationName.lemguin_lunge_dk : [0x65E, 5],
}
lemguin_lunge_region = create_region(world, player, active_locations, LocationName.lemguin_lunge_region,
lemguin_lunge_region_locations, None)
buzzer_barrage_region_locations = {
LocationName.buzzer_barrage_flag : [0x676, 1],
LocationName.buzzer_barrage_bonus_1 : [0x676, 2],
LocationName.buzzer_barrage_bonus_2 : [0x676, 3],
LocationName.buzzer_barrage_dk : [0x676, 5],
}
buzzer_barrage_region = create_region(world, player, active_locations, LocationName.buzzer_barrage_region,
buzzer_barrage_region_locations, None)
kong_fused_cliffs_region_locations = {
LocationName.kong_fused_cliffs_flag : [0x674, 1],
LocationName.kong_fused_cliffs_bonus_1 : [0x674, 2],
LocationName.kong_fused_cliffs_bonus_2 : [0x674, 3],
LocationName.kong_fused_cliffs_dk : [0x674, 5],
}
kong_fused_cliffs_region = create_region(world, player, active_locations, LocationName.kong_fused_cliffs_region,
kong_fused_cliffs_region_locations, None)
floodlit_fish_region_locations = {
LocationName.floodlit_fish_flag : [0x669, 1],
LocationName.floodlit_fish_bonus_1 : [0x669, 2],
LocationName.floodlit_fish_bonus_2 : [0x669, 3],
LocationName.floodlit_fish_dk : [0x669, 5],
}
floodlit_fish_region = create_region(world, player, active_locations, LocationName.floodlit_fish_region,
floodlit_fish_region_locations, None)
pothole_panic_region_locations = {
LocationName.pothole_panic_flag : [0x677, 1],
LocationName.pothole_panic_bonus_1 : [0x677, 2],
LocationName.pothole_panic_bonus_2 : [0x677, 3],
LocationName.pothole_panic_dk : [0x677, 5],
}
pothole_panic_region = create_region(world, player, active_locations, LocationName.pothole_panic_region,
pothole_panic_region_locations, None)
ropey_rumpus_region_locations = {
LocationName.ropey_rumpus_flag : [0x675, 1],
LocationName.ropey_rumpus_bonus_1 : [0x675, 2],
LocationName.ropey_rumpus_bonus_2 : [0x675, 3],
LocationName.ropey_rumpus_dk : [0x675, 5],
}
ropey_rumpus_region = create_region(world, player, active_locations, LocationName.ropey_rumpus_region,
ropey_rumpus_region_locations, None)
konveyor_rope_clash_region_locations = {
LocationName.konveyor_rope_clash_flag : [0x657, 1],
LocationName.konveyor_rope_clash_bonus_1 : [0x657, 2],
LocationName.konveyor_rope_clash_bonus_2 : [0x657, 3],
LocationName.konveyor_rope_clash_dk : [0x657, 5],
}
konveyor_rope_clash_region = create_region(world, player, active_locations, LocationName.konveyor_rope_clash_region,
konveyor_rope_clash_region_locations, None)
creepy_caverns_region_locations = {
LocationName.creepy_caverns_flag : [0x678, 1],
LocationName.creepy_caverns_bonus_1 : [0x678, 2],
LocationName.creepy_caverns_bonus_2 : [0x678, 3],
LocationName.creepy_caverns_dk : [0x678, 5],
}
creepy_caverns_region = create_region(world, player, active_locations, LocationName.creepy_caverns_region,
creepy_caverns_region_locations, None)
lightning_lookout_region_locations = {
LocationName.lightning_lookout_flag : [0x665, 1],
LocationName.lightning_lookout_bonus_1 : [0x665, 2],
LocationName.lightning_lookout_bonus_2 : [0x665, 3],
LocationName.lightning_lookout_dk : [0x665, 5],
}
lightning_lookout_region = create_region(world, player, active_locations, LocationName.lightning_lookout_region,
lightning_lookout_region_locations, None)
koindozer_klamber_region_locations = {
LocationName.koindozer_klamber_flag : [0x679, 1],
LocationName.koindozer_klamber_bonus_1 : [0x679, 2],
LocationName.koindozer_klamber_bonus_2 : [0x679, 3],
LocationName.koindozer_klamber_dk : [0x679, 5],
}
koindozer_klamber_region = create_region(world, player, active_locations, LocationName.koindozer_klamber_region,
koindozer_klamber_region_locations, None)
poisonous_pipeline_region_locations = {
LocationName.poisonous_pipeline_flag : [0x671, 1],
LocationName.poisonous_pipeline_bonus_1 : [0x671, 2],
LocationName.poisonous_pipeline_bonus_2 : [0x671, 3],
LocationName.poisonous_pipeline_dk : [0x671, 5],
}
poisonous_pipeline_region = create_region(world, player, active_locations, LocationName.poisonous_pipeline_region,
poisonous_pipeline_region_locations, None)
stampede_sprint_region_locations = {
LocationName.stampede_sprint_flag : [0x67B, 1],
LocationName.stampede_sprint_bonus_1 : [0x67B, 2],
LocationName.stampede_sprint_bonus_2 : [0x67B, 3],
LocationName.stampede_sprint_bonus_3 : [0x67B, 4],
LocationName.stampede_sprint_dk : [0x67B, 5],
}
stampede_sprint_region = create_region(world, player, active_locations, LocationName.stampede_sprint_region,
stampede_sprint_region_locations, None)
criss_cross_cliffs_region_locations = {
LocationName.criss_cross_cliffs_flag : [0x67C, 1],
LocationName.criss_cross_cliffs_bonus_1 : [0x67C, 2],
LocationName.criss_cross_cliffs_bonus_2 : [0x67C, 3],
LocationName.criss_cross_cliffs_dk : [0x67C, 5],
}
criss_cross_cliffs_region = create_region(world, player, active_locations, LocationName.criss_cross_cliffs_region,
criss_cross_cliffs_region_locations, None)
tyrant_twin_tussle_region_locations = {
LocationName.tyrant_twin_tussle_flag : [0x67D, 1],
LocationName.tyrant_twin_tussle_bonus_1 : [0x67D, 2],
LocationName.tyrant_twin_tussle_bonus_2 : [0x67D, 3],
LocationName.tyrant_twin_tussle_bonus_3 : [0x67D, 4],
LocationName.tyrant_twin_tussle_dk : [0x67D, 5],
}
tyrant_twin_tussle_region = create_region(world, player, active_locations, LocationName.tyrant_twin_tussle_region,
tyrant_twin_tussle_region_locations, None)
swoopy_salvo_region_locations = {
LocationName.swoopy_salvo_flag : [0x663, 1],
LocationName.swoopy_salvo_bonus_1 : [0x663, 2],
LocationName.swoopy_salvo_bonus_2 : [0x663, 3],
LocationName.swoopy_salvo_bonus_3 : [0x663, 4],
LocationName.swoopy_salvo_dk : [0x663, 5],
}
swoopy_salvo_region = create_region(world, player, active_locations, LocationName.swoopy_salvo_region,
swoopy_salvo_region_locations, None)
rocket_rush_region_locations = {
LocationName.rocket_rush_flag : [0x67E, 1],
LocationName.rocket_rush_dk : [0x67E, 5],
}
rocket_rush_region = create_region(world, player, active_locations, LocationName.rocket_rush_region,
rocket_rush_region_locations, None)
belchas_barn_region_locations = {
LocationName.belchas_barn: [0x64F, 1],
}
belchas_barn_region = create_region(world, player, active_locations, LocationName.belchas_barn_region,
belchas_barn_region_locations, None)
arichs_ambush_region_locations = {
LocationName.arichs_ambush: [0x650, 1],
}
arichs_ambush_region = create_region(world, player, active_locations, LocationName.arichs_ambush_region,
arichs_ambush_region_locations, None)
squirts_showdown_region_locations = {
LocationName.squirts_showdown: [0x651, 1],
}
squirts_showdown_region = create_region(world, player, active_locations, LocationName.squirts_showdown_region,
squirts_showdown_region_locations, None)
kaos_karnage_region_locations = {
LocationName.kaos_karnage: [0x652, 1],
}
kaos_karnage_region = create_region(world, player, active_locations, LocationName.kaos_karnage_region,
kaos_karnage_region_locations, None)
bleaks_house_region_locations = {
LocationName.bleaks_house: [0x653, 1],
}
bleaks_house_region = create_region(world, player, active_locations, LocationName.bleaks_house_region,
bleaks_house_region_locations, None)
barboss_barrier_region_locations = {
LocationName.barboss_barrier: [0x654, 1],
}
barboss_barrier_region = create_region(world, player, active_locations, LocationName.barboss_barrier_region,
barboss_barrier_region_locations, None)
kastle_kaos_region_locations = {
LocationName.kastle_kaos: [0x655, 1],
}
kastle_kaos_region = create_region(world, player, active_locations, LocationName.kastle_kaos_region,
kastle_kaos_region_locations, None)
knautilus_region_locations = {
LocationName.knautilus: [0x656, 1],
}
knautilus_region = create_region(world, player, active_locations, LocationName.knautilus_region,
knautilus_region_locations, None)
belchas_burrow_region_locations = {
LocationName.belchas_burrow: [0x647, 1],
}
belchas_burrow_region = create_region(world, player, active_locations, LocationName.belchas_burrow_region,
belchas_burrow_region_locations, None)
kong_cave_region_locations = {
LocationName.kong_cave: [0x645, 1],
}
kong_cave_region = create_region(world, player, active_locations, LocationName.kong_cave_region,
kong_cave_region_locations, None)
undercover_cove_region_locations = {
LocationName.undercover_cove: [0x644, 1],
}
undercover_cove_region = create_region(world, player, active_locations, LocationName.undercover_cove_region,
undercover_cove_region_locations, None)
ks_cache_region_locations = {
LocationName.ks_cache: [0x642, 1],
}
ks_cache_region = create_region(world, player, active_locations, LocationName.ks_cache_region,
ks_cache_region_locations, None)
hill_top_hoard_region_locations = {
LocationName.hill_top_hoard: [0x643, 1],
}
hill_top_hoard_region = create_region(world, player, active_locations, LocationName.hill_top_hoard_region,
hill_top_hoard_region_locations, None)
bounty_beach_region_locations = {
LocationName.bounty_beach: [0x646, 1],
}
bounty_beach_region = create_region(world, player, active_locations, LocationName.bounty_beach_region,
bounty_beach_region_locations, None)
smugglers_cove_region_locations = {
LocationName.smugglers_cove: [0x648, 1],
}
smugglers_cove_region = create_region(world, player, active_locations, LocationName.smugglers_cove_region,
smugglers_cove_region_locations, None)
arichs_hoard_region_locations = {
LocationName.arichs_hoard: [0x649, 1],
}
arichs_hoard_region = create_region(world, player, active_locations, LocationName.arichs_hoard_region,
arichs_hoard_region_locations, None)
bounty_bay_region_locations = {
LocationName.bounty_bay: [0x64A, 1],
}
bounty_bay_region = create_region(world, player, active_locations, LocationName.bounty_bay_region,
bounty_bay_region_locations, None)
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 = create_region(world, player, active_locations, LocationName.sky_high_secret_region,
sky_high_secret_region_locations, None)
glacial_grotto_region_locations = {
LocationName.glacial_grotto: [0x64C, 1],
}
glacial_grotto_region = create_region(world, player, active_locations, LocationName.glacial_grotto_region,
glacial_grotto_region_locations, None)
cifftop_cache_region_locations = {}
if False:#world.include_trade_sequence[player]:
cifftop_cache_region_locations.update({
LocationName.cifftop_cache: [0x64D, 1],
})
cifftop_cache_region = create_region(world, player, active_locations, LocationName.cifftop_cache_region,
cifftop_cache_region_locations, None)
sewer_stockpile_region_locations = {
LocationName.sewer_stockpile: [0x64E, 1],
}
sewer_stockpile_region = create_region(world, player, active_locations, LocationName.sewer_stockpile_region,
sewer_stockpile_region_locations, None)
# Set up the regions correctly.
world.regions += [
menu_region,
overworld_1_region,
overworld_2_region,
overworld_3_region,
overworld_4_region,
lake_orangatanga_region,
kremwood_forest_region,
cotton_top_cove_region,
mekanos_region,
k3_region,
razor_ridge_region,
kaos_kore_region,
krematoa_region,
lakeside_limbo_region,
doorstop_dash_region,
tidal_trouble_region,
skiddas_row_region,
murky_mill_region,
barrel_shield_bust_up_region,
riverside_race_region,
squeals_on_wheels_region,
springin_spiders_region,
bobbing_barrel_brawl_region,
bazzas_blockade_region,
rocket_barrel_ride_region,
kreeping_klasps_region,
tracker_barrel_trek_region,
fish_food_frenzy_region,
fire_ball_frenzy_region,
demolition_drain_pipe_region,
ripsaw_rage_region,
blazing_bazookas_region,
low_g_labyrinth_region,
krevice_kreepers_region,
tearaway_toboggan_region,
barrel_drop_bounce_region,
krack_shot_kroc_region,
lemguin_lunge_region,
buzzer_barrage_region,
kong_fused_cliffs_region,
floodlit_fish_region,
pothole_panic_region,
ropey_rumpus_region,
konveyor_rope_clash_region,
creepy_caverns_region,
lightning_lookout_region,
koindozer_klamber_region,
poisonous_pipeline_region,
stampede_sprint_region,
criss_cross_cliffs_region,
tyrant_twin_tussle_region,
swoopy_salvo_region,
rocket_rush_region,
belchas_barn_region,
arichs_ambush_region,
squirts_showdown_region,
kaos_karnage_region,
bleaks_house_region,
barboss_barrier_region,
kastle_kaos_region,
knautilus_region,
belchas_burrow_region,
kong_cave_region,
undercover_cove_region,
ks_cache_region,
hill_top_hoard_region,
bounty_beach_region,
smugglers_cove_region,
arichs_hoard_region,
bounty_bay_region,
sky_high_secret_region,
glacial_grotto_region,
cifftop_cache_region,
sewer_stockpile_region,
]
bazaar_region_locations = {}
bramble_region_locations = {}
flower_spot_region_locations = {}
barter_region_locations = {}
barnacle_region_locations = {}
blue_region_locations = {}
blizzard_region_locations = {}
if False:#world.include_trade_sequence[player]:
bazaar_region_locations.update({
LocationName.bazaars_general_store_1: [0x615, 2, True],
LocationName.bazaars_general_store_2: [0x615, 3, True],
})
bramble_region_locations.update({
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],
})
barnacle_region_locations.update({
LocationName.barnacles_island: [0x61D, 2],
})
blue_region_locations.update({
LocationName.blues_beach_hut: [0x621, 4],
})
blizzard_region_locations.update({
LocationName.blizzards_basecamp: [0x625, 4, True],
})
bazaar_region = create_region(world, player, active_locations, LocationName.bazaar_region,
bazaar_region_locations, None)
bramble_region = create_region(world, player, active_locations, LocationName.bramble_region,
bramble_region_locations, None)
flower_spot_region = create_region(world, player, active_locations, LocationName.flower_spot_region,
flower_spot_region_locations, None)
barter_region = create_region(world, player, active_locations, LocationName.barter_region,
barter_region_locations, None)
barnacle_region = create_region(world, player, active_locations, LocationName.barnacle_region,
barnacle_region_locations, None)
blue_region = create_region(world, player, active_locations, LocationName.blue_region,
blue_region_locations, None)
blizzard_region = create_region(world, player, active_locations, LocationName.blizzard_region,
blizzard_region_locations, None)
world.regions += [
bazaar_region,
bramble_region,
flower_spot_region,
barter_region,
barnacle_region,
blue_region,
blizzard_region,
]
def connect_regions(world, player, level_list):
names: typing.Dict[str, int] = {}
# Overworld
connect(world, player, names, 'Menu', LocationName.overworld_1_region)
connect(world, player, names, LocationName.overworld_1_region, LocationName.overworld_2_region,
lambda state: (state.has(ItemName.progressive_boat, player, 1)))
connect(world, player, names, LocationName.overworld_2_region, LocationName.overworld_3_region,
lambda state: (state.has(ItemName.progressive_boat, player, 3)))
connect(world, player, names, LocationName.overworld_1_region, LocationName.overworld_4_region,
lambda state: (state.has(ItemName.dk_coin, player, world.dk_coins_for_gyrocopter[player].value) and
state.has(ItemName.progressive_boat, player, 3)))
# World Connections
connect(world, player, names, LocationName.overworld_1_region, LocationName.lake_orangatanga_region)
connect(world, player, names, LocationName.overworld_1_region, LocationName.kremwood_forest_region)
connect(world, player, names, LocationName.overworld_1_region, LocationName.bounty_beach_region)
connect(world, player, names, LocationName.overworld_1_region, LocationName.bazaar_region)
connect(world, player, names, LocationName.overworld_2_region, LocationName.cotton_top_cove_region)
connect(world, player, names, LocationName.overworld_2_region, LocationName.mekanos_region)
connect(world, player, names, LocationName.overworld_2_region, LocationName.kong_cave_region)
connect(world, player, names, LocationName.overworld_2_region, LocationName.bramble_region)
connect(world, player, names, LocationName.overworld_3_region, LocationName.k3_region)
connect(world, player, names, LocationName.overworld_3_region, LocationName.razor_ridge_region)
connect(world, player, names, LocationName.overworld_3_region, LocationName.kaos_kore_region)
connect(world, player, names, LocationName.overworld_3_region, LocationName.krematoa_region)
connect(world, player, names, LocationName.overworld_3_region, LocationName.undercover_cove_region)
connect(world, player, names, LocationName.overworld_3_region, LocationName.flower_spot_region)
connect(world, player, names, LocationName.overworld_3_region, LocationName.barter_region)
connect(world, player, names, LocationName.overworld_4_region, LocationName.belchas_burrow_region)
connect(world, player, names, LocationName.overworld_4_region, LocationName.ks_cache_region)
connect(world, player, names, LocationName.overworld_4_region, LocationName.hill_top_hoard_region)
# Lake Orangatanga Connections
lake_orangatanga_levels = [
level_list[0],
level_list[1],
level_list[2],
level_list[3],
level_list[4],
LocationName.belchas_barn_region,
LocationName.barnacle_region,
LocationName.smugglers_cove_region,
]
for i in range(0, len(lake_orangatanga_levels)):
connect(world, player, names, LocationName.lake_orangatanga_region, lake_orangatanga_levels[i])
# Kremwood Forest Connections
kremwood_forest_levels = [
level_list[5],
level_list[6],
level_list[7],
level_list[8],
level_list[9],
LocationName.arichs_ambush_region,
LocationName.arichs_hoard_region,
]
for i in range(0, len(kremwood_forest_levels) - 1):
connect(world, player, names, LocationName.kremwood_forest_region, kremwood_forest_levels[i])
connect(world, player, names, LocationName.kremwood_forest_region, kremwood_forest_levels[-1],
lambda state: (state.can_reach(LocationName.riverside_race_flag, "Location", player)))
# Cotton-Top Cove Connections
cotton_top_cove_levels = [
LocationName.blue_region,
level_list[10],
level_list[11],
level_list[12],
level_list[13],
level_list[14],
LocationName.squirts_showdown_region,
LocationName.bounty_bay_region,
]
for i in range(0, len(cotton_top_cove_levels)):
connect(world, player, names, LocationName.cotton_top_cove_region, cotton_top_cove_levels[i])
# Mekanos Connections
mekanos_levels = [
level_list[15],
level_list[16],
level_list[17],
level_list[18],
level_list[19],
LocationName.kaos_karnage_region,
]
for i in range(0, len(mekanos_levels)):
connect(world, player, names, LocationName.mekanos_region, mekanos_levels[i])
if False:#world.include_trade_sequence[player]:
connect(world, player, names, LocationName.mekanos_region, LocationName.sky_high_secret_region,
lambda state: (state.has(ItemName.bowling_ball, player, 1)))
else:
connect(world, player, names, LocationName.mekanos_region, LocationName.sky_high_secret_region,
lambda state: (state.can_reach(LocationName.bleaks_house, "Location", player)))
# K3 Connections
k3_levels = [
level_list[20],
level_list[21],
level_list[22],
level_list[23],
level_list[24],
LocationName.bleaks_house_region,
LocationName.blizzard_region,
LocationName.glacial_grotto_region,
]
for i in range(0, len(k3_levels)):
connect(world, player, names, LocationName.k3_region, k3_levels[i])
# Razor Ridge Connections
razor_ridge_levels = [
level_list[25],
level_list[26],
level_list[27],
level_list[28],
level_list[29],
LocationName.barboss_barrier_region,
]
for i in range(0, len(razor_ridge_levels)):
connect(world, player, names, LocationName.razor_ridge_region, razor_ridge_levels[i])
if False:#world.include_trade_sequence[player]:
connect(world, player, names, LocationName.razor_ridge_region, LocationName.cifftop_cache_region,
lambda state: (state.has(ItemName.wrench, player, 1)))
else:
connect(world, player, names, LocationName.razor_ridge_region, LocationName.cifftop_cache_region)
# KAOS Kore Connections
kaos_kore_levels = [
level_list[30],
level_list[31],
level_list[32],
level_list[33],
level_list[34],
LocationName.kastle_kaos_region,
LocationName.sewer_stockpile_region,
]
for i in range(0, len(kaos_kore_levels)):
connect(world, player, names, LocationName.kaos_kore_region, kaos_kore_levels[i])
# Krematoa Connections
krematoa_levels = [
level_list[35],
level_list[36],
level_list[37],
level_list[38],
LocationName.rocket_rush_region,
]
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)))
def create_region(world: MultiWorld, player: int, active_locations, name: str, locations=None, exits=None):
# Shamelessly stolen from the ROR2 definition
ret = Region(name, None, name, player)
ret.world = world
if locations:
for locationName, locationData in locations.items():
loc_id = active_locations.get(locationName, 0)
if loc_id:
loc_byte = locationData[0] if (len(locationData) > 0) else 0
loc_bit = locationData[1] if (len(locationData) > 1) else 0
loc_invert = locationData[2] if (len(locationData) > 2) else False
location = DKC3Location(player, locationName, loc_id, ret, loc_byte, loc_bit, loc_invert)
ret.locations.append(location)
if exits:
for exit in exits:
ret.exits.append(Entrance(player, exit, ret))
return ret
def connect(world: MultiWorld, player: int, used_names: typing.Dict[str, int], source: str, target: str,
rule: typing.Optional[typing.Callable] = None):
source_region = world.get_region(source, player)
target_region = world.get_region(target, player)
if target not in used_names:
used_names[target] = 1
name = target
else:
used_names[target] += 1
name = target + (' ' * used_names[target])
connection = Entrance(player, name, source_region)
if rule:
connection.access_rule = rule
source_region.exits.append(connection)
connection.connect(target_region)

View File

@@ -1,562 +0,0 @@
import Utils
from Patch import read_rom, APDeltaPatch
from .Locations import lookup_id_to_name, all_locations
from .Levels import level_list, level_dict
USHASH = '120abf304f0c40fe059f6a192ed4f947'
ROM_PLAYER_LIMIT = 65535
import hashlib
import os
import math
location_rom_data = {
0xDC3000: [0x657, 1], # Lakeside Limbo
0xDC3001: [0x657, 2],
0xDC3002: [0x657, 3],
0xDC3003: [0x657, 5],
0xDC3004: [0x65A, 1], # Doorstop Dash
0xDC3005: [0x65A, 2],
0xDC3006: [0x65A, 3],
0xDC3007: [0x65A, 5],
0xDC3008: [0x659, 1], # Tidal Trouble
0xDC3009: [0x659, 2],
0xDC300A: [0x659, 3],
0xDC300B: [0x659, 5],
0xDC300C: [0x65D, 1], # Skidda's Row
0xDC300D: [0x65D, 2],
0xDC300E: [0x65D, 3],
0xDC300F: [0x65D, 5],
0xDC3010: [0x65C, 1], # Murky Mill
0xDC3011: [0x65C, 2],
0xDC3012: [0x65C, 3],
0xDC3013: [0x65C, 5],
0xDC3014: [0x662, 1], # Barrel Shield Bust-Up
0xDC3015: [0x662, 2],
0xDC3016: [0x662, 3],
0xDC3017: [0x662, 5],
0xDC3018: [0x664, 1], # Riverside Race
0xDC3019: [0x664, 2],
0xDC301A: [0x664, 3],
0xDC301B: [0x664, 5],
0xDC301C: [0x65B, 1], # Squeals on Wheels
0xDC301D: [0x65B, 2],
0xDC301E: [0x65B, 3],
0xDC301F: [0x65B, 5],
0xDC3020: [0x661, 1], # Springin' Spiders
0xDC3021: [0x661, 2],
0xDC3022: [0x661, 3],
0xDC3023: [0x661, 5],
0xDC3024: [0x666, 1], # Bobbing Barrel Brawl
0xDC3025: [0x666, 2],
0xDC3026: [0x666, 3],
0xDC3027: [0x666, 5],
0xDC3028: [0x667, 1], # Bazza's Blockade
0xDC3029: [0x667, 2],
0xDC302A: [0x667, 3],
0xDC302B: [0x667, 5],
0xDC302C: [0x66A, 1], # Rocket Barrel Ride
0xDC302D: [0x66A, 2],
0xDC302E: [0x66A, 3],
0xDC302F: [0x66A, 5],
0xDC3030: [0x658, 1], # Kreeping Klasps
0xDC3031: [0x658, 2],
0xDC3032: [0x658, 3],
0xDC3033: [0x658, 5],
0xDC3034: [0x66B, 1], # Tracker Barrel Trek
0xDC3035: [0x66B, 2],
0xDC3036: [0x66B, 3],
0xDC3037: [0x66B, 5],
0xDC3038: [0x668, 1], # Fish Food Frenzy
0xDC3039: [0x668, 2],
0xDC303A: [0x668, 3],
0xDC303B: [0x668, 5],
0xDC303C: [0x66D, 1], # Fire-ball Frenzy
0xDC303D: [0x66D, 2],
0xDC303E: [0x66D, 3],
0xDC303F: [0x66D, 5],
0xDC3040: [0x672, 1], # Demolition Drainpipe
0xDC3041: [0x672, 2],
0xDC3042: [0x672, 3],
0xDC3043: [0x672, 5],
0xDC3044: [0x660, 1], # Ripsaw Rage
0xDC3045: [0x660, 2],
0xDC3046: [0x660, 3],
0xDC3047: [0x660, 5],
0xDC3048: [0x66E, 1], # Blazing Bazukas
0xDC3049: [0x66E, 2],
0xDC304A: [0x66E, 3],
0xDC304B: [0x66E, 5],
0xDC304C: [0x670, 1], # Low-G Labyrinth
0xDC304D: [0x670, 2],
0xDC304E: [0x670, 3],
0xDC304F: [0x670, 5],
0xDC3050: [0x673, 1], # Krevice Kreepers
0xDC3051: [0x673, 2],
0xDC3052: [0x673, 3],
0xDC3053: [0x673, 5],
0xDC3054: [0x65F, 1], # Tearaway Toboggan
0xDC3055: [0x65F, 2],
0xDC3056: [0x65F, 3],
0xDC3057: [0x65F, 5],
0xDC3058: [0x66C, 1], # Barrel Drop Bounce
0xDC3059: [0x66C, 2],
0xDC305A: [0x66C, 3],
0xDC305B: [0x66C, 5],
0xDC305C: [0x66F, 1], # Krack-Shot Kroc
0xDC305D: [0x66F, 2],
0xDC305E: [0x66F, 3],
0xDC305F: [0x66F, 5],
0xDC3060: [0x65E, 1], # Lemguin Lunge
0xDC3061: [0x65E, 2],
0xDC3062: [0x65E, 3],
0xDC3063: [0x65E, 5],
0xDC3064: [0x676, 1], # Buzzer Barrage
0xDC3065: [0x676, 2],
0xDC3066: [0x676, 3],
0xDC3067: [0x676, 5],
0xDC3068: [0x674, 1], # Kong-Fused Cliffs
0xDC3069: [0x674, 2],
0xDC306A: [0x674, 3],
0xDC306B: [0x674, 5],
0xDC306C: [0x669, 1], # Floodlit Fish
0xDC306D: [0x669, 2],
0xDC306E: [0x669, 3],
0xDC306F: [0x669, 5],
0xDC3070: [0x677, 1], # Pothole Panic
0xDC3071: [0x677, 2],
0xDC3072: [0x677, 3],
0xDC3073: [0x677, 5],
0xDC3074: [0x675, 1], # Ropey Rumpus
0xDC3075: [0x675, 2],
0xDC3076: [0x675, 3],
0xDC3077: [0x675, 5],
0xDC3078: [0x67A, 1], # Konveyor Rope Klash
0xDC3079: [0x67A, 2],
0xDC307A: [0x67A, 3],
0xDC307B: [0x67A, 5],
0xDC307C: [0x678, 1], # Creepy Caverns
0xDC307D: [0x678, 2],
0xDC307E: [0x678, 3],
0xDC307F: [0x678, 5],
0xDC3080: [0x665, 1], # Lightning Lookout
0xDC3081: [0x665, 2],
0xDC3082: [0x665, 3],
0xDC3083: [0x665, 5],
0xDC3084: [0x679, 1], # Koindozer Klamber
0xDC3085: [0x679, 2],
0xDC3086: [0x679, 3],
0xDC3087: [0x679, 5],
0xDC3088: [0x671, 1], # Poisonous Pipeline
0xDC3089: [0x671, 2],
0xDC308A: [0x671, 3],
0xDC308B: [0x671, 5],
0xDC308C: [0x67B, 1], # Stampede Sprint
0xDC308D: [0x67B, 2],
0xDC308E: [0x67B, 3],
0xDC308F: [0x67B, 4],
0xDC3090: [0x67B, 5],
0xDC3091: [0x67C, 1], # Criss Kross Cliffs
0xDC3092: [0x67C, 2],
0xDC3093: [0x67C, 3],
0xDC3094: [0x67C, 5],
0xDC3095: [0x67D, 1], # Tyrant Twin Tussle
0xDC3096: [0x67D, 2],
0xDC3097: [0x67D, 3],
0xDC3098: [0x67D, 4],
0xDC3099: [0x67D, 5],
0xDC309A: [0x663, 1], # Swoopy Salvo
0xDC309B: [0x663, 2],
0xDC309C: [0x663, 3],
0xDC309D: [0x663, 4],
0xDC309E: [0x663, 5],
0xDC309F: [0x67E, 1], # Rocket Rush
0xDC30A0: [0x67E, 5],
0xDC30A1: [0x64F, 1], # Bosses
0xDC30A2: [0x650, 1],
0xDC30A3: [0x651, 1],
0xDC30A4: [0x652, 1],
0xDC30A5: [0x653, 1],
0xDC30A6: [0x654, 1],
0xDC30A7: [0x655, 1],
0xDC30A8: [0x656, 1],
0xDC30A9: [0x647, 1], # Banana Bird Caves
0xDC30AA: [0x645, 1],
0xDC30AB: [0x644, 1],
0xDC30AC: [0x642, 1],
0xDC30AD: [0x643, 1],
0xDC30AE: [0x646, 1],
0xDC30AF: [0x648, 1],
0xDC30B0: [0x649, 1],
0xDC30B1: [0x64A, 1],
#0xDC30B2: [0x64B, 1], # Disabled until Trade Sequence
0xDC30B3: [0x64C, 1],
#0xDC30B4: [0x64D, 1], # Disabled until Trade Sequence
0xDC30B5: [0x64E, 1],
0xDC30B6: [0x5FD, 4], # Banana Bird Mother
# DKC3_TODO: Disabled until Trade Sequence
#0xDC30B7: [0x615, 2, True],
#0xDC30B8: [0x615, 3, True],
#0xDC30B9: [0x619, 2],
##0xDC30BA:
#0xDC30BB: [0x61B, 3],
#0xDC30BC: [0x61D, 2],
#0xDC30BD: [0x621, 4],
#0xDC30BE: [0x625, 4, True],
}
item_rom_data = {
0xDC3001: [0x5D5], # 1-Up Balloon
0xDC3002: [0x5C9], # Bear Coin
0xDC3003: [0x5CB], # Bonus Coin
0xDC3004: [0x5CF], # DK Coin
0xDC3005: [0x5CD], # Banana Bird
0xDC3006: [0x5D1, 0x603], # Cog
}
music_rom_data = [
0x3D06B1,
0x3D0753,
0x3D071D,
0x3D07FA,
0x3D07C4,
0x3D08FE,
0x3D096C,
0x3D078E,
0x3D08CD,
0x3D09DD,
0x3D0A0E,
0x3D0AB3,
0x3D06E7,
0x3D0AE4,
0x3D0A45,
0x3D0B46,
0x3D0C40,
0x3D0897,
0x3D0B77,
0x3D0BD9,
0x3D0C71,
0x3D0866,
0x3D0B15,
0x3D0BA8,
0x3D0830,
0x3D0D04,
0x3D0CA2,
0x3D0A7C,
0x3D0D35,
0x3D0CD3,
0x3D0DC8,
0x3D0D66,
0x3D09AC,
0x3D0D97,
0x3D0C0F,
0x3D0DF9,
0x3D0E31,
0x3D0E62,
0x3D0934,
0x3D0E9A,
]
level_music_ids = [
0x06,
0x07,
0x08,
0x0A,
0x0B,
0x0E,
0x0F,
0x10,
0x17,
0x19,
0x1C,
0x1D,
0x1E,
0x21,
]
class LocalRom(object):
def __init__(self, file, patch=True, vanillaRom=None, name=None, hash=None):
self.name = name
self.hash = hash
self.orig_buffer = None
with open(file, 'rb') as stream:
self.buffer = read_rom(stream)
#if patch:
# self.patch_rom()
# self.orig_buffer = self.buffer.copy()
#if vanillaRom:
# with open(vanillaRom, 'rb') as vanillaStream:
# self.orig_buffer = read_rom(vanillaStream)
def read_bit(self, address: int, bit_number: int) -> bool:
bitflag = (1 << bit_number)
return ((self.buffer[address] & bitflag) != 0)
def read_byte(self, address: int) -> int:
return self.buffer[address]
def read_bytes(self, startaddress: int, length: int) -> bytes:
return self.buffer[startaddress:startaddress + length]
def write_byte(self, address: int, value: int):
self.buffer[address] = value
def write_bytes(self, startaddress: int, values):
self.buffer[startaddress:startaddress + len(values)] = values
def write_to_file(self, file):
with open(file, 'wb') as outfile:
outfile.write(self.buffer)
def read_from_file(self, file):
with open(file, 'rb') as stream:
self.buffer = bytearray(stream.read())
def patch_rom(world, rom, player, active_level_list):
local_random = world.slot_seeds[player]
# Boomer Costs
bonus_coin_cost = world.krematoa_bonus_coin_cost[player]
inverted_bonus_coin_cost = 0x100 - bonus_coin_cost
rom.write_byte(0x3498B9, inverted_bonus_coin_cost)
rom.write_byte(0x3498BA, inverted_bonus_coin_cost)
rom.write_byte(0x3498BB, inverted_bonus_coin_cost)
rom.write_byte(0x3498BC, inverted_bonus_coin_cost)
rom.write_byte(0x3498BD, inverted_bonus_coin_cost)
rom.write_byte(0x349857, bonus_coin_cost)
rom.write_byte(0x349862, bonus_coin_cost)
# Gyrocopter Costs
dk_coin_cost = world.dk_coins_for_gyrocopter[player]
rom.write_byte(0x3484A6, dk_coin_cost)
rom.write_byte(0x3484D5, dk_coin_cost)
rom.write_byte(0x3484D7, 0x90)
rom.write_byte(0x3484DC, 0xEA)
rom.write_byte(0x3484DD, 0xEA)
rom.write_byte(0x3484DE, 0xEA)
rom.write_byte(0x348528, 0x80) # Prevent Single-Ski Lock
# Make Swanky free
rom.write_byte(0x348C48, 0x00)
# 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)
rom.write_byte(0x34AB85, banana_bird_cost)
rom.write_byte(0x329FD8, banana_bird_cost)
rom.write_byte(0x32A025, banana_bird_cost)
rom.write_byte(0x329FDA, 0xB0)
else:
# rom.write_byte(0x34AB84, 0x20) # These cause hangs at Wrinkly's
# rom.write_byte(0x329FD8, 0x20)
# rom.write_byte(0x32A025, 0x20)
rom.write_byte(0x329FDA, 0xB0)
# Baffle Mirror Fix
rom.write_byte(0x9133, 0x08)
rom.write_byte(0x9135, 0x0C)
rom.write_byte(0x9136, 0x2B)
rom.write_byte(0x9137, 0x06)
# Palette Swap
rom.write_byte(0x3B96A5, 0xD0)
if world.kong_palette_swap[player] == "default":
rom.write_byte(0x3B96A9, 0x00)
rom.write_byte(0x3B96A8, 0x00)
elif world.kong_palette_swap[player] == "purple":
rom.write_byte(0x3B96A9, 0x00)
rom.write_byte(0x3B96A8, 0x3C)
elif world.kong_palette_swap[player] == "spooky":
rom.write_byte(0x3B96A9, 0x00)
rom.write_byte(0x3B96A8, 0xA0)
elif world.kong_palette_swap[player] == "dark":
rom.write_byte(0x3B96A9, 0x05)
rom.write_byte(0x3B96A8, 0xA0)
elif world.kong_palette_swap[player] == "chocolate":
rom.write_byte(0x3B96A9, 0x1D)
rom.write_byte(0x3B96A8, 0xA0)
elif world.kong_palette_swap[player] == "shadow":
rom.write_byte(0x3B96A9, 0x45)
rom.write_byte(0x3B96A8, 0xA0)
elif world.kong_palette_swap[player] == "red_gold":
rom.write_byte(0x3B96A9, 0x5D)
rom.write_byte(0x3B96A8, 0xA0)
elif world.kong_palette_swap[player] == "gbc":
rom.write_byte(0x3B96A9, 0x20)
rom.write_byte(0x3B96A8, 0x3C)
elif world.kong_palette_swap[player] == "halloween":
rom.write_byte(0x3B96A9, 0x70)
rom.write_byte(0x3B96A8, 0x3C)
if world.music_shuffle[player]:
for address in music_rom_data:
rand_song = local_random.choice(level_music_ids)
rom.write_byte(address, rand_song)
# Starting Lives
rom.write_byte(0x9130, world.starting_life_count[player].value)
rom.write_byte(0x913B, world.starting_life_count[player].value)
# Handle Level Shuffle Here
if world.level_shuffle[player]:
for i in range(len(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)
# 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))
rom.write_byte(0x34BC4A, (0x32 + level_dict[active_level_list[10]].levelID))
rom.write_byte(0x34BC53, (0x32 + level_dict[active_level_list[15]].levelID))
rom.write_byte(0x34BC59, (0x32 + level_dict[active_level_list[20]].levelID))
rom.write_byte(0x34BC5C, (0x32 + level_dict[active_level_list[25]].levelID))
rom.write_byte(0x34BC65, (0x32 + level_dict[active_level_list[30]].levelID))
rom.write_byte(0x34BC6E, (0x32 + level_dict[active_level_list[35]].levelID))
# Cotton-Top Cove Boss Unlock
rom.write_byte(0x34C02A, (0x32 + level_dict[active_level_list[14]].levelID))
# Kong-Fused Cliffs Unlock
rom.write_byte(0x34C213, (0x32 + level_dict[active_level_list[25]].levelID))
rom.write_byte(0x34C21B, (0x32 + level_dict[active_level_list[26]].levelID))
if world.goal[player] == "knautilus":
# Swap Kastle KAOS and Knautilus
rom.write_byte(0x34D4E1, 0xC2)
rom.write_byte(0x34D4E2, 0x24)
rom.write_byte(0x34D551, 0xBA)
rom.write_byte(0x34D552, 0x23)
rom.write_byte(0x32F339, 0x55)
from Main import __version__
rom.name = bytearray(f'D3{__version__.replace(".", "")[0:3]}_{player}_{world.seed:11}\0', 'utf8')[:21]
rom.name.extend([0] * (21 - len(rom.name)))
rom.write_bytes(0x7FC0, rom.name)
# DKC3_TODO: This is a hack, reconsider
# Don't grant (DK, Bonus, Bear) Coins
rom.write_byte(0x3BD454, 0xEA)
rom.write_byte(0x3BD455, 0xEA)
# Don't grant Cogs
rom.write_byte(0x3BD574, 0xEA)
rom.write_byte(0x3BD575, 0xEA)
rom.write_byte(0x3BD576, 0xEA)
# Don't grant Banana Birds at their caves
rom.write_byte(0x32DD62, 0xEA)
rom.write_byte(0x32DD63, 0xEA)
rom.write_byte(0x32DD64, 0xEA)
# Don't grant Patch and Skis from their bosses
rom.write_byte(0x3F3762, 0x00)
rom.write_byte(0x3F377B, 0x00)
rom.write_byte(0x3F3797, 0x00)
# Always allow Start+Select
rom.write_byte(0x8BAB, 0x01)
# Handle Alt Palettes in Krematoa
rom.write_byte(0x3B97E9, 0x80)
rom.write_byte(0x3B97EA, 0xEA)
class DKC3DeltaPatch(APDeltaPatch):
hash = USHASH
game = "Donkey Kong Country 3"
patch_file_ending = ".apdkc3"
@classmethod
def get_source_data(cls) -> bytes:
return get_base_rom_bytes()
def get_base_rom_bytes(file_name: str = "") -> bytes:
base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None)
if not base_rom_bytes:
file_name = get_base_rom_path(file_name)
base_rom_bytes = bytes(read_rom(open(file_name, "rb")))
basemd5 = hashlib.md5()
basemd5.update(base_rom_bytes)
if USHASH != basemd5.hexdigest():
raise Exception('Supplied Base Rom does not match known MD5 for US(1.0) release. '
'Get the correct game and version, then dump it')
get_base_rom_bytes.base_rom_bytes = base_rom_bytes
return base_rom_bytes
def get_base_rom_path(file_name: str = "") -> str:
options = Utils.get_options()
if not file_name:
file_name = options["dkc3_options"]["rom_file"]
if not os.path.exists(file_name):
file_name = Utils.local_path(file_name)
return file_name

View File

@@ -1,32 +0,0 @@
import math
from BaseClasses import MultiWorld
from .Names import LocationName, ItemName
from ..AutoWorld import LogicMixin
from ..generic.Rules import add_rule, set_rule
def set_rules(world: MultiWorld, player: int):
if False:#world.include_trade_sequence[player]:
add_rule(world.get_location(LocationName.barnacles_island, player),
lambda state: state.has(ItemName.shell, player))
add_rule(world.get_location(LocationName.blues_beach_hut, player),
lambda state: state.has(ItemName.present, player))
add_rule(world.get_location(LocationName.brambles_bungalow, player),
lambda state: state.has(ItemName.flower, player))
add_rule(world.get_location(LocationName.barters_swap_shop, player),
lambda state: state.has(ItemName.mirror, player))
if world.goal[player] != "knautilus":
required_banana_birds = math.floor(
world.number_of_banana_birds[player].value * (world.percentage_of_banana_birds[player].value / 100.0))
add_rule(world.get_location(LocationName.banana_bird_mother, player),
lambda state: state.has(ItemName.banana_bird, player, required_banana_birds))
world.completion_condition[player] = lambda state: state.has(ItemName.victory, player)

View File

@@ -1,208 +0,0 @@
import os
import typing
import math
import threading
from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification
from .Items import DKC3Item, ItemData, item_table, inventory_table
from .Locations import DKC3Location, all_locations, setup_locations
from .Options import dkc3_options
from .Regions import create_regions, connect_regions
from .Levels import level_list
from .Rules import set_rules
from .Names import ItemName, LocationName
from ..AutoWorld import WebWorld, World
from .Rom import LocalRom, patch_rom, get_base_rom_path, DKC3DeltaPatch
import Patch
class DKC3Web(WebWorld):
theme = "jungle"
setup_en = Tutorial(
"Multiworld Setup Guide",
"A guide to setting up the Donkey Kong Country 3 randomizer connected to an Archipelago Multiworld.",
"English",
"setup_en.md",
"setup/en",
["PoryGone"]
)
tutorials = [setup_en]
class DKC3World(World):
"""
Donkey Kong Country 3 is an action platforming game.
Play as Dixie Kong and her baby cousin Kiddy as they try to solve the
mystery of why Donkey Kong and Diddy disappeared while on vacation.
"""
game: str = "Donkey Kong Country 3"
option_definitions = dkc3_options
topology_present = False
data_version = 1
#hint_blacklist = {LocationName.rocket_rush_flag}
item_name_to_id = {name: data.code for name, data in item_table.items()}
location_name_to_id = all_locations
active_level_list: typing.List[str]
web = DKC3Web()
def __init__(self, world: MultiWorld, player: int):
self.rom_name_available_event = threading.Event()
super().__init__(world, player)
@classmethod
def stage_assert_generate(cls, world):
rom_file = get_base_rom_path()
if not os.path.exists(rom_file):
raise FileNotFoundError(rom_file)
def _get_slot_data(self):
return {
#"death_link": self.world.death_link[self.player].value,
"active_levels": self.active_level_list,
}
def _create_items(self, name: str):
data = item_table[name]
return [self.create_item(name)] * data.quantity
def fill_slot_data(self) -> dict:
slot_data = self._get_slot_data()
for option_name in dkc3_options:
option = getattr(self.world, option_name)[self.player]
slot_data[option_name] = option.value
return slot_data
def generate_basic(self):
self.topology_present = self.world.level_shuffle[self.player].value
itempool: typing.List[DKC3Item] = []
# Levels
total_required_locations = 159
number_of_banana_birds = 0
# Rocket Rush Cog
total_required_locations -= 1
number_of_cogs = 4
self.world.get_location(LocationName.rocket_rush_flag, self.player).place_locked_item(self.create_item(ItemName.krematoa_cog))
number_of_bosses = 8
if self.world.goal[self.player] == "knautilus":
self.world.get_location(LocationName.kastle_kaos, self.player).place_locked_item(self.create_item(ItemName.victory))
number_of_bosses = 7
else:
self.world.get_location(LocationName.banana_bird_mother, self.player).place_locked_item(self.create_item(ItemName.victory))
number_of_banana_birds = self.world.number_of_banana_birds[self.player]
# Bosses
total_required_locations += number_of_bosses
# Secret Caves
total_required_locations += 13
## Brothers Bear
if False:#self.world.include_trade_sequence[self.player]:
total_required_locations += 10
number_of_bonus_coins = (self.world.krematoa_bonus_coin_cost[self.player] * 5)
number_of_bonus_coins += math.ceil((85 - number_of_bonus_coins) * self.world.percentage_of_extra_bonus_coins[self.player] / 100)
itempool += [self.create_item(ItemName.bonus_coin)] * number_of_bonus_coins
itempool += [self.create_item(ItemName.dk_coin)] * 41
itempool += [self.create_item(ItemName.banana_bird)] * number_of_banana_birds
itempool += [self.create_item(ItemName.krematoa_cog)] * number_of_cogs
itempool += [self.create_item(ItemName.progressive_boat)] * 3
total_junk_count = total_required_locations - len(itempool)
itempool += [self.create_item(ItemName.bear_coin)] * total_junk_count
self.active_level_list = level_list.copy()
if self.world.level_shuffle[self.player]:
self.world.random.shuffle(self.active_level_list)
connect_regions(self.world, self.player, self.active_level_list)
self.world.itempool += itempool
def generate_output(self, output_directory: str):
try:
world = self.world
player = self.player
rom = LocalRom(get_base_rom_path())
patch_rom(self.world, rom, self.player, self.active_level_list)
self.active_level_list.append(LocationName.rocket_rush_region)
outfilepname = f'_P{player}'
outfilepname += f"_{world.player_name[player].replace(' ', '_')}" \
if world.player_name[player] != 'Player%d' % player else ''
rompath = os.path.join(output_directory, f'AP_{world.seed_name}{outfilepname}.sfc')
rom.write_to_file(rompath)
self.rom_name = rom.name
patch = DKC3DeltaPatch(os.path.splitext(rompath)[0]+DKC3DeltaPatch.patch_file_ending, player=player,
player_name=world.player_name[player], patched_path=rompath)
patch.write()
except:
raise
finally:
if os.path.exists(rompath):
os.unlink(rompath)
self.rom_name_available_event.set() # make sure threading continues and errors are collected
def modify_multidata(self, multidata: dict):
import base64
# wait for self.rom_name to be available.
self.rom_name_available_event.wait()
rom_name = getattr(self, "rom_name", None)
# we skip in case of error, so that the original error in the output thread is the one that gets raised
if rom_name:
new_name = base64.b64encode(bytes(self.rom_name)).decode()
multidata["connect_names"][new_name] = multidata["connect_names"][self.world.player_name[self.player]]
if self.topology_present:
world_names = [
LocationName.lake_orangatanga_region,
LocationName.kremwood_forest_region,
LocationName.cotton_top_cove_region,
LocationName.mekanos_region,
LocationName.k3_region,
LocationName.razor_ridge_region,
LocationName.kaos_kore_region,
LocationName.krematoa_region,
]
er_hint_data = {}
for world_index in range(len(world_names)):
for level_index in range(5):
level_region = self.world.get_region(self.active_level_list[world_index * 5 + level_index], self.player)
for location in level_region.locations:
er_hint_data[location.address] = world_names[world_index]
multidata['er_hint_data'][self.player] = er_hint_data
def create_regions(self):
location_table = setup_locations(self.world, self.player)
create_regions(self.world, self.player, location_table)
def create_item(self, name: str, force_non_progression=False) -> Item:
data = item_table[name]
if force_non_progression:
classification = ItemClassification.filler
elif data.progression:
classification = ItemClassification.progression
else:
classification = ItemClassification.filler
created_item = DKC3Item(name, classification, data.code, self.player)
return created_item
def set_rules(self):
set_rules(self.world, self.player)

View File

@@ -1,35 +0,0 @@
# Donkey Kong Country 3
## Where is the settings page?
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a config file.
## What does randomization do to this game?
Items which the player would normally acquire throughout the game have been moved around. Logic remains, so the game is
always able to be completed, but because of the item shuffle the player may need to access certain areas before they
would in the vanilla game.
## What is the goal of Donkey Kong Country 3 when randomized?
There are two goals which can be chosen:
- `Knautilus`: Collect Bonus Coins and Krematoa Cogs to reach K. Rool's submarine in Krematoa
- `Banana Bird Hunt`: Collect Banana Birds to free the Banana Bird Mother
## What items and locations get shuffled?
All Bonus Coins, DK Coins, and Banana Birds (if on a `Banana Bird Hunt` goal) are randomized. Additionally, level clears award a location check.
The Patch and two Skis for upgrading the boat are included. Bear Coins are provided if additional items are needed for the item pool.
Four of the Five Krematoa Cogs are randomized, but the final one is always in its vanilla location at the Flag of Rocket Rush in Krematoa
## Which items can be in another player's world?
Any shuffled item can be in other players' worlds.
## What does another world's item look like in Donkey Kong Country 3
Items pickups all retain their original appearance. You won't know if an item belongs to another player until you collect.
## When the player receives an item, what happens?
Currently, the items are silently added to the player's inventory, which can be seen when saving the game.

Some files were not shown because too many files have changed in this diff Show More