forked from mirror/Archipelago
Compare commits
109 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db5b7e5db9 | ||
|
|
7c808bb03b | ||
|
|
530b6cc360 | ||
|
|
95012c004f | ||
|
|
59918b9dbc | ||
|
|
b47cca4515 | ||
|
|
5f27019855 | ||
|
|
0b228834c2 | ||
|
|
57979b9287 | ||
|
|
4b85000960 | ||
|
|
d1f34d088b | ||
|
|
3bc9392e5b | ||
|
|
75165803a0 | ||
|
|
afc9c772be | ||
|
|
07450bb83d | ||
|
|
2ff7e83ad9 | ||
|
|
d817fdcfdb | ||
|
|
f3d966897f | ||
|
|
9acaf1c279 | ||
|
|
fd6a0b547f | ||
|
|
c02f355479 | ||
|
|
7d9203ef84 | ||
|
|
e849e4792d | ||
|
|
4565b3af8d | ||
|
|
e5b868e0e9 | ||
|
|
489450d3fa | ||
|
|
73afab67c8 | ||
|
|
c61f77029b | ||
|
|
79702aba65 | ||
|
|
1e366ff66f | ||
|
|
a0482cf27e | ||
|
|
288a623ab6 | ||
|
|
3b2037a2d4 | ||
|
|
ce536fa3ac | ||
|
|
41883e44e7 | ||
|
|
c3ff201b90 | ||
|
|
e6635cdd77 | ||
|
|
cfc9d79c79 | ||
|
|
fe2c355739 | ||
|
|
04c3429839 | ||
|
|
cabbe0aaf6 | ||
|
|
a7787d87f9 | ||
|
|
79b851189f | ||
|
|
9e972eafb2 | ||
|
|
53a995372f | ||
|
|
17351021b3 | ||
|
|
8ff2c1b6f3 | ||
|
|
45aea2c8ff | ||
|
|
9f5e40283a | ||
|
|
025309ec64 | ||
|
|
bd4850b2b5 | ||
|
|
472e114fb9 | ||
|
|
828bcb1266 | ||
|
|
9897f4eb4b | ||
|
|
e1ef820184 | ||
|
|
b3ad766680 | ||
|
|
74b19dc1f5 | ||
|
|
449bc93307 | ||
|
|
622af17705 | ||
|
|
a42f7f99fe | ||
|
|
3c6bd555b4 | ||
|
|
a4211d5f11 | ||
|
|
090c5bcf00 | ||
|
|
82850d7f66 | ||
|
|
86112351a6 | ||
|
|
ce789d1e3e | ||
|
|
73fb1b8074 | ||
|
|
8e15fe51b6 | ||
|
|
aa954b776d | ||
|
|
76f6eb1434 | ||
|
|
e38308bac3 | ||
|
|
e804f592de | ||
|
|
6e0a0c5c4a | ||
|
|
122590fc68 | ||
|
|
c806366469 | ||
|
|
0d3bd6e2e8 | ||
|
|
beac0b1acd | ||
|
|
1cc9c7a469 | ||
|
|
17db0805a7 | ||
|
|
2f53972c85 | ||
|
|
9ac780102e | ||
|
|
60b80083e0 | ||
|
|
8597b04c41 | ||
|
|
6a60c46a99 | ||
|
|
5c2163a1a7 | ||
|
|
a49bcd618d | ||
|
|
d76b41afe7 | ||
|
|
ab2b635a77 | ||
|
|
7072c7bd45 | ||
|
|
530c5500c3 | ||
|
|
8870b577d0 | ||
|
|
7d85ab471a | ||
|
|
3205cbf932 | ||
|
|
b9fb4de878 | ||
|
|
bcd7096e1d | ||
|
|
b206f2846a | ||
|
|
8a8bc6aa34 | ||
|
|
bce7c258c3 | ||
|
|
cea7278faf | ||
|
|
d7a9b98ce8 | ||
|
|
7dcde12e2e | ||
|
|
ba2a5c4744 | ||
|
|
39ac3c38bf | ||
|
|
61f751a1db | ||
|
|
5f2193f2e4 | ||
|
|
98b714f84a | ||
|
|
2a0198b618 | ||
|
|
cd9f8f3119 | ||
|
|
37b569eca6 |
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
@@ -17,9 +17,9 @@ jobs:
|
||||
python-version: '3.8'
|
||||
- name: Download run-time dependencies
|
||||
run: |
|
||||
Invoke-WebRequest -Uri https://github.com/alttpo/sni/releases/download/v0.0.81/sni-v0.0.81-windows-amd64.zip -OutFile sni.zip
|
||||
Invoke-WebRequest -Uri https://github.com/alttpo/sni/releases/download/v0.0.82/sni-v0.0.82-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/win-x64.zip -OutFile enemizer.zip
|
||||
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
|
||||
- name: Build
|
||||
run: |
|
||||
@@ -63,11 +63,11 @@ jobs:
|
||||
chmod a+rx appimagetool
|
||||
- name: Download run-time dependencies
|
||||
run: |
|
||||
wget -nv https://github.com/alttpo/sni/releases/download/v0.0.81/sni-v0.0.81-manylinux2014-amd64.tar.xz
|
||||
wget -nv https://github.com/alttpo/sni/releases/download/v0.0.82/sni-v0.0.82-manylinux2014-amd64.tar.xz
|
||||
tar xf sni-*.tar.xz
|
||||
rm sni-*.tar.xz
|
||||
mv sni-* SNI
|
||||
wget -nv https://github.com/Ijwu/Enemizer/releases/download/7.0/ubuntu.16.04-x64.7z
|
||||
wget -nv https://github.com/Ijwu/Enemizer/releases/download/7.0.1/ubuntu.16.04-x64.7z
|
||||
7za x -oEnemizerCLI/ ubuntu.16.04-x64.7z
|
||||
- name: Build
|
||||
run: |
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -51,11 +51,11 @@ jobs:
|
||||
chmod a+rx appimagetool
|
||||
- name: Download run-time dependencies
|
||||
run: |
|
||||
wget -nv https://github.com/alttpo/sni/releases/download/v0.0.81/sni-v0.0.81-manylinux2014-amd64.tar.xz
|
||||
wget -nv https://github.com/alttpo/sni/releases/download/v0.0.82/sni-v0.0.82-manylinux2014-amd64.tar.xz
|
||||
tar xf sni-*.tar.xz
|
||||
rm sni-*.tar.xz
|
||||
mv sni-* SNI
|
||||
wget -nv https://github.com/Ijwu/Enemizer/releases/download/7.0/ubuntu.16.04-x64.7z
|
||||
wget -nv https://github.com/Ijwu/Enemizer/releases/download/7.0.1/ubuntu.16.04-x64.7z
|
||||
7za x -oEnemizerCLI/ ubuntu.16.04-x64.7z
|
||||
- name: Build
|
||||
run: |
|
||||
|
||||
14
.gitignore
vendored
14
.gitignore
vendored
@@ -116,6 +116,9 @@ target/
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# vim editor
|
||||
*.swp
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
@@ -152,10 +155,17 @@ 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
|
||||
|
||||
@@ -126,7 +126,6 @@ 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)
|
||||
@@ -390,20 +389,14 @@ class MultiWorld():
|
||||
self.state.collect(item, True)
|
||||
|
||||
def push_item(self, location: Location, item: Item, collect: bool = True):
|
||||
if not isinstance(location, Location):
|
||||
raise RuntimeError(
|
||||
'Cannot assign item %s to invalid location %s (player %d).' % (item, location, item.player))
|
||||
assert location.can_fill(self.state, item, False), f"Cannot place {item} into {location}."
|
||||
location.item = item
|
||||
item.location = location
|
||||
item.world = self # try to not have this here anymore and create it with item?
|
||||
if collect:
|
||||
self.state.collect(item, location.event, 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))
|
||||
logging.debug('Placed %s at %s', item, location)
|
||||
|
||||
def get_entrances(self) -> List[Entrance]:
|
||||
if self._cached_entrances is None:
|
||||
@@ -1191,19 +1184,19 @@ class Item:
|
||||
|
||||
@property
|
||||
def advancement(self) -> bool:
|
||||
return bool(self.classification & ItemClassification.progression)
|
||||
return ItemClassification.progression in self.classification
|
||||
|
||||
@property
|
||||
def skip_in_prog_balancing(self) -> bool:
|
||||
return self.classification == ItemClassification.progression_skip_balancing
|
||||
return ItemClassification.progression_skip_balancing in self.classification
|
||||
|
||||
@property
|
||||
def useful(self) -> bool:
|
||||
return bool(self.classification & ItemClassification.useful)
|
||||
return ItemClassification.useful in self.classification
|
||||
|
||||
@property
|
||||
def trap(self) -> bool:
|
||||
return bool(self.classification & ItemClassification.trap)
|
||||
return ItemClassification.trap in self.classification
|
||||
|
||||
@property
|
||||
def flags(self) -> int:
|
||||
@@ -1431,8 +1424,6 @@ 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' %
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import sys
|
||||
import asyncio
|
||||
import shutil
|
||||
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.update()
|
||||
@@ -32,20 +34,34 @@ 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:
|
||||
await super(ChecksFinderContext, self).server_auth(password_requested)
|
||||
if not self.auth: # TODO: Replace this if block with await self.getusername() once that PR is merged in.
|
||||
logger.info('Enter slot name:')
|
||||
self.auth = await self.console_input()
|
||||
|
||||
await self.get_username()
|
||||
await self.send_connect()
|
||||
|
||||
async def connection_closed(self):
|
||||
await super(ChecksFinderContext, self).connection_closed()
|
||||
path = os.path.expandvars(r"%localappdata%/ChecksFinder")
|
||||
for root, dirs, files in os.walk(path):
|
||||
for root, dirs, files in os.walk(self.game_communication_path):
|
||||
for file in files:
|
||||
if file.find("obtain") <= -1:
|
||||
os.remove(root + "/" + file)
|
||||
@@ -59,26 +75,25 @@ class ChecksFinderContext(CommonContext):
|
||||
|
||||
async def shutdown(self):
|
||||
await super(ChecksFinderContext, self).shutdown()
|
||||
path = os.path.expandvars(r"%localappdata%/ChecksFinder")
|
||||
for root, dirs, files in os.walk(path):
|
||||
for root, dirs, files in os.walk(self.game_communication_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(os.path.expandvars(r"%localappdata%/ChecksFinder")):
|
||||
os.mkdir(os.path.expandvars(r"%localappdata%/ChecksFinder"))
|
||||
if not os.path.exists(self.game_communication_path):
|
||||
os.makedirs(self.game_communication_path)
|
||||
for ss in self.checked_locations:
|
||||
filename = f"send{ss}"
|
||||
with open(os.path.expandvars(r"%localappdata%/ChecksFinder/" + filename), 'w') as f:
|
||||
with open(os.path.join(self.game_communication_path, 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.expandvars(r"%localappdata%/ChecksFinder/" + filename), 'w') as f:
|
||||
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
|
||||
f.write(str(NetworkItem(*item).item))
|
||||
f.close()
|
||||
|
||||
@@ -86,7 +101,7 @@ class ChecksFinderContext(CommonContext):
|
||||
if "checked_locations" in args:
|
||||
for ss in self.checked_locations:
|
||||
filename = f"send{ss}"
|
||||
with open(os.path.expandvars(r"%localappdata%/ChecksFinder/" + filename), 'w') as f:
|
||||
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
|
||||
f.close()
|
||||
|
||||
def run_gui(self):
|
||||
@@ -112,10 +127,9 @@ 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(path):
|
||||
for root, dirs, files in os.walk(ctx.game_communication_path):
|
||||
for file in files:
|
||||
if file.find("send") > -1:
|
||||
st = file.split("send", -1)[1]
|
||||
|
||||
@@ -43,12 +43,14 @@ class ClientCommandProcessor(CommandProcessor):
|
||||
def _cmd_connect(self, address: str = "") -> bool:
|
||||
"""Connect to a MultiWorld Server"""
|
||||
self.ctx.server_address = None
|
||||
self.ctx.username = None
|
||||
asyncio.create_task(self.ctx.connect(address if address else None), name="connecting")
|
||||
return True
|
||||
|
||||
def _cmd_disconnect(self) -> bool:
|
||||
"""Disconnect from a MultiWorld Server"""
|
||||
self.ctx.server_address = None
|
||||
self.ctx.username = None
|
||||
asyncio.create_task(self.ctx.disconnect(), name="disconnecting")
|
||||
return True
|
||||
|
||||
@@ -161,6 +163,7 @@ class CommonContext:
|
||||
def __init__(self, server_address, password):
|
||||
# server state
|
||||
self.server_address = server_address
|
||||
self.username = None
|
||||
self.password = password
|
||||
self.hint_cost = None
|
||||
self.slot_info = {}
|
||||
@@ -253,6 +256,13 @@ class CommonContext:
|
||||
self.password = await self.console_input()
|
||||
return self.password
|
||||
|
||||
async def get_username(self):
|
||||
if not self.auth:
|
||||
self.auth = self.username
|
||||
if not self.auth:
|
||||
logger.info('Enter slot name:')
|
||||
self.auth = await self.console_input()
|
||||
|
||||
async def send_connect(self, **kwargs):
|
||||
payload = {
|
||||
'cmd': 'Connect',
|
||||
@@ -309,6 +319,7 @@ class CommonContext:
|
||||
|
||||
async def shutdown(self):
|
||||
self.server_address = ""
|
||||
self.username = None
|
||||
if self.server and not self.server.socket.closed:
|
||||
await self.server.socket.close()
|
||||
if self.server_task:
|
||||
@@ -469,12 +480,20 @@ async def server_loop(ctx: CommonContext, address=None):
|
||||
logger.info('Please connect to an Archipelago server.')
|
||||
return
|
||||
|
||||
address = f"ws://{address}" if "://" not in address else address
|
||||
port = urllib.parse.urlparse(address).port or 38281
|
||||
address = f"ws://{address}" if "://" not in address \
|
||||
else address.replace("archipelago://", "ws://")
|
||||
|
||||
server_url = urllib.parse.urlparse(address)
|
||||
if server_url.username:
|
||||
ctx.username = server_url.username
|
||||
if server_url.password:
|
||||
ctx.password = server_url.password
|
||||
port = server_url.port or 38281
|
||||
|
||||
logger.info(f'Connecting to Archipelago server at {address}')
|
||||
try:
|
||||
socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None)
|
||||
ctx.ui.update_address_bar(server_url.netloc)
|
||||
ctx.server = Endpoint(socket)
|
||||
logger.info('Connected')
|
||||
ctx.server_address = address
|
||||
@@ -585,6 +604,7 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
||||
raise Exception('Connection refused by the multiworld host, no reason provided')
|
||||
|
||||
elif cmd == 'Connected':
|
||||
ctx.username = ctx.auth
|
||||
ctx.team = args["team"]
|
||||
ctx.slot = args["slot"]
|
||||
# int keys get lost in JSON transfer
|
||||
@@ -708,10 +728,7 @@ if __name__ == '__main__':
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
await super(TextContext, self).server_auth(password_requested)
|
||||
if not self.auth:
|
||||
logger.info('Enter slot name:')
|
||||
self.auth = await self.console_input()
|
||||
|
||||
await self.get_username()
|
||||
await self.send_connect()
|
||||
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
@@ -722,6 +739,7 @@ if __name__ == '__main__':
|
||||
async def main(args):
|
||||
ctx = TextContext(args.connect, args.password)
|
||||
ctx.auth = args.name
|
||||
ctx.server_address = args.connect
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
|
||||
|
||||
if gui_enabled:
|
||||
|
||||
@@ -400,6 +400,7 @@ 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,6 +411,9 @@ 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.")
|
||||
@@ -421,7 +425,10 @@ if __name__ == '__main__':
|
||||
else:
|
||||
raise FileNotFoundError(f"Path {executable} is not an executable file.")
|
||||
|
||||
server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password, *rest)
|
||||
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)
|
||||
|
||||
asyncio.run(main(args))
|
||||
colorama.deinit()
|
||||
|
||||
12
Fill.py
12
Fill.py
@@ -42,8 +42,16 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
|
||||
|
||||
has_beaten_game = world.has_beaten_game(maximum_exploration_state)
|
||||
|
||||
for item_to_place in items_to_place:
|
||||
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)
|
||||
|
||||
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) \
|
||||
@@ -54,7 +62,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):
|
||||
# poping by index is faster than removing by content,
|
||||
# popping by index is faster than removing by content,
|
||||
spot_to_fill = locations.pop(i)
|
||||
# skipping a scan for the element
|
||||
break
|
||||
|
||||
74
Generate.py
74
Generate.py
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import random
|
||||
@@ -7,6 +9,7 @@ from typing import Set, Dict, Tuple, Callable, Any, Union
|
||||
import os
|
||||
from collections import Counter
|
||||
import string
|
||||
import enum
|
||||
|
||||
import ModuleUpdate
|
||||
|
||||
@@ -25,7 +28,43 @@ from worlds.alttp.Text import TextTable
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
import copy
|
||||
|
||||
categories = set(AutoWorldRegister.world_types)
|
||||
|
||||
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"
|
||||
|
||||
|
||||
def mystery_argparse():
|
||||
@@ -64,7 +103,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: Set[str] = {arg.strip().lower() for arg in args.plando.split(",")}
|
||||
args.plando: PlandoSettings = PlandoSettings.from_option_string(args.plando)
|
||||
return args, options
|
||||
|
||||
|
||||
@@ -127,7 +166,7 @@ def main(args=None, callback=ERmain):
|
||||
|
||||
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"{', '.join(args.plando)}")
|
||||
f"{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. "
|
||||
@@ -403,7 +442,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 "bosses" in plando_options:
|
||||
elif PlandoSettings.bosses in plando_options:
|
||||
options = boss_shuffle.lower().split(";")
|
||||
remainder_shuffle = "none" # vanilla
|
||||
bosses = []
|
||||
@@ -452,7 +491,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: Set[str] = frozenset(("bosses",))):
|
||||
def roll_settings(weights: dict, plando_options: PlandoSettings = PlandoSettings.bosses):
|
||||
if "linked_options" in weights:
|
||||
weights = roll_linked_options(weights)
|
||||
|
||||
@@ -465,17 +504,11 @@ def roll_settings(weights: dict, plando_options: Set[str] = frozenset(("bosses",
|
||||
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 = 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
|
||||
required_plando_options = PlandoSettings.from_option_string(requirements.get("plando", ""))
|
||||
if required_plando_options not in plando_options:
|
||||
if required_plando_options:
|
||||
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.")
|
||||
raise Exception(f"Settings reports required plando module {str(required_plando_options)}, "
|
||||
f"which is not enabled.")
|
||||
|
||||
ret = argparse.Namespace()
|
||||
for option_key in Options.per_game_common_options:
|
||||
@@ -504,12 +537,12 @@ def roll_settings(weights: dict, plando_options: Set[str] = frozenset(("bosses",
|
||||
# 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 "items" in plando_options:
|
||||
if PlandoSettings.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 "connections" in plando_options:
|
||||
if PlandoSettings.connections in plando_options:
|
||||
options = game_weights.get("plando_connections", [])
|
||||
for placement in options:
|
||||
if roll_percentage(get_choice("percentage", placement, 100)):
|
||||
@@ -555,9 +588,6 @@ 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')
|
||||
|
||||
@@ -629,7 +659,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 "texts" in plando_options:
|
||||
if PlandoSettings.texts in plando_options:
|
||||
tt = TextTable()
|
||||
tt.removeUnwantedText()
|
||||
options = weights.get("plando_texts", [])
|
||||
@@ -641,7 +671,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 "connections" in plando_options:
|
||||
if PlandoSettings.connections in plando_options:
|
||||
options = weights.get("plando_connections", [])
|
||||
for placement in options:
|
||||
if roll_percentage(get_choice_legacy("percentage", placement, 100)):
|
||||
|
||||
@@ -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')),
|
||||
file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3')),
|
||||
Component('LttP Adjuster', 'LttPAdjuster'),
|
||||
# Factorio
|
||||
Component('Factorio Client', 'FactorioClient'),
|
||||
|
||||
@@ -47,7 +47,7 @@ def main():
|
||||
|
||||
parser.add_argument('rom', nargs="?", default='AP_LttP.sfc', help='Path to an ALttP rom to adjust.')
|
||||
parser.add_argument('--baserom', default='Zelda no Densetsu - Kamigami no Triforce (Japan).sfc',
|
||||
help='Path to an ALttP JAP(1.0) rom to use as a base.')
|
||||
help='Path to an ALttP Japan(1.0) rom to use as a base.')
|
||||
parser.add_argument('--loglevel', default='info', const='info', nargs='?',
|
||||
choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
|
||||
parser.add_argument('--menuspeed', default='normal', const='normal', nargs='?',
|
||||
@@ -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.isSet():
|
||||
while not done.is_set():
|
||||
task.do_events()
|
||||
logging.info("Done updating sprites")
|
||||
|
||||
@@ -300,6 +300,7 @@ 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:
|
||||
@@ -1263,4 +1264,4 @@ class ToolTips(object):
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
main()
|
||||
|
||||
4
Main.py
4
Main.py
@@ -47,7 +47,6 @@ 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()
|
||||
@@ -364,7 +363,8 @@ 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.address should then also be None. Location: " \
|
||||
f" {location}"
|
||||
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]:
|
||||
|
||||
@@ -720,16 +720,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):
|
||||
text = "Player Status on your team:"
|
||||
def get_status_string(ctx: Context, team: int, tag: str):
|
||||
text = f"Player Status on team {team}:"
|
||||
for slot in ctx.locations:
|
||||
connected = len(ctx.clients[team][slot])
|
||||
death_link = len([client for client in ctx.clients[team][slot] if "DeathLink" in client.tags])
|
||||
tagged = len([client for client in ctx.clients[team][slot] if tag in client.tags])
|
||||
completion_text = f"({len(ctx.location_checks[team, slot])}/{len(ctx.locations[slot])})"
|
||||
death_text = f" {death_link} of which are death link" if connected else ""
|
||||
tag_text = f" {tagged} of which are tagged {tag}" if connected and tag 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"{death_text}{goal_text} {completion_text}"
|
||||
f"{tag_text}{goal_text} {completion_text}"
|
||||
return text
|
||||
|
||||
|
||||
@@ -766,7 +766,7 @@ def update_checked_locations(ctx: Context, team: int, slot: int):
|
||||
def forfeit_player(ctx: Context, team: int, slot: int):
|
||||
"""register any locations that are in the multidata"""
|
||||
all_locations = set(ctx.locations[slot])
|
||||
ctx.notify_all("%s (Team #%d) has forfeited" % (ctx.player_names[(team, slot)], team + 1))
|
||||
ctx.notify_all("%s (Team #%d) has released all remaining items from their world." % (ctx.player_names[(team, slot)], team + 1))
|
||||
register_location_checks(ctx, team, slot, all_locations)
|
||||
update_checked_locations(ctx, team, slot)
|
||||
|
||||
@@ -779,7 +779,7 @@ def collect_player(ctx: Context, team: int, slot: int, is_group: bool = False):
|
||||
if values[1] == slot:
|
||||
all_locations[source_slot].add(location_id)
|
||||
|
||||
ctx.notify_all("%s (Team #%d) has collected" % (ctx.player_names[(team, slot)], team + 1))
|
||||
ctx.notify_all("%s (Team #%d) has collected their items from other worlds." % (ctx.player_names[(team, slot)], team + 1))
|
||||
for source_player, location_ids in all_locations.items():
|
||||
register_location_checks(ctx, team, source_player, location_ids, count_activity=False)
|
||||
update_checked_locations(ctx, team, source_player)
|
||||
@@ -1106,20 +1106,26 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
return self.ctx.commandprocessor(command)
|
||||
|
||||
def _cmd_players(self) -> bool:
|
||||
"""Get information about connected and missing players"""
|
||||
"""Get information about connected and missing players."""
|
||||
if len(self.ctx.player_names) < 10:
|
||||
self.ctx.notify_all(get_players_string(self.ctx))
|
||||
else:
|
||||
self.output(get_players_string(self.ctx))
|
||||
return True
|
||||
|
||||
def _cmd_status(self) -> bool:
|
||||
"""Get status information about your team."""
|
||||
self.output(get_status_string(self.ctx, self.client.team))
|
||||
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))
|
||||
return True
|
||||
|
||||
def _cmd_release(self) -> bool:
|
||||
"""Sends remaining items in your world to their recipients."""
|
||||
return self._cmd_forfeit()
|
||||
|
||||
def _cmd_forfeit(self) -> bool:
|
||||
"""Surrender and send your remaining items out to their recipients"""
|
||||
"""Surrender and send your remaining items out to their recipients. Use release in the future."""
|
||||
if self.ctx.allow_forfeits.get((self.client.team, self.client.slot), False):
|
||||
forfeit_player(self.ctx, self.client.team, self.client.slot)
|
||||
return True
|
||||
@@ -1128,7 +1134,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
return True
|
||||
elif "disabled" in self.ctx.forfeit_mode:
|
||||
self.output(
|
||||
"Sorry, client forfeiting has been disabled on this server. You can ask the server admin for a /forfeit")
|
||||
"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:
|
||||
@@ -1136,8 +1142,8 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
return True
|
||||
else:
|
||||
self.output(
|
||||
"Sorry, client forfeiting requires you to have beaten the game on this server."
|
||||
" You can ask the server admin for a /forfeit")
|
||||
"Sorry, client item releasing requires you to have beaten the game on this server."
|
||||
" You can ask the server admin for a /release")
|
||||
return False
|
||||
|
||||
def _cmd_collect(self) -> bool:
|
||||
@@ -1302,6 +1308,8 @@ 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:
|
||||
@@ -1653,6 +1661,14 @@ 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())
|
||||
@@ -1698,43 +1714,48 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
self.output(f"Could not find player {player_name} to collect")
|
||||
return False
|
||||
|
||||
@mark_raw
|
||||
def _cmd_release(self, player_name: str) -> bool:
|
||||
"""Send out the remaining items from a player to their intended recipients."""
|
||||
return self._cmd_forfeit(player_name)
|
||||
|
||||
@mark_raw
|
||||
def _cmd_forfeit(self, player_name: str) -> bool:
|
||||
"""Send out the remaining items from a player to their intended recipients"""
|
||||
"""Send out the remaining items from a player to their intended recipients."""
|
||||
seeked_player = player_name.lower()
|
||||
for (team, slot), name in self.ctx.player_names.items():
|
||||
if name.lower() == seeked_player:
|
||||
forfeit_player(self.ctx, team, slot)
|
||||
return True
|
||||
|
||||
self.output(f"Could not find player {player_name} to forfeit")
|
||||
self.output(f"Could not find player {player_name} to release")
|
||||
return False
|
||||
|
||||
@mark_raw
|
||||
def _cmd_allow_forfeit(self, player_name: str) -> bool:
|
||||
"""Allow the specified player to use the !forfeit command"""
|
||||
"""Allow the specified player to use the !release command."""
|
||||
seeked_player = player_name.lower()
|
||||
for (team, slot), name in self.ctx.player_names.items():
|
||||
if name.lower() == seeked_player:
|
||||
self.ctx.allow_forfeits[(team, slot)] = True
|
||||
self.output(f"Player {player_name} is now allowed to use the !forfeit command at any time.")
|
||||
self.output(f"Player {player_name} is now allowed to use the !release command at any time.")
|
||||
return True
|
||||
|
||||
self.output(f"Could not find player {player_name} to allow the !forfeit command for.")
|
||||
self.output(f"Could not find player {player_name} to allow the !release command for.")
|
||||
return False
|
||||
|
||||
@mark_raw
|
||||
def _cmd_forbid_forfeit(self, player_name: str) -> bool:
|
||||
""""Disallow the specified player from using the !forfeit command"""
|
||||
""""Disallow the specified player from using the !release command."""
|
||||
seeked_player = player_name.lower()
|
||||
for (team, slot), name in self.ctx.player_names.items():
|
||||
if name.lower() == seeked_player:
|
||||
self.ctx.allow_forfeits[(team, slot)] = False
|
||||
self.output(
|
||||
f"Player {player_name} has to follow the server restrictions on use of the !forfeit command.")
|
||||
f"Player {player_name} has to follow the server restrictions on use of the !release command.")
|
||||
return True
|
||||
|
||||
self.output(f"Could not find player {player_name} to forbid the !forfeit command for.")
|
||||
self.output(f"Could not find player {player_name} to forbid the !release command for.")
|
||||
return False
|
||||
|
||||
def _cmd_send_multiple(self, amount: typing.Union[int, str], player_name: str, *item_name: str) -> bool:
|
||||
|
||||
@@ -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 = 1
|
||||
script_version: int = 2
|
||||
|
||||
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))
|
||||
|
||||
34
Patch.py
34
Patch.py
@@ -166,27 +166,31 @@ GAME_ALTTP = "A Link to the Past"
|
||||
GAME_SM = "Super Metroid"
|
||||
GAME_SOE = "Secret of Evermore"
|
||||
GAME_SMZ3 = "SMZ3"
|
||||
supported_games = {"A Link to the Past", "Super Metroid", "Secret of Evermore", "SMZ3"}
|
||||
GAME_DKC3 = "Donkey Kong Country 3"
|
||||
supported_games = {"A Link to the Past", "Super Metroid", "Secret of Evermore", "SMZ3", "Donkey Kong Country 3"}
|
||||
|
||||
preferred_endings = {
|
||||
GAME_ALTTP: "apbp",
|
||||
GAME_SM: "apm3",
|
||||
GAME_SOE: "apsoe",
|
||||
GAME_SMZ3: "apsmz"
|
||||
GAME_SMZ3: "apsmz",
|
||||
GAME_DKC3: "apdkc3"
|
||||
}
|
||||
|
||||
|
||||
def generate_yaml(patch: bytes, metadata: Optional[dict] = None, game: str = GAME_ALTTP) -> bytes:
|
||||
if game == GAME_ALTTP:
|
||||
from worlds.alttp.Rom import JAP10HASH as HASH
|
||||
from worlds.alttp.Rom import LTTPJPN10HASH as HASH
|
||||
elif game == GAME_SM:
|
||||
from worlds.sm.Rom import JAP10HASH as HASH
|
||||
from worlds.sm.Rom import SMJUHASH as HASH
|
||||
elif game == GAME_SOE:
|
||||
from worlds.soe.Patch import USHASH as HASH
|
||||
elif game == GAME_SMZ3:
|
||||
from worlds.alttp.Rom import JAP10HASH as ALTTPHASH
|
||||
from worlds.sm.Rom import JAP10HASH as SMHASH
|
||||
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.")
|
||||
|
||||
@@ -216,7 +220,10 @@ 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 ".apm3")
|
||||
".apbp" if game == GAME_ALTTP
|
||||
else ".apsmz" if game == GAME_SMZ3
|
||||
else ".apdkc3" if game == GAME_DKC3
|
||||
else ".apm3")
|
||||
write_lzma(bytes, target)
|
||||
return target
|
||||
|
||||
@@ -245,6 +252,8 @@ 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()
|
||||
@@ -389,6 +398,13 @@ 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}")
|
||||
@@ -396,7 +412,9 @@ 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"):
|
||||
if zfinfo.filename.endswith(".apbp") or \
|
||||
zfinfo.filename.endswith(".apm3") or \
|
||||
zfinfo.filename.endswith(".apdkc3"):
|
||||
data = update_patch_data(data, server)
|
||||
with ziplock:
|
||||
zfw.writestr(zfinfo, data)
|
||||
|
||||
@@ -26,6 +26,8 @@ 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
|
||||
@@ -49,7 +51,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 wiki page on [running Archipelago from source](https://github.com/ArchipelagoMW/Archipelago/wiki/Running-from-source).
|
||||
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).
|
||||
|
||||
## 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.
|
||||
@@ -66,7 +68,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 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](docs/) 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/)
|
||||
|
||||
101
SNIClient.py
101
SNIClient.py
@@ -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
|
||||
from Patch import GAME_ALTTP, GAME_SM, GAME_SMZ3, GAME_DKC3
|
||||
|
||||
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:8080 1" """
|
||||
Examples: "/snes", "/snes 1", "/snes localhost:23074 1" """
|
||||
|
||||
snes_address = self.ctx.snes_address
|
||||
snes_device_number = -1
|
||||
@@ -188,7 +188,10 @@ class Context(CommonContext):
|
||||
async def shutdown(self):
|
||||
await super(Context, self).shutdown()
|
||||
if self.snes_connect_task:
|
||||
await self.snes_connect_task
|
||||
try:
|
||||
await asyncio.wait_for(self.snes_connect_task, 1)
|
||||
except asyncio.TimeoutError:
|
||||
self.snes_connect_task.cancel()
|
||||
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
if cmd in {"Connected", "RoomUpdate"}:
|
||||
@@ -251,6 +254,9 @@ 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()
|
||||
|
||||
|
||||
@@ -595,7 +601,7 @@ class SNESState(enum.IntEnum):
|
||||
SNES_ATTACHED = 3
|
||||
|
||||
|
||||
def launch_sni(ctx: Context):
|
||||
def launch_sni():
|
||||
sni_path = Utils.get_options()["lttp_options"]["sni"]
|
||||
|
||||
if not os.path.isdir(sni_path):
|
||||
@@ -633,11 +639,9 @@ 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()
|
||||
succesful = False
|
||||
while not succesful:
|
||||
while 1:
|
||||
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
|
||||
@@ -647,7 +651,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(ctx)
|
||||
launch_sni()
|
||||
|
||||
await asyncio.sleep(1)
|
||||
else:
|
||||
@@ -1034,44 +1038,48 @@ async def game_watcher(ctx: Context):
|
||||
if not ctx.rom:
|
||||
ctx.finished_game = False
|
||||
ctx.death_link_allow_survive = False
|
||||
game_name = await snes_read(ctx, SM_ROMNAME_START, 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]
|
||||
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
|
||||
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]
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
if ctx.awaiting_rom:
|
||||
await ctx.server_auth(False)
|
||||
@@ -1279,6 +1287,9 @@ 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):
|
||||
@@ -1296,7 +1307,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:8080', help='Address of the SNI server.')
|
||||
parser.add_argument('--snes', default='localhost:23074', help='Address of the SNI server.')
|
||||
parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
@@ -19,7 +19,13 @@ 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 Utils import init_logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
from MultiServer import mark_raw
|
||||
import ctypes
|
||||
import sys
|
||||
|
||||
from Utils import init_logging, is_windows
|
||||
|
||||
if __name__ == "__main__":
|
||||
init_logging("SC2Client", exception_logger="Client")
|
||||
@@ -73,6 +79,17 @@ 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
|
||||
@@ -95,10 +112,7 @@ class SC2Context(CommonContext):
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
await super(SC2Context, self).server_auth(password_requested)
|
||||
if not self.auth:
|
||||
logger.info('Enter slot name:')
|
||||
self.auth = await self.console_input()
|
||||
|
||||
await self.get_username()
|
||||
await self.send_connect()
|
||||
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
@@ -114,6 +128,11 @@ 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"]):
|
||||
@@ -418,8 +437,9 @@ 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.")
|
||||
|
||||
run_game(sc2.maps.get(maps_table[mission_id - 1]), [Bot(Race.Terran, ArchipelagoBot(ctx, mission_id),
|
||||
name="Archipelago", fullscreen=True)], realtime=True)
|
||||
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)
|
||||
|
||||
|
||||
class ArchipelagoBot(sc2.bot_ai.BotAI):
|
||||
@@ -799,6 +819,101 @@ 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())
|
||||
|
||||
19
Utils.py
19
Utils.py
@@ -30,7 +30,7 @@ class Version(typing.NamedTuple):
|
||||
build: int
|
||||
|
||||
|
||||
__version__ = "0.3.3"
|
||||
__version__ = "0.3.4"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
|
||||
is_linux = sys.platform.startswith('linux')
|
||||
@@ -277,7 +277,12 @@ 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
|
||||
@@ -474,9 +479,13 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, wri
|
||||
def stream_input(stream, queue):
|
||||
def queuer():
|
||||
while 1:
|
||||
text = stream.readline().strip()
|
||||
if text:
|
||||
queue.put_nowait(text)
|
||||
try:
|
||||
text = stream.readline().strip()
|
||||
except UnicodeDecodeError as e:
|
||||
logging.exception(e)
|
||||
else:
|
||||
if text:
|
||||
queue.put_nowait(text)
|
||||
|
||||
from threading import Thread
|
||||
thread = Thread(target=queuer, name=f"Stream handler for {stream.name}", daemon=True)
|
||||
|
||||
@@ -69,6 +69,10 @@ class B64UUIDConverter(BaseConverter):
|
||||
app.url_map.converters["suuid"] = B64UUIDConverter
|
||||
app.jinja_env.filters['suuid'] = lambda value: base64.urlsafe_b64encode(value.bytes).rstrip(b'=').decode('ascii')
|
||||
|
||||
# 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:
|
||||
@@ -141,6 +145,11 @@ 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)
|
||||
|
||||
@@ -154,8 +154,10 @@ 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)
|
||||
generation for generation in Generation
|
||||
if generation.state == STATE_QUEUED).for_update()
|
||||
for generation in to_start:
|
||||
launch_generator(generator_pool, generation)
|
||||
except AlreadyRunningException:
|
||||
|
||||
@@ -12,7 +12,7 @@ def allowed_file(filename):
|
||||
return filename.endswith(('.txt', ".yaml", ".zip"))
|
||||
|
||||
|
||||
from Generate import roll_settings
|
||||
from Generate import roll_settings, PlandoSettings
|
||||
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 = set(plando_options)
|
||||
plando_options = PlandoSettings.from_set(set(plando_options))
|
||||
results = {}
|
||||
rolled_results = {}
|
||||
for filename, text in options.items():
|
||||
|
||||
@@ -78,6 +78,8 @@ 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, attachment_filename=fname)
|
||||
|
||||
@@ -4,7 +4,7 @@ import random
|
||||
import json
|
||||
import zipfile
|
||||
from collections import Counter
|
||||
from typing import Dict, Optional as TypeOptional
|
||||
from typing import Dict, Optional, Any
|
||||
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
|
||||
from Generate import handle_name, PlandoSettings
|
||||
import pickle
|
||||
|
||||
from .models import *
|
||||
from .models import Generation, STATE_ERROR, STATE_QUEUED, commit, db_session, Seed, UUID
|
||||
from WebHostLib import app
|
||||
from .check import get_yaml_data, roll_options
|
||||
from .upload import upload_zip_to_db
|
||||
@@ -30,16 +30,15 @@ def get_meta(options_source: dict) -> dict:
|
||||
}
|
||||
plando_options -= {""}
|
||||
|
||||
meta = {
|
||||
server_options = {
|
||||
"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 meta
|
||||
return {"server_options": server_options, "plando_options": list(plando_options)}
|
||||
|
||||
|
||||
@app.route('/generate', methods=['GET', 'POST'])
|
||||
@@ -60,13 +59,13 @@ def generate(race=False):
|
||||
results, gen_options = roll_options(options, meta["plando_options"])
|
||||
|
||||
if race:
|
||||
meta["item_cheat"] = False
|
||||
meta["remaining_mode"] = "disabled"
|
||||
meta["server_options"]["item_cheat"] = False
|
||||
meta["server_options"]["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 for now. "
|
||||
flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players. "
|
||||
f"If you have a larger group, please generate it yourself and upload it.")
|
||||
elif len(gen_options) >= app.config["JOB_THRESHOLD"]:
|
||||
gen = Generation(
|
||||
@@ -92,35 +91,35 @@ def generate(race=False):
|
||||
return render_template("generate.html", race=race, version=__version__)
|
||||
|
||||
|
||||
def gen_game(gen_options, meta: TypeOptional[Dict[str, object]] = None, owner=None, sid=None):
|
||||
def gen_game(gen_options, meta: Optional[Dict[str, Any]] = None, owner=None, sid=None):
|
||||
if not meta:
|
||||
meta: Dict[str, object] = {}
|
||||
meta: Dict[str, Any] = {}
|
||||
|
||||
meta.setdefault("server_options", {}).setdefault("hint_cost", 10)
|
||||
race = meta.setdefault("race", False)
|
||||
|
||||
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() # reset to time-based random source
|
||||
random.seed() # use time-based random source
|
||||
else:
|
||||
random.seed(seed)
|
||||
|
||||
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 overwrittin in mystery
|
||||
erargs.name = {x: "" for x in range(1, playercount + 1)} # only so it can be overwritten 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 = ", ".join(plando_options)
|
||||
erargs.plando_options = PlandoSettings.from_set(meta.setdefault("plando_options",
|
||||
{"bosses", "items", "connections", "texts"}))
|
||||
|
||||
name_counter = Counter()
|
||||
for player, (playerfile, settings) in enumerate(gen_options.items(), 1):
|
||||
@@ -136,7 +135,7 @@ def gen_game(gen_options, meta: TypeOptional[Dict[str, object]] = None, owner=No
|
||||
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)
|
||||
ERmain(erargs, seed, baked_server_options=meta["server_options"])
|
||||
|
||||
return upload_to_db(target.name, sid, owner, race)
|
||||
except BaseException as e:
|
||||
@@ -148,7 +147,6 @@ def gen_game(gen_options, meta: TypeOptional[Dict[str, object]] = None, owner=No
|
||||
meta = json.loads(gen.meta)
|
||||
meta["error"] = (e.__class__.__name__ + ": " + str(e))
|
||||
gen.meta = json.dumps(meta)
|
||||
|
||||
commit()
|
||||
raise
|
||||
|
||||
|
||||
@@ -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: 6 * 60 * 60) # seconds since last activity to shutdown
|
||||
timeout = Required(int, default=lambda: 2 * 60 * 60) # seconds since last activity to shutdown
|
||||
tracker = Optional(UUID, index=True)
|
||||
last_port = Optional(int, default=lambda: 0)
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ def create():
|
||||
|
||||
for game_name, world in AutoWorldRegister.world_types.items():
|
||||
|
||||
all_options = {**world.options, **Options.per_game_common_options}
|
||||
all_options = {**Options.per_game_common_options, **world.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,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
flask>=2.1.2
|
||||
flask>=2.1.3
|
||||
pony>=0.7.16
|
||||
waitress>=2.1.1
|
||||
flask-caching>=1.11.1
|
||||
Flask-Caching>=2.0.1
|
||||
Flask-Compress>=1.12
|
||||
Flask-Limiter>=2.4.6
|
||||
bokeh>=2.4.3
|
||||
Flask-Limiter>=2.5.0
|
||||
bokeh>=2.4.3
|
||||
|
||||
@@ -49,6 +49,12 @@ If you are ready to start randomizing games, or want to start playing your favor
|
||||
our discord server at the [Archipelago Discord](https://discord.gg/archipelago). There are always people ready to answer
|
||||
any questions you might have.
|
||||
|
||||
## What are some common terms I should know?
|
||||
|
||||
As randomizers and multiworld randomizers have been around for a while now there are quite a lot of common terms
|
||||
and jargon that is used in conjunction by the communities surrounding them. For a lot of the terms that are more common
|
||||
to Archipelago and its specific systems please see the [Glossary](/glossary/en).
|
||||
|
||||
## I want to add a game to the Archipelago randomizer. How do I do that?
|
||||
|
||||
The best way to get started is to take a look at our code on GitHub
|
||||
|
||||
94
WebHostLib/static/assets/faq/glossary_en.md
Normal file
94
WebHostLib/static/assets/faq/glossary_en.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# Multiworld Glossary
|
||||
|
||||
There are a lot of common terms used when playing in different game randomizer communities and in multiworld as well.
|
||||
This document serves as a lookup for common terms that may be used by users in the community or in various other
|
||||
documentation.
|
||||
|
||||
## Item
|
||||
Items are what get shuffled around in your world or other worlds that you then receive. This could be a sword, a stat
|
||||
upgrade, a spell, or any other potential receivable for your game.
|
||||
|
||||
## Location
|
||||
Locations are where items are placed in your game. Whenever you interact with a location, you or another player will
|
||||
then receive an item. A location could be a chest, an enemy drop, a shop purchase, or any other interactable that can
|
||||
contain items in your game.
|
||||
|
||||
## Check
|
||||
A check is a common term for when you "check", or pick up, a location. In terms of Archipelago this is usually used for
|
||||
when a player goes to a location and sends its item, or "checks" the location. Players will often reference their now
|
||||
randomized locations as checks.
|
||||
|
||||
## Slot
|
||||
A slot is the player name and number assigned during generation. The number of slots is equal to the number of players,
|
||||
or "worlds", created. Each name must be unique as these are used to identify the slot user.
|
||||
|
||||
## World
|
||||
World in terms of Archipelago can mean multiple things and is used interchangeably in many situations.
|
||||
* During gameplay, a world is a single instance of a game, occupying one player "slot". However,
|
||||
Archipelago allows multiple players to connect to the same slot; then those players can share a world
|
||||
and complete it cooperatively. For games with native cooperative play, you can also play together and
|
||||
share a world that way, usually with only one player connected to the multiworld.
|
||||
* On the programming side, a world typically represents the package that integrates Archipelago with a
|
||||
particular game. For example this could be the entire `worlds/factorio` directory.
|
||||
|
||||
## RNG
|
||||
Acronym for "Random Number Generator." Archipelago uses its own custom Random object with a unique seed per generation,
|
||||
or, if running from source, a seed can be supplied and this seed will control all randomization during generation as all
|
||||
game worlds will have access to it.
|
||||
|
||||
## Seed
|
||||
A "seed" is a number used to initialize a pseudorandom number generator. Whenever you generate a new game on Archipelago
|
||||
this is a new "seed" as it has unique item placement, and you can create multiple "rooms" on the Archipelago site from a
|
||||
single seed. Using the same seed results in the random placement being the same.
|
||||
|
||||
## Room
|
||||
Whenever you generate a seed on the Archipelago website you will be put on a seed page that contains all the seed info
|
||||
with a link to the spoiler if one exists and will show how many unique rooms exist per seed. Each room has its own
|
||||
unique identifier that is separate from the seed. The room page is where you can find information to connect to the
|
||||
multiworld and download any patches if necessary. If you have a particularly fun or interesting seed, and you want to
|
||||
share it with somebody you can link them to this seed page, where they can generate a new room to play it! For seeds
|
||||
generated with race mode enabled, the seed page will only show rooms created by the unique user so the seed page is
|
||||
perfectly safe to share for racing purposes.
|
||||
|
||||
## Logic
|
||||
Base behavior of all seeds generated by Archipelago is they are expected to be completable based on the requirements of
|
||||
the settings. This is done by using "logic" in order to determine valid locations to place items while still being able
|
||||
to reach said location without this item. For the purposes of the randomizer a location is considered "in logic" if you
|
||||
can reach it with your current toolset of items or skills based on settings. Some players are able to obtain locations
|
||||
"out of logic" by performing various glitches or tricks that the settings may not account for and tend to mention this
|
||||
when sending out an item they obtained this way.
|
||||
|
||||
## Progression
|
||||
Certain items will allow access to more locations and are considered progression items as they "progress" the seed.
|
||||
|
||||
## Trash
|
||||
A term used for "filler" items that have no bearing on the generation and are either marginally useful for the player
|
||||
or useless. These items can be very useful depending on the player but are never very important and as such are usually
|
||||
termed trash.
|
||||
|
||||
## Burger King / BK Mode
|
||||
A term used in multiworlds when a player is unable to continue to progress and is awaiting an item. The term came to be
|
||||
after a player, allegedly, was unable to progress during a multiworld and went to Burger King while waiting to receive
|
||||
items from other players.
|
||||
|
||||
* "Logical BK" is when the player is unable to progress according to the settings of their game but may still be able to do
|
||||
things that would be "out of logic" by the generation.
|
||||
|
||||
* "Hard / full BK" is when the player is completely unable to progress even with tricks they may know and are unable to
|
||||
continue to play, aside from doing something like killing enemies for experience or money.
|
||||
|
||||
## Sphere
|
||||
Archipelago calculates the game playthrough by using a "sphere" system where it has a state for each player and checks
|
||||
to see what the players are able to reach with their current items. Any location that is reachable with the current
|
||||
state of items is a "sphere." For the purposes of Archipelago it starts playthrough calculation by distributing sphere 0
|
||||
items which are items that are either forced in the player's inventory by the game or placed in the `start_inventory` in
|
||||
their settings. Sphere 1 is then all accessible locations the players can reach with all the items they received from
|
||||
sphere 0, or their starting inventory. The playthrough continues in this fashion calculating a number of spheres until
|
||||
all players have completed their goal.
|
||||
|
||||
## Scouts / Scouting
|
||||
In some games there are locations that have visible items even if the item itself is unobtainable at the current time.
|
||||
Some games utilize a scouting feature where when the player "sees" the item it will give a free hint for the item in the
|
||||
client letting the players know what the exact item is, since if the item was for that game it would know but the item
|
||||
being foreign is a lot harder to represent visually.
|
||||
|
||||
53
WebHostLib/static/assets/glossary.js
Normal file
53
WebHostLib/static/assets/glossary.js
Normal file
@@ -0,0 +1,53 @@
|
||||
window.addEventListener('load', () => {
|
||||
const tutorialWrapper = document.getElementById('glossary-wrapper');
|
||||
new Promise((resolve, reject) => {
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.onreadystatechange = () => {
|
||||
if (ajax.readyState !== 4) { return; }
|
||||
if (ajax.status === 404) {
|
||||
reject("Sorry, the glossary page is not available in that language yet.");
|
||||
return;
|
||||
}
|
||||
if (ajax.status !== 200) {
|
||||
reject("Something went wrong while loading the glossary.");
|
||||
return;
|
||||
}
|
||||
resolve(ajax.responseText);
|
||||
};
|
||||
ajax.open('GET', `${window.location.origin}/static/assets/faq/` +
|
||||
`glossary_${tutorialWrapper.getAttribute('data-lang')}.md`, true);
|
||||
ajax.send();
|
||||
}).then((results) => {
|
||||
// Populate page with HTML generated from markdown
|
||||
showdown.setOption('tables', true);
|
||||
showdown.setOption('strikethrough', true);
|
||||
showdown.setOption('literalMidWordUnderscores', true);
|
||||
tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results);
|
||||
adjustHeaderWidth();
|
||||
|
||||
// Reset the id of all header divs to something nicer
|
||||
const headers = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6'));
|
||||
const scrollTargetIndex = window.location.href.search(/#[A-z0-9-_]*$/);
|
||||
for (let i=0; i < headers.length; i++){
|
||||
const headerId = headers[i].innerText.replace(/[ ]/g,'-').toLowerCase()
|
||||
headers[i].setAttribute('id', headerId);
|
||||
headers[i].addEventListener('click', () =>
|
||||
window.location.href = window.location.href.substring(0, scrollTargetIndex) + `#${headerId}`);
|
||||
}
|
||||
|
||||
// Manually scroll the user to the appropriate header if anchor navigation is used
|
||||
if (scrollTargetIndex > -1) {
|
||||
try{
|
||||
const scrollTarget = window.location.href.substring(scrollTargetIndex + 1);
|
||||
document.getElementById(scrollTarget).scrollIntoView({ behavior: "smooth" });
|
||||
} catch(error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
tutorialWrapper.innerHTML =
|
||||
`<h2>This page is out of logic!</h2>
|
||||
<h3>Click <a href="${window.location.origin}">here</a> to return to safety.</h3>`;
|
||||
});
|
||||
});
|
||||
@@ -23,6 +23,7 @@ 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) => {
|
||||
@@ -65,6 +66,15 @@ 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();
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
from collections import Counter, defaultdict
|
||||
from itertools import cycle
|
||||
from colorsys import hsv_to_rgb
|
||||
from datetime import datetime, timedelta, date
|
||||
from math import tau
|
||||
import typing
|
||||
|
||||
from bokeh.embed import components
|
||||
from bokeh.palettes import Dark2_8 as palette
|
||||
from bokeh.models import HoverTool
|
||||
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():
|
||||
|
||||
def get_db_data() -> typing.Tuple[typing.Dict[str, int], typing.Dict[datetime.date, typing.Dict[str, int]]]:
|
||||
games_played = defaultdict(Counter)
|
||||
total_games = Counter()
|
||||
cutoff = date.today()-timedelta(days=30000)
|
||||
cutoff = date.today()-timedelta(days=30)
|
||||
room: Room
|
||||
for room in select(room for room in Room if room.creation_time >= cutoff):
|
||||
for slot in room.seed.slots:
|
||||
@@ -26,29 +30,72 @@ def get_db_data():
|
||||
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():
|
||||
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=500, height=500)
|
||||
y_axis_label="Games Played", sizing_mode="scale_both", width=PLOT_WIDTH, height=500)
|
||||
|
||||
total_games, games_played = get_db_data()
|
||||
days = sorted(games_played)
|
||||
|
||||
cyc_palette = cycle(palette)
|
||||
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)}
|
||||
|
||||
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=next(cyc_palette))
|
||||
occurences, legend_label=game, line_width=2, color=game_to_color[game])
|
||||
|
||||
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=500, height=500)
|
||||
sizing_mode="scale_both", width=PLOT_WIDTH, height=500, x_range=(-0.5, 1.2))
|
||||
pie.axis.visible = False
|
||||
pie.xgrid.visible = False
|
||||
pie.ygrid.visible = False
|
||||
|
||||
data = {
|
||||
"games": [],
|
||||
@@ -65,12 +112,15 @@ def stats():
|
||||
current_angle += angle
|
||||
data["end_angles"].append(current_angle)
|
||||
|
||||
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,
|
||||
data["colors"] = [game_to_color[game] for game in data["games"]]
|
||||
|
||||
pie.wedge(x=0, y=0, radius=0.5,
|
||||
start_angle="start_angles", end_angle="end_angles", fill_color="colors",
|
||||
source=ColumnDataSource(data=data), legend_field="games")
|
||||
|
||||
script, charts = components((plot, pie))
|
||||
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))
|
||||
return render_template("stats.html", js_resources=INLINE.render_js(), css_resources=INLINE.render_css(),
|
||||
chart_data=script, charts=charts)
|
||||
|
||||
17
WebHostLib/templates/glossary.html
Normal file
17
WebHostLib/templates/glossary.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<title>Glossary</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.1/showdown.min.js"
|
||||
integrity="sha512-L03kznCrNOfVxOUovR6ESfCz9Gfny7gihUX/huVbQB9zjODtYpxaVtIaAkpetoiyV2eqWbvxMH9fiSv5enX7bw=="
|
||||
crossorigin="anonymous"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/glossary.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div id="glossary-wrapper" data-lang="{{ lang }}" class="markdown">
|
||||
<!-- Content generated by JavaScript -->
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -16,9 +16,9 @@
|
||||
This room has a <a href="{{ url_for("getTracker", tracker=room.tracker) }}">Multiworld Tracker</a> enabled.
|
||||
<br />
|
||||
{% endif %}
|
||||
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>
|
||||
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>
|
||||
{% 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 }}.">
|
||||
|
||||
@@ -40,9 +40,12 @@
|
||||
{% 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 in ["A Link to the Past", "Secret of Evermore", "Super Metroid", "SMZ3"] %}
|
||||
{% elif patch.game | supports_apdeltapatch %}
|
||||
<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 %}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
<li><a href="/user-content">User Content</a></li>
|
||||
<li><a href="/weighted-settings">Weighted Settings Page</a></li>
|
||||
<li><a href="{{url_for('stats')}}">Game Statistics</a></li>
|
||||
<li><a href="/glossary/en">Glossary</a></li>
|
||||
</ul>
|
||||
|
||||
<h2>Game Info Pages</h2>
|
||||
|
||||
@@ -15,10 +15,15 @@
|
||||
<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 %}
|
||||
|
||||
@@ -80,6 +80,11 @@ 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")
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ local socket = require("socket")
|
||||
local json = require('json')
|
||||
local math = require('math')
|
||||
|
||||
local last_modified_date = '2022-05-25' -- Should be the last modified date
|
||||
local script_version = 1
|
||||
local last_modified_date = '2022-07-24' -- Should be the last modified date
|
||||
local script_version = 2
|
||||
|
||||
--------------------------------------------------
|
||||
-- Heavily modified form of RiptideSage's tracker
|
||||
@@ -1723,6 +1723,11 @@ 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
|
||||
|
||||
@@ -1824,13 +1829,15 @@ 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
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 374 KiB After Width: | Height: | Size: 246 KiB |
@@ -8,6 +8,15 @@ 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]
|
||||
@@ -72,12 +81,14 @@ flowchart LR
|
||||
V6[VVVVVV]
|
||||
MT[Meritous]
|
||||
TW[The Witness]
|
||||
SA2B[Sonic Adventure 2: Battle]
|
||||
|
||||
APCLIENTPP <--> SOE
|
||||
APCLIENTPP <--> MT
|
||||
APCLIENTPP <-- The Witness Randomizer --> TW
|
||||
APCPP <--> SM64
|
||||
APCPP <--> V6
|
||||
APCPP <--> SA2B
|
||||
end
|
||||
SOE <--> SNI <-- Various, depending on SNES device --> SOESNES
|
||||
AS <-- WebSockets --> APCLIENTPP
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
@@ -191,8 +191,23 @@ 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 |
|
||||
| ---- | ---- | ----- |
|
||||
|
||||
58
docs/running from source.md
Normal file
58
docs/running from source.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# 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.
|
||||
49
docs/style.md
Normal file
49
docs/style.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# 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.
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
|
||||
13
host.yaml
13
host.yaml
@@ -101,7 +101,9 @@ sm_options:
|
||||
# Alternatively, a path to a program to open the .sfc file with
|
||||
rom_start: true
|
||||
factorio_options:
|
||||
executable: "factorio\\bin\\x64\\factorio"
|
||||
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"
|
||||
minecraft_options:
|
||||
forge_directory: "Minecraft Forge server"
|
||||
max_heap_size: "2G"
|
||||
@@ -127,3 +129,12 @@ 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
|
||||
|
||||
@@ -54,6 +54,7 @@ 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
|
||||
@@ -62,6 +63,7 @@ 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
|
||||
@@ -76,6 +78,7 @@ 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
|
||||
@@ -129,6 +132,7 @@ Type: dirifempty; Name: "{app}"
|
||||
|
||||
[InstallDelete]
|
||||
Type: files; Name: "{app}\ArchipelagoLttPClient.exe"
|
||||
Type: filesandordirs; Name: "{app}\lib\worlds\rogue-legacy*"
|
||||
|
||||
[Registry]
|
||||
|
||||
@@ -142,6 +146,11 @@ 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
|
||||
@@ -205,6 +214,9 @@ var LttPROMFilePage: TInputFileWizardPage;
|
||||
var smrom: string;
|
||||
var SMRomFilePage: TInputFileWizardPage;
|
||||
|
||||
var dkc3rom: string;
|
||||
var DKC3RomFilePage: TInputFileWizardPage;
|
||||
|
||||
var soerom: string;
|
||||
var SoERomFilePage: TInputFileWizardPage;
|
||||
|
||||
@@ -294,6 +306,8 @@ 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
|
||||
@@ -334,6 +348,22 @@ 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
|
||||
@@ -378,6 +408,10 @@ 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');
|
||||
@@ -391,6 +425,8 @@ 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
|
||||
|
||||
11
kvui.py
11
kvui.py
@@ -334,8 +334,8 @@ class GameManager(App):
|
||||
# top part
|
||||
server_label = ServerLabel()
|
||||
self.connect_layout.add_widget(server_label)
|
||||
self.server_connect_bar = ConnectBarTextInput(text="archipelago.gg", size_hint_y=None, height=30, multiline=False,
|
||||
write_tab=False)
|
||||
self.server_connect_bar = ConnectBarTextInput(text=self.ctx.server_address or "archipelago.gg", size_hint_y=None,
|
||||
height=30, multiline=False, write_tab=False)
|
||||
self.server_connect_bar.bind(on_text_validate=self.connect_button_action)
|
||||
self.connect_layout.add_widget(self.server_connect_bar)
|
||||
self.server_connect_button = Button(text="Connect", size=(100, 30), size_hint_y=None, size_hint_x=None)
|
||||
@@ -412,6 +412,7 @@ class GameManager(App):
|
||||
def connect_button_action(self, button):
|
||||
if self.ctx.server:
|
||||
self.ctx.server_address = None
|
||||
self.ctx.username = None
|
||||
asyncio.create_task(self.ctx.disconnect())
|
||||
else:
|
||||
asyncio.create_task(self.ctx.connect(self.server_connect_bar.text.replace("/connect ", "")))
|
||||
@@ -445,6 +446,12 @@ class GameManager(App):
|
||||
self.log_panels["Archipelago"].on_message_markup(text)
|
||||
self.log_panels["All"].on_message_markup(text)
|
||||
|
||||
def update_address_bar(self, text: str):
|
||||
if hasattr(self, "server_connect_bar"):
|
||||
self.server_connect_bar.text = text
|
||||
else:
|
||||
logging.getLogger("Client").info("Could not update address bar as the GUI is not yet initialized.")
|
||||
|
||||
def enable_energy_link(self):
|
||||
if not hasattr(self, "energy_link_label"):
|
||||
self.energy_link_label = Label(text="Energy Link: Standby",
|
||||
|
||||
@@ -175,12 +175,15 @@ 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: # 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.
|
||||
hints: # 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
|
||||
@@ -273,6 +276,7 @@ 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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
colorama>=0.4.4
|
||||
colorama>=0.4.5
|
||||
websockets>=10.3
|
||||
PyYAML>=6.0
|
||||
jellyfish>=0.9.0
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import typing
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink
|
||||
|
||||
|
||||
@@ -27,6 +28,35 @@ 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
|
||||
@@ -185,9 +215,11 @@ 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}
|
||||
|
||||
@@ -331,6 +363,7 @@ 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,
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import Utils
|
||||
from Patch import read_rom
|
||||
|
||||
JAP10HASH = '03a63945398191337e896e5771f77173'
|
||||
LTTPJPN10HASH = '03a63945398191337e896e5771f77173'
|
||||
RANDOMIZERBASEHASH = '9952c2a3ec1b421e408df0d20c8f0c7f'
|
||||
ROM_PLAYER_LIMIT = 255
|
||||
|
||||
@@ -19,7 +19,7 @@ import threading
|
||||
import xxtea
|
||||
import concurrent.futures
|
||||
import bsdiff4
|
||||
from typing import Optional
|
||||
from typing import Optional, List
|
||||
|
||||
from BaseClasses import CollectionState, Region, Location
|
||||
from worlds.alttp.Shops import ShopType, ShopPriceType
|
||||
@@ -186,7 +186,7 @@ def check_enemizer(enemizercli):
|
||||
# some time may have passed since the lock was acquired, as such a quick re-check doesn't hurt
|
||||
if getattr(check_enemizer, "done", None):
|
||||
return
|
||||
|
||||
wanted_version = (7, 0, 1)
|
||||
# version info is saved on the lib, for some reason
|
||||
library_info = os.path.join(os.path.dirname(enemizercli), "EnemizerCLI.Core.deps.json")
|
||||
with open(library_info) as f:
|
||||
@@ -197,10 +197,11 @@ def check_enemizer(enemizercli):
|
||||
version = lib.split("/")[-1]
|
||||
version = tuple(int(element) for element in version.split("."))
|
||||
enemizer_logger.debug(f"Found Enemizer version {version}")
|
||||
if version < (6, 4, 0):
|
||||
if version < wanted_version:
|
||||
raise Exception(
|
||||
f"Enemizer found at {enemizercli} is outdated ({info}), please update your Enemizer. "
|
||||
f"Such as https://github.com/Ijwu/Enemizer/releases")
|
||||
f"Enemizer found at {enemizercli} is outdated ({version}) < ({wanted_version}), "
|
||||
f"please update your Enemizer. "
|
||||
f"Such as from https://github.com/Ijwu/Enemizer/releases")
|
||||
break
|
||||
else:
|
||||
raise Exception(f"Could not find Enemizer library version information in {library_info}")
|
||||
@@ -1246,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] else 0x00) # pre-open Pyramid Hole
|
||||
rom.write_byte(0x18008B, 0x01 if world.open_pyramid[player].to_bool(world, 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
|
||||
@@ -1645,8 +1646,7 @@ def patch_rom(world, rom, player, enemized):
|
||||
# set rom name
|
||||
# 21 bytes
|
||||
from Main import __version__
|
||||
# TODO: Adjust Enemizer to accept AP and AD
|
||||
rom.name = bytearray(f'BM{__version__.replace(".", "")[0:3]}_{player}_{world.seed:11}\0', 'utf8')[:21]
|
||||
rom.name = bytearray(f'AP{__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)
|
||||
|
||||
@@ -1677,7 +1677,7 @@ def patch_race_rom(rom, world, player):
|
||||
rom.encrypt(world, player)
|
||||
|
||||
|
||||
def get_price_data(price: int, price_type: int) -> bytes:
|
||||
def get_price_data(price: int, price_type: int) -> List[int]:
|
||||
if price_type != ShopPriceType.Rupees:
|
||||
# Set special price flag 0x8000
|
||||
# Then set the type of price we're setting 0x7F00 (this starts from Hearts, not Rupees, subtract 1)
|
||||
@@ -2890,7 +2890,7 @@ hash_alphabet = [
|
||||
|
||||
|
||||
class LttPDeltaPatch(Patch.APDeltaPatch):
|
||||
hash = JAP10HASH
|
||||
hash = LTTPJPN10HASH
|
||||
game = "A Link to the Past"
|
||||
patch_file_ending = ".aplttp"
|
||||
|
||||
@@ -2907,8 +2907,8 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
|
||||
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5.update(base_rom_bytes)
|
||||
if JAP10HASH != basemd5.hexdigest():
|
||||
raise Exception('Supplied Base Rom does not match known MD5 for JAP(1.0) release. '
|
||||
if LTTPJPN10HASH != basemd5.hexdigest():
|
||||
raise Exception('Supplied Base Rom does not match known MD5 for Japan(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
|
||||
|
||||
@@ -460,10 +460,11 @@ 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(1, 'Bomb Upgrade (+5)', 100, bombupgrades)
|
||||
capacityshop.add_inventory(next(slots), 'Bomb Upgrade (+5)', 100, bombupgrades)
|
||||
if arrowupgrades:
|
||||
capacityshop.add_inventory(1, 'Arrow Upgrade (+5)', 100, arrowupgrades)
|
||||
capacityshop.add_inventory(next(slots), 'Arrow Upgrade (+5)', 100, arrowupgrades)
|
||||
else:
|
||||
for item in new_items:
|
||||
world.push_precollected(ItemFactory(item, player))
|
||||
|
||||
@@ -176,17 +176,6 @@ 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])
|
||||
|
||||
@@ -144,7 +144,8 @@ 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.
|
||||

|
||||
|
||||

|
||||
4. Ve a Menú principal --> Actualizador en línea --> Descargador de núcleos. Desplázate y selecciona "Nintendo - SNES /
|
||||
SFC (bsnes-mercury Performance)".
|
||||
|
||||
|
||||
@@ -299,4 +299,5 @@ item_table = (
|
||||
'A Shrubbery',
|
||||
'Roomba with a Knife',
|
||||
'Wet Cat',
|
||||
'The missing moderator, Frostwares',
|
||||
)
|
||||
|
||||
@@ -25,7 +25,7 @@ class ArchipIDLEWorld(World):
|
||||
"""
|
||||
game = "ArchipIDLE"
|
||||
topology_present = False
|
||||
data_version = 3
|
||||
data_version = 4
|
||||
hidden = (datetime.now().month != 4) # ArchipIDLE is only visible during April
|
||||
web = ArchipIDLEWebWorld()
|
||||
|
||||
|
||||
41
worlds/dark_souls_3/Options.py
Normal file
41
worlds/dark_souls_3/Options.py
Normal file
@@ -0,0 +1,41 @@
|
||||
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,
|
||||
}
|
||||
|
||||
287
worlds/dark_souls_3/__init__.py
Normal file
287
worlds/dark_souls_3/__init__.py
Normal file
@@ -0,0 +1,287 @@
|
||||
# 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):
|
||||
tutorials = [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"]
|
||||
)]
|
||||
|
||||
|
||||
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"
|
||||
options = 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)
|
||||
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"
|
||||
383
worlds/dark_souls_3/data/items_data.py
Normal file
383
worlds/dark_souls_3/data/items_data.py
Normal file
@@ -0,0 +1,383 @@
|
||||
"""
|
||||
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}
|
||||
434
worlds/dark_souls_3/data/locations_data.py
Normal file
434
worlds/dark_souls_3/data/locations_data.py
Normal file
@@ -0,0 +1,434 @@
|
||||
"""
|
||||
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}
|
||||
22
worlds/dark_souls_3/docs/en_Dark Souls III.md
Normal file
22
worlds/dark_souls_3/docs/en_Dark Souls III.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# 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 )
|
||||
|
||||
## 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.
|
||||
35
worlds/dark_souls_3/docs/setup_en.md
Normal file
35
worlds/dark_souls_3/docs/setup_en.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# 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)
|
||||
|
||||
## 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
|
||||
|
||||
**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).
|
||||
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** (renamed from the generated file AP-{ROOM_ID}.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
|
||||
221
worlds/dkc3/Client.py
Normal file
221
worlds/dkc3/Client.py
Normal file
@@ -0,0 +1,221 @@
|
||||
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)
|
||||
52
worlds/dkc3/Items.py
Normal file
52
worlds/dkc3/Items.py
Normal file
@@ -0,0 +1,52 @@
|
||||
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}
|
||||
115
worlds/dkc3/Levels.py
Normal file
115
worlds/dkc3/Levels.py
Normal file
@@ -0,0 +1,115 @@
|
||||
|
||||
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,
|
||||
]
|
||||
283
worlds/dkc3/Locations.py
Normal file
283
worlds/dkc3/Locations.py
Normal file
@@ -0,0 +1,283 @@
|
||||
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()}
|
||||
21
worlds/dkc3/Names/ItemName.py
Normal file
21
worlds/dkc3/Names/ItemName.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# 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"
|
||||
336
worlds/dkc3/Names/LocationName.py
Normal file
336
worlds/dkc3/Names/LocationName.py
Normal file
@@ -0,0 +1,336 @@
|
||||
# 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"
|
||||
132
worlds/dkc3/Options.py
Normal file
132
worlds/dkc3/Options.py
Normal file
@@ -0,0 +1,132 @@
|
||||
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,
|
||||
}
|
||||
883
worlds/dkc3/Regions.py
Normal file
883
worlds/dkc3/Regions.py
Normal file
@@ -0,0 +1,883 @@
|
||||
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)
|
||||
562
worlds/dkc3/Rom.py
Normal file
562
worlds/dkc3/Rom.py
Normal file
@@ -0,0 +1,562 @@
|
||||
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
|
||||
32
worlds/dkc3/Rules.py
Normal file
32
worlds/dkc3/Rules.py
Normal file
@@ -0,0 +1,32 @@
|
||||
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)
|
||||
208
worlds/dkc3/__init__.py
Normal file
208
worlds/dkc3/__init__.py
Normal file
@@ -0,0 +1,208 @@
|
||||
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"
|
||||
options = 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)
|
||||
35
worlds/dkc3/docs/en_Donkey Kong Country 3.md
Normal file
35
worlds/dkc3/docs/en_Donkey Kong Country 3.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# 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.
|
||||
161
worlds/dkc3/docs/setup_en.md
Normal file
161
worlds/dkc3/docs/setup_en.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# Donkey Kong Country 3 Randomizer Setup Guide
|
||||
|
||||
## Required Software
|
||||
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `SNI Client - Donkey Kong Country 3 Patch Setup`
|
||||
|
||||
|
||||
- Hardware or software capable of loading and playing SNES ROM files
|
||||
- An emulator capable of connecting to SNI such as:
|
||||
- snes9x Multitroid
|
||||
from: [snes9x Multitroid Download](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz),
|
||||
- BizHawk from: [BizHawk Website](http://tasvideos.org/BizHawk.html)
|
||||
- RetroArch 1.10.3 or newer from: [RetroArch Website](https://retroarch.com?page=platforms). Or,
|
||||
- An SD2SNES, FXPak Pro ([FXPak Pro Store Page](https://krikzz.com/store/home/54-fxpak-pro.html)), or other
|
||||
compatible hardware
|
||||
- Your legally obtained Donkey Kong Country 3 ROM file, probably named `Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc`
|
||||
|
||||
## Installation Procedures
|
||||
|
||||
### Windows Setup
|
||||
|
||||
1. During the installation of Archipelago, you will have been asked to install the SNI Client. If you did not do this,
|
||||
or you are on an older version, you may run the installer again to install the SNI Client.
|
||||
2. During setup, you will be asked to locate your base ROM file. This is your Donkey Kong Country 3 ROM file.
|
||||
3. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
|
||||
files.
|
||||
1. Extract your emulator's folder to your Desktop, or somewhere you will remember.
|
||||
2. Right-click on a ROM file and select **Open with...**
|
||||
3. Check the box next to **Always use this app to open .sfc files**
|
||||
4. Scroll to the bottom of the list and click the grey text **Look for another App on this PC**
|
||||
5. Browse for your emulator's `.exe` file and click **Open**. This file should be located inside the folder you
|
||||
extracted in step one.
|
||||
|
||||
## Create a Config (.yaml) File
|
||||
|
||||
### What is a config file and why do I need one?
|
||||
|
||||
See the guide on setting up a basic YAML at the Archipelago setup
|
||||
guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
|
||||
|
||||
### Where do I get a config file?
|
||||
|
||||
The Player Settings page on the website allows you to configure your personal settings and export a config file from
|
||||
them. Player settings page: [Donkey Kong Country 3 Player Settings Page](/games/Donkey%20Kong%20Country%203/player-settings)
|
||||
|
||||
### Verifying your config file
|
||||
|
||||
If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML
|
||||
validator page: [YAML Validation page](/mysterycheck)
|
||||
|
||||
## Generating a Single-Player Game
|
||||
|
||||
1. Navigate to the Player Settings page, configure your options, and click the "Generate Game" button.
|
||||
- Player Settings page: [Donkey Kong Country 3 Player Settings Page](/games/Donkey%20Kong%20Country%203/player-settings)
|
||||
2. You will be presented with a "Seed Info" page.
|
||||
3. Click the "Create New Room" link.
|
||||
4. You will be presented with a server page, from which you can download your patch file.
|
||||
5. Double-click on your patch file, and the Donkey Kong Country 3 Client will launch automatically, create your ROM from the
|
||||
patch file, and open your emulator for you.
|
||||
6. Since this is a single-player game, you will no longer need the client, so feel free to close it.
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
### Obtain your patch file and create your ROM
|
||||
|
||||
When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that is done,
|
||||
the host will provide you with either a link to download your patch file, or with a zip file containing everyone's patch
|
||||
files. Your patch file should have a `.apsm` extension.
|
||||
|
||||
Put your patch file on your desktop or somewhere convenient, and double click it. This should automatically launch the
|
||||
client, and will also create your ROM in the same place as your patch file.
|
||||
|
||||
### Connect to the client
|
||||
|
||||
#### With an emulator
|
||||
|
||||
When the client launched automatically, SNI should have also automatically launched in the background. If this is its
|
||||
first time launching, you may be prompted to allow it to communicate through the Windows Firewall.
|
||||
|
||||
##### snes9x Multitroid
|
||||
|
||||
1. Load your ROM file if it hasn't already been loaded.
|
||||
2. Click on the File menu and hover on **Lua Scripting**
|
||||
3. Click on **New Lua Script Window...**
|
||||
4. In the new window, click **Browse...**
|
||||
5. Select the connector lua file included with your client
|
||||
- Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the
|
||||
emulator is 64-bit or 32-bit.
|
||||
6. If you see an error while loading the script that states `socket.dll missing` or similar, navigate to the folder of
|
||||
the lua you are using in your file explorer and copy the `socket.dll` to the base folder of your snes9x install.
|
||||
|
||||
##### BizHawk
|
||||
|
||||
1. Ensure you have the BSNES core loaded. You may do this by clicking on the Tools menu in BizHawk and following these
|
||||
menu options:
|
||||
`Config --> Cores --> SNES --> BSNES`
|
||||
Once you have changed the loaded core, you must restart BizHawk.
|
||||
2. Load your ROM file if it hasn't already been loaded.
|
||||
3. Click on the Tools menu and click on **Lua Console**
|
||||
4. Click the button to open a new Lua script.
|
||||
5. Select the `Connector.lua` file included with your client
|
||||
- Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the
|
||||
emulator is 64-bit or 32-bit. Please note the most recent versions of BizHawk are 64-bit only.
|
||||
|
||||
##### RetroArch 1.10.3 or newer
|
||||
|
||||
You only have to do these steps once. Note, RetroArch 1.9.x will not work as it is older than 1.10.3.
|
||||
|
||||
1. Enter the RetroArch main menu screen.
|
||||
2. Go to Settings --> User Interface. Set "Show Advanced Settings" to ON.
|
||||
3. Go to Settings --> Network. Set "Network Commands" to ON. (It is found below Request Device 16.) Leave the default
|
||||
Network Command Port at 55355.
|
||||
|
||||

|
||||
4. Go to Main Menu --> Online Updater --> Core Downloader. Scroll down and select "Nintendo - SNES / SFC (bsnes-mercury
|
||||
Performance)".
|
||||
|
||||
When loading a ROM, be sure to select a **bsnes-mercury** core. These are the only cores that allow external tools to
|
||||
read ROM data.
|
||||
|
||||
#### With hardware
|
||||
|
||||
This guide assumes you have downloaded the correct firmware for your device. If you have not done so already, please do
|
||||
this now. SD2SNES and FXPak Pro users may download the appropriate firmware on the SD2SNES releases page. SD2SNES
|
||||
releases page: [SD2SNES Releases Page](https://github.com/RedGuyyyy/sd2snes/releases)
|
||||
|
||||
Other hardware may find helpful information on the usb2snes platforms
|
||||
page: [usb2snes Supported Platforms Page](http://usb2snes.com/#supported-platforms)
|
||||
|
||||
1. Close your emulator, which may have auto-launched.
|
||||
2. Power on your device and load the ROM.
|
||||
|
||||
### Connect to the Archipelago Server
|
||||
|
||||
The patch file which launched your client should have automatically connected you to the AP Server. There are a few
|
||||
reasons this may not happen however, including if the game is hosted on the website but was generated elsewhere. If the
|
||||
client window shows "Server Status: Not Connected", simply ask the host for the address of the server, and copy/paste it
|
||||
into the "Server" input field then press enter.
|
||||
|
||||
The client will attempt to reconnect to the new server address, and should momentarily show "Server Status: Connected".
|
||||
|
||||
### Play the game
|
||||
|
||||
When the client shows both SNES Device and Server as connected, you're ready to begin playing. Congratulations on
|
||||
successfully joining a multiworld game!
|
||||
|
||||
## Hosting a MultiWorld game
|
||||
|
||||
The recommended way to host a game is to use our hosting service. The process is relatively simple:
|
||||
|
||||
1. Collect config files from your players.
|
||||
2. Create a zip file containing your players' config files.
|
||||
3. Upload that zip file to the Generate page above.
|
||||
- Generate page: [WebHost Seed Generation Page](/generate)
|
||||
4. Wait a moment while the seed is generated.
|
||||
5. When the seed is generated, you will be redirected to a "Seed Info" page.
|
||||
6. Click "Create New Room". This will take you to the server page. Provide the link to this page to your players, so
|
||||
they may download their patch files from there.
|
||||
7. Note that a link to a MultiWorld Tracker is at the top of the room page. The tracker shows the progress of all
|
||||
players in the game. Any observers may also be given the link to this page.
|
||||
8. Once all players have joined, you may begin playing.
|
||||
@@ -501,6 +501,7 @@ def get_science_pack_pools() -> Dict[str, Set[str]]:
|
||||
item_stack_sizes: Dict[str, int] = items_future.result()
|
||||
non_stacking_items: Set[str] = {item for item, stack in item_stack_sizes.items() if stack == 1}
|
||||
stacking_items: Set[str] = set(item_stack_sizes) - non_stacking_items
|
||||
valid_ingredients: Set[str] = stacking_items | fluids
|
||||
|
||||
# cleanup async helpers
|
||||
pool.shutdown()
|
||||
|
||||
@@ -8,7 +8,7 @@ from .Technologies import base_tech_table, recipe_sources, base_technology_table
|
||||
all_ingredient_names, all_product_sources, required_technologies, get_rocket_requirements, \
|
||||
progressive_technology_table, common_tech_table, tech_to_progressive_lookup, progressive_tech_table, \
|
||||
get_science_pack_pools, Recipe, recipes, technology_table, tech_table, factorio_base_id, useless_technologies, \
|
||||
fluids, stacking_items
|
||||
fluids, stacking_items, valid_ingredients
|
||||
from .Shapes import get_shapes
|
||||
from .Mod import generate_mod
|
||||
from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal
|
||||
@@ -221,7 +221,7 @@ class Factorio(World):
|
||||
# Return the liquid to the pool and get a new ingredient.
|
||||
pool.append(new_ingredient)
|
||||
new_ingredient = pool.pop(0)
|
||||
liquids_used += 1
|
||||
liquids_used += 1 if new_ingredient in fluids else 0
|
||||
new_ingredients[new_ingredient] = 1
|
||||
return Recipe(original.name, self.get_category(original.category, liquids_used), new_ingredients,
|
||||
original.products, original.energy)
|
||||
@@ -231,7 +231,7 @@ class Factorio(World):
|
||||
"""Generate a recipe from pool with time and cost similar to original * factor"""
|
||||
new_ingredients = {}
|
||||
# have to first sort for determinism, while filtering out non-stacking items
|
||||
pool: typing.List[str] = sorted(pool & stacking_items)
|
||||
pool: typing.List[str] = sorted(pool & valid_ingredients)
|
||||
# then sort with random data to shuffle
|
||||
self.world.random.shuffle(pool)
|
||||
target_raw = int(sum((count for ingredient, count in original.base_cost.items())) * factor)
|
||||
@@ -329,10 +329,8 @@ class Factorio(World):
|
||||
def set_custom_recipes(self):
|
||||
original_rocket_part = recipes["rocket-part"]
|
||||
science_pack_pools = get_science_pack_pools()
|
||||
valid_pool = sorted(science_pack_pools[self.world.max_science_pack[self.player].get_max_pack()])
|
||||
valid_pool = sorted(science_pack_pools[self.world.max_science_pack[self.player].get_max_pack()] & valid_ingredients)
|
||||
self.world.random.shuffle(valid_pool)
|
||||
while any([valid_pool[x] in fluids for x in range(3)]):
|
||||
self.world.random.shuffle(valid_pool)
|
||||
self.custom_recipes = {"rocket-part": Recipe("rocket-part", original_rocket_part.category,
|
||||
{valid_pool[x]: 10 for x in range(3)},
|
||||
original_rocket_part.products,
|
||||
|
||||
@@ -1,6 +1,48 @@
|
||||
{% from "macros.lua" import dict_to_recipe %}
|
||||
-- this file gets written automatically by the Archipelago Randomizer and is in its raw form a Jinja2 Template
|
||||
require('lib')
|
||||
data.raw["rocket-silo"]["rocket-silo"].fluid_boxes = {
|
||||
{
|
||||
production_type = "input",
|
||||
pipe_picture = assembler2pipepictures(),
|
||||
pipe_covers = pipecoverspictures(),
|
||||
base_area = 10,
|
||||
base_level = -1,
|
||||
pipe_connections = {
|
||||
{ type = "input", position = { 0, 5 } },
|
||||
{ type = "input", position = { 0, -5 } },
|
||||
{ type = "input", position = { 5, 0 } },
|
||||
{ type = "input", position = { -5, 0 } }
|
||||
}
|
||||
},
|
||||
{
|
||||
production_type = "input",
|
||||
pipe_picture = assembler2pipepictures(),
|
||||
pipe_covers = pipecoverspictures(),
|
||||
base_area = 10,
|
||||
base_level = -1,
|
||||
pipe_connections = {
|
||||
{ type = "input", position = { -3, 5 } },
|
||||
{ type = "input", position = { -3, -5 } },
|
||||
{ type = "input", position = { 5, -3 } },
|
||||
{ type = "input", position = { -5, -3 } }
|
||||
}
|
||||
},
|
||||
{
|
||||
production_type = "input",
|
||||
pipe_picture = assembler2pipepictures(),
|
||||
pipe_covers = pipecoverspictures(),
|
||||
base_area = 10,
|
||||
base_level = -1,
|
||||
pipe_connections = {
|
||||
{ type = "input", position = { 3, 5 } },
|
||||
{ type = "input", position = { 3, -5 } },
|
||||
{ type = "input", position = { 5, 3 } },
|
||||
{ type = "input", position = { -5, 3 } }
|
||||
}
|
||||
},
|
||||
off_when_no_fluid_recipe = true
|
||||
}
|
||||
|
||||
{%- for recipe_name, recipe in custom_recipes.items() %}
|
||||
data.raw["recipe"]["{{recipe_name}}"].category = "{{recipe.category}}"
|
||||
|
||||
@@ -1 +1 @@
|
||||
factorio-rcon-py>=1.2.1
|
||||
factorio-rcon-py>=2.0.1
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from typing import Dict
|
||||
from BaseClasses import Item, Location, MultiWorld, Tutorial
|
||||
from BaseClasses import Item, Location, MultiWorld, Tutorial, ItemClassification
|
||||
from .Items import ItemData, FF1Items, FF1_STARTER_ITEMS, FF1_PROGRESSION_LIST, FF1_BRIDGE
|
||||
from .Locations import EventId, FF1Locations, generate_rule, CHAOS_TERMINATED_EVENT
|
||||
from .Options import ff1_options
|
||||
@@ -55,7 +55,7 @@ class FF1World(World):
|
||||
rules = get_options(self.world, 'rules', self.player)
|
||||
menu_region = self.ff1_locations.create_menu_region(self.player, locations, rules)
|
||||
terminated_event = Location(self.player, CHAOS_TERMINATED_EVENT, EventId, menu_region)
|
||||
terminated_item = Item(CHAOS_TERMINATED_EVENT, True, EventId, self.player)
|
||||
terminated_item = Item(CHAOS_TERMINATED_EVENT, ItemClassification.progression, EventId, self.player)
|
||||
terminated_event.place_locked_item(terminated_item)
|
||||
|
||||
items = get_options(self.world, 'items', self.player)
|
||||
@@ -114,5 +114,6 @@ class FF1World(World):
|
||||
def get_filler_item_name(self) -> str:
|
||||
return self.world.random.choice(["Heal", "Pure", "Soft", "Tent", "Cabin", "House"])
|
||||
|
||||
|
||||
def get_options(world: MultiWorld, name: str, player: int):
|
||||
return getattr(world, name, None)[player].value
|
||||
|
||||
@@ -15,6 +15,8 @@ class GenericWeb(WebWorld):
|
||||
commands = Tutorial('Archipelago Server and Client Commands',
|
||||
'A guide detailing the commands available to the user when participating in an Archipelago session.',
|
||||
'English', 'commands_en.md', 'commands/en', ['jat2980', 'Ijwu'])
|
||||
mac = Tutorial('Archipelago Setup Guide for Mac', 'A guide detailing how to run Archipelago clients on macOS.',
|
||||
'English', 'mac_en.md','mac/en', ['Bicoloursnake'])
|
||||
plando = Tutorial('Archipelago Plando Guide', 'A guide to understanding and using plando for your game.',
|
||||
'English', 'plando_en.md', 'plando/en', ['alwaysintreble', 'Alchav'])
|
||||
setup = Tutorial('Multiworld Setup Tutorial',
|
||||
@@ -25,7 +27,7 @@ class GenericWeb(WebWorld):
|
||||
using_website = Tutorial('Archipelago Website User Guide',
|
||||
'A guide to using the Archipelago website to generate multiworlds or host pre-generated multiworlds.',
|
||||
'English', 'using_website_en.md', 'using_website/en', ['alwaysintreble'])
|
||||
tutorials = [setup, using_website, commands, advanced_settings, triggers, plando]
|
||||
tutorials = [setup, using_website, mac, commands, advanced_settings, triggers, plando]
|
||||
|
||||
|
||||
class GenericWorld(World):
|
||||
|
||||
@@ -52,7 +52,7 @@ For `nested_option_two`, `option_two_setting_one` will be rolled 14 times and `o
|
||||
times against each other. This means `option_two_setting_two` will be more likely to occur, but it isn't guaranteed,
|
||||
adding more randomness and "mystery" to your settings. Every configurable setting supports weights.
|
||||
|
||||
### Root Options
|
||||
## Root Options
|
||||
|
||||
Currently, there are only a few options that are root options. Everything else should be nested within one of these root
|
||||
options or in some cases nested within other nested options. The only options that should exist in root
|
||||
@@ -95,14 +95,14 @@ games you want settings for.
|
||||
more triggers in the triggers guide. Triggers
|
||||
guide: [Archipelago Triggers Guide](/tutorial/Archipelago/triggers/en)
|
||||
|
||||
### Game Options
|
||||
## Game Options
|
||||
|
||||
One of your root settings will be the name of the game you would like to populate with settings. Since it is possible to
|
||||
give a weight to any option it is possible to have one file that can generate a seed for you where you don't know which
|
||||
game you'll play. For these cases you'll want to fill the game options for every game that can be rolled by these
|
||||
settings. If a game can be rolled it **must** have a settings section even if it is empty.
|
||||
|
||||
#### Universal Game Options
|
||||
### Universal Game Options
|
||||
|
||||
Some options in Archipelago can be used by every game but must still be placed within the relevant game's section.
|
||||
|
||||
@@ -174,6 +174,8 @@ A Link to the Past:
|
||||
- Moon Pearl
|
||||
start_location_hints:
|
||||
- Spike Cave
|
||||
priority_locations:
|
||||
- Link's House
|
||||
exclude_locations:
|
||||
- Cave 45
|
||||
item_links:
|
||||
@@ -207,9 +209,10 @@ Timespinner:
|
||||
|
||||
* `description` gives us a general overview so if we pull up this file later we can understand the intent.
|
||||
* `name` is `Example Player` and this will be used in the server console when sending and receiving items.
|
||||
* `game` has an equal chance of being either `A Link to the Past` or `Timespinner` with a 10/20 chance for each. The reason for this is becuase each game has a weight of 10 and the toal of all weights is 20.
|
||||
* `game` has an equal chance of being either `A Link to the Past` or `Timespinner` with a 10/20 chance for each. This is
|
||||
because each game has a weight of 10 and the total of all weights is 20.
|
||||
* `requires` is set to required release version 0.3.2 or higher.
|
||||
* `accesibility` is set to `none` which will set this seed to beatable only meaning some locations and items may be
|
||||
* `accessibility` is set to `none` which will set this seed to beatable only, so some locations and items may be
|
||||
completely inaccessible but the seed will still be completable.
|
||||
* `progression_balancing` is set on, giving it the default value, meaning we will likely receive important items
|
||||
earlier increasing the chance of having things to do.
|
||||
@@ -225,8 +228,8 @@ Timespinner:
|
||||
1 and 7 will be chosen at random, weighted towards a high number.
|
||||
* `start_inventory` defines an area for us to determine what items we would like to start the seed with. For this
|
||||
example we have:
|
||||
* `Pegasus Boots: 1` which gives us 1 copy of the Pegasus Boots
|
||||
* `Bombs (3)` gives us 2 packs of 3 bombs or 6 total bombs
|
||||
* `Pegasus Boots: 1` which gives us 1 copy of the Pegasus Boots
|
||||
* `Bombs (3): 2` gives us 2 packs of 3 bombs or 6 total bombs
|
||||
* `start_hints` gives us a starting hint for the hammer available at the beginning of the multiworld which we can use
|
||||
with no cost.
|
||||
* `local_items` forces the `Bombos`, `Ether`, and `Quake` medallions to all be placed within our own world, meaning we
|
||||
@@ -234,22 +237,19 @@ Timespinner:
|
||||
* `non_local_items` forces the `Moon Pearl` to be placed in someone else's world, meaning we won't be able to find it.
|
||||
* `start_location_hints` gives us a starting hint for the `Spike Cave` location available at the beginning of the
|
||||
multiworld that can be used for no cost.
|
||||
* `priority_locations` forces a progression item to be placed on the `Link's House` location.
|
||||
* `exclude_locations` forces a not important item to be placed on the `Cave 45` location.
|
||||
* `item_links`
|
||||
* For `A Link to the Past` all players in the `rods` item link group will share their fire and ice rods and the player
|
||||
items will be replaced with single rupees.
|
||||
* For `Timespinner` all players in the `TSAll` item link group will share their entire item pool and the`Twin Pyramid
|
||||
* For `A Link to the Past` all players in the `rods` item link group will share their fire and ice rods and the player
|
||||
items will be replaced with single rupees.
|
||||
items will be replaced with single rupees.
|
||||
* For `Timespinner` all players in the `TSAll` item link group will share their entire item pool and the `Twin Pyramid
|
||||
Key` and `Timespinner Wheel` will be forced among the worlds of those in the group. The `null` replacement item will, instead
|
||||
of forcing a specific chosen item, allow the generator to randomly pick a filler item in place of putting in another one of the linked item.
|
||||
Key` and `Timespinner Wheel` will be forced among the worlds of those in the group. The `null` replacement item will,
|
||||
instead of forcing a specific chosen item, allow the generator to randomly pick a filler item to replace the player items.
|
||||
* `triggers` allows us to define a trigger such that if our `smallkey_shuffle` option happens to roll the `any_world`
|
||||
result it will also ensure that `bigkey_shuffle`, `map_shuffle`, and `compass_shuffle` are also forced to
|
||||
the `any_world`
|
||||
result.
|
||||
result it will also ensure that `bigkey_shuffle`, `map_shuffle`, and `compass_shuffle` are also forced to the
|
||||
`any_world` result. More information on triggers can be found in the [triggers guide](/tutorial/Archipelago/triggers/en).
|
||||
|
||||
### Generating Multiple Worlds
|
||||
## Generating Multiple Worlds
|
||||
|
||||
YAML files can be configured to generate multiple worlds using only one file. This is mostly useful if you are playing an asynchronous multiworld (shortened to async) and are wanting to submit multiple worlds as they can be condensed into one file, removing the need to manage separate files if one chooses to do so.
|
||||
|
||||
@@ -257,7 +257,7 @@ As a precautionary measure, before submitting a multi-game yaml like this one in
|
||||
|
||||
To configure your file to generate multiple worlds, use 3 dashes `---` on an empty line to separate the ending of one world and the beginning of another world.
|
||||
|
||||
#### Example
|
||||
### Example
|
||||
|
||||
```yaml
|
||||
description: Example of generating multiple worlds. World 1 of 3
|
||||
@@ -272,7 +272,7 @@ Super Mario 64:
|
||||
StrictCapRequirements: true
|
||||
StrictCannonRequirements: true
|
||||
StarsToFinish: 70
|
||||
ExtraStars: 30
|
||||
AmountOfStars: 70
|
||||
DeathLink: true
|
||||
BuddyChecks: true
|
||||
AreaRandomizer: true
|
||||
|
||||
32
worlds/generic/docs/mac_en.md
Normal file
32
worlds/generic/docs/mac_en.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Guide to Run Archipelago from Source Code on macOS
|
||||
Archipelago does not have a compiled release on macOS. However, it is possible to run from source code on macOS. This guide expects you to have some experience with running software from the terminal.
|
||||
## Prerequisite Software
|
||||
Here is a list of software to install and source code to download.
|
||||
1. Python 3.8 or newer from the [macOS Python downloads page](https://www.python.org/downloads/macos/).
|
||||
2. Xcode from the [macOS App Store](https://apps.apple.com/us/app/xcode/id497799835).
|
||||
3. The source code from the [Archipelago releases page](https://github.com/ArchipelagoMW/Archipelago/releases).
|
||||
4. The asset with darwin in the name from the [SNI Github releases page](https://github.com/alttpo/sni/releases).
|
||||
5. If you would like to generate Enemized seeds for ALTTP locally (not on the website), you may need the EnemizerCLI from its [Github releases page](https://github.com/Ijwu/Enemizer/releases).
|
||||
6. An Emulator of your choosing for games that need an emulator. For SNES games, I recommend RetroArch, entirely because it was the easiest for me to setup on macOS. It can be downloaded at the [RetroArch downloads page](https://www.retroarch.com/?page=platforms)
|
||||
## Extracting the Archipelago Directory
|
||||
1. Double click on the Archipelago source code zip file to extract the files to an Archipelago directory.
|
||||
2. Move this Archipelago directory out of your downloads directory.
|
||||
3. Open terminal and navigate to your Archipelago directory.
|
||||
## Setting up a Virtual Environment
|
||||
It is generally recommended that you use a virtual environment to run python based software to avoid contamination that can break some software. If Archipelago is the only piece of software you use that runs from python source code however, it is not necessary to use a virtual environment.
|
||||
1. Open terminal and navigate to the Archipelago directory.
|
||||
2. Run the command `python3 -m venv venv` to create a virtual environment. Running this command will create a new directory at the specified path, so make sure that path is clear for a new directory to be created.
|
||||
3. Run the command `source venv/bin/activate` to activate the virtual environment.
|
||||
4. If you want to exit the virtual environment, run the command `deactivate`.
|
||||
## Steps to Run the Clients
|
||||
1. If your game doesn't have a patch file, run the command `python3 SNIClient.py`, changing the filename with the file of the client you want to run.
|
||||
2. If your game does have a patch file, move the base rom to the Archipelago directory and run the command `python3 SNIClient.py 'patchfile'` with the filename extension for the patch file (apsm, aplttp, apsmz3, etc.) included and changing the filename with the file of the client you want to run.
|
||||
3. Your client should now be running and rom created (where applicable).
|
||||
## Additional Steps for SNES Games
|
||||
1. If using RetroArch, the instructions to set up your emulator [here in the Link to the Past setup guide](https://archipelago.gg/tutorial/A%20Link%20to%20the%20Past/multiworld/en) also work on the macOS version of RetroArch.
|
||||
2. Double click on the SNI tar.gz download to extract the files to an SNI directory. If it isn't already, rename this directory to SNI to make some steps easier.
|
||||
3. Move the SNI directory out of the downloads directory, preferably into the Archipelago directory created earlier.
|
||||
4. If the SNI directory is correctly named and moved into the Archipelago directory, it should auto run with the SNI client. If it doesn't automatically run, open up the SNI directory and run the SNI executable file manually.
|
||||
5. If using EnemizerCLI, extract that downloaded directory and rename it to EnemizerCLI.
|
||||
6. Move the EnemizerCLI directory into the Archipelago directory so that Generate.py can take advantage of it.
|
||||
7. Now that SNI, the client, and the emulator are all running, you should be good to go.
|
||||
File diff suppressed because one or more lines are too long
@@ -74,7 +74,7 @@ class Absorber(ast.NodeTransformer):
|
||||
self.truth_values = truth_values
|
||||
self.truth_values |= {"True", "None", "ANY", "ITEMRANDO"}
|
||||
self.false_values = false_values
|
||||
self.false_values |= {"False", "NONE", "RANDOMELEVATORS"}
|
||||
self.false_values |= {"False", "NONE"}
|
||||
|
||||
super(Absorber, self).__init__()
|
||||
|
||||
@@ -203,7 +203,58 @@ logic_folder = os.path.join(resources_source, "Logic")
|
||||
logic_options: typing.Dict[str, str] = hk_loads(os.path.join(data_folder, "logic_settings.json"))
|
||||
for logic_key, logic_value in logic_options.items():
|
||||
logic_options[logic_key] = logic_value.split(".", 1)[-1]
|
||||
del (logic_options["RANDOMELEVATORS"])
|
||||
|
||||
vanilla_cost_data: typing.Dict[str, typing.Dict[str, typing.Any]] = hk_loads(os.path.join(data_folder, "costs.json"))
|
||||
vanilla_location_costs = {
|
||||
key: {
|
||||
value["term"]: int(value["amount"])
|
||||
}
|
||||
for key, value in vanilla_cost_data.items()
|
||||
if value["amount"] > 0 and value["term"] == "GEO"
|
||||
}
|
||||
|
||||
salubra_geo_costs_by_charm_count = {
|
||||
5: 120,
|
||||
10: 500,
|
||||
18: 900,
|
||||
25: 1400,
|
||||
40: 800
|
||||
}
|
||||
|
||||
# Can't extract this data, so supply it ourselves. Source: the wiki
|
||||
vanilla_shop_costs = {
|
||||
('Sly', 'Simple_Key'): [{'GEO': 950}],
|
||||
('Sly', 'Rancid_Egg'): [{'GEO': 60}],
|
||||
('Sly', 'Lumafly_Lantern'): [{'GEO': 1800}],
|
||||
('Sly', 'Gathering_Swarm'): [{'GEO': 300}],
|
||||
('Sly', 'Stalwart_Shell'): [{'GEO': 200}],
|
||||
('Sly', 'Mask_Shard'): [
|
||||
{'GEO': 150},
|
||||
{'GEO': 500},
|
||||
],
|
||||
('Sly', 'Vessel_Fragment'): [{'GEO': 550}],
|
||||
('Sly_(Key)', 'Heavy_Blow'): [{'GEO': 350}],
|
||||
('Sly_(Key)', 'Elegant_Key'): [{'GEO': 800}],
|
||||
('Sly_(Key)', 'Mask_Shard'): [
|
||||
{'GEO': 800},
|
||||
{'GEO': 1500},
|
||||
],
|
||||
('Sly_(Key)', 'Vessel_Fragment'): [{'GEO': 900}],
|
||||
('Sly_(Key)', 'Sprintmaster'): [{'GEO': 400}],
|
||||
|
||||
('Iselda', 'Wayward_Compass'): [{'GEO': 220}],
|
||||
('Iselda', 'Quill'): [{'GEO': 120}],
|
||||
|
||||
('Salubra', 'Lifeblood_Heart'): [{'GEO': 250}],
|
||||
('Salubra', 'Longnail'): [{'GEO': 300}],
|
||||
('Salubra', 'Steady_Body'): [{'GEO': 120}],
|
||||
('Salubra', 'Shaman_Stone'): [{'GEO': 220}],
|
||||
('Salubra', 'Quick_Focus'): [{'GEO': 800}],
|
||||
|
||||
('Leg_Eater', 'Fragile_Heart'): [{'GEO': 350}],
|
||||
('Leg_Eater', 'Fragile_Greed'): [{'GEO': 250}],
|
||||
('Leg_Eater', 'Fragile_Strength'): [{'GEO': 600}],
|
||||
}
|
||||
extra_pool_options: typing.List[typing.Dict[str, typing.Any]] = hk_loads(os.path.join(data_folder, "pools.json"))
|
||||
pool_options: typing.Dict[str, typing.Tuple[typing.List[str], typing.List[str]]] = {}
|
||||
for option in extra_pool_options:
|
||||
@@ -213,8 +264,23 @@ for option in extra_pool_options:
|
||||
for pairing in option["Vanilla"]:
|
||||
items.append(pairing["item"])
|
||||
location_name = pairing["location"]
|
||||
if any(cost_entry["term"] == "CHARMS" for cost_entry in pairing.get("costs", [])):
|
||||
location_name += "_(Requires_Charms)"
|
||||
item_costs = pairing.get("costs", [])
|
||||
if item_costs:
|
||||
if any(cost_entry["term"] == "CHARMS" for cost_entry in item_costs):
|
||||
location_name += "_(Requires_Charms)"
|
||||
#vanilla_shop_costs[pairing["location"], pairing["item"]] = \
|
||||
cost = {
|
||||
entry["term"]: int(entry["amount"]) for entry in item_costs
|
||||
}
|
||||
# Rando4 doesn't include vanilla geo costs for Salubra charms, so dirty hardcode here.
|
||||
if 'CHARMS' in cost:
|
||||
geo = salubra_geo_costs_by_charm_count.get(cost['CHARMS'])
|
||||
if geo:
|
||||
cost['GEO'] = geo
|
||||
|
||||
key = (pairing["location"], pairing["item"])
|
||||
vanilla_shop_costs.setdefault(key, []).append(cost)
|
||||
|
||||
locations.append(location_name)
|
||||
if option["Path"]:
|
||||
# basename carries over from prior entry if no Path given
|
||||
@@ -229,6 +295,12 @@ for option in extra_pool_options:
|
||||
pool_options[basename] = items, locations
|
||||
del extra_pool_options
|
||||
|
||||
# reverse all the vanilla shop costs (really, this is just for Salubra).
|
||||
# When we use these later, we pop off the end of the list so this ensures they are still sorted.
|
||||
vanilla_shop_costs = {
|
||||
k: list(reversed(v)) for k, v in vanilla_shop_costs.items()
|
||||
}
|
||||
|
||||
# items
|
||||
items: typing.Dict[str, typing.Dict] = hk_loads(os.path.join(data_folder, "items.json"))
|
||||
logic_items: typing.Set[str] = set()
|
||||
@@ -364,9 +436,15 @@ for event in events:
|
||||
event_rules.update(connectors_rules)
|
||||
connectors_rules = {}
|
||||
|
||||
|
||||
# Apply some final fixes
|
||||
item_effects.update({
|
||||
'Left_Mothwing_Cloak': {'LEFTDASH': 1},
|
||||
'Right_Mothwing_Cloak': {'RIGHTDASH': 1},
|
||||
})
|
||||
names = sorted({"logic_options", "starts", "pool_options", "locations", "multi_locations", "location_to_region_lookup",
|
||||
"event_names", "item_effects", "items", "logic_items", "region_names",
|
||||
"exits", "connectors", "one_ways"})
|
||||
"exits", "connectors", "one_ways", "vanilla_shop_costs", "vanilla_location_costs"})
|
||||
warning = "# This module is written by Extractor.py, do not edit manually!.\n\n"
|
||||
with open(os.path.join(os.path.dirname(__file__), "ExtractedData.py"), "wt") as py:
|
||||
py.write(warning)
|
||||
@@ -385,6 +463,6 @@ rules_template = template_env.get_template("RulesTemplate.pyt")
|
||||
rules = rules_template.render(location_rules=location_rules, one_ways=one_ways, connectors_rules=connectors_rules,
|
||||
event_rules=event_rules)
|
||||
|
||||
with open("Rules.py", "wt") as py:
|
||||
with open("GeneratedRules.py", "wt") as py:
|
||||
py.write(warning)
|
||||
py.write(rules)
|
||||
|
||||
1699
worlds/hk/GeneratedRules.py
Normal file
1699
worlds/hk/GeneratedRules.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
import typing
|
||||
from .ExtractedData import logic_options, starts, pool_options
|
||||
from .Rules import cost_terms
|
||||
|
||||
from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, SpecialRange
|
||||
from .Charms import vanilla_costs, names as charm_names
|
||||
@@ -11,19 +12,6 @@ else:
|
||||
Random = typing.Any
|
||||
|
||||
|
||||
class Disabled(Toggle):
|
||||
def __init__(self, value: int):
|
||||
super(Disabled, self).__init__(0)
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, text: str) -> Toggle:
|
||||
return cls(0)
|
||||
|
||||
@classmethod
|
||||
def from_any(cls, data: typing.Any):
|
||||
return cls(0)
|
||||
|
||||
|
||||
locations = {"option_" + start: i for i, start in enumerate(starts)}
|
||||
# This way the dynamic start names are picked up by the MetaClass Choice belongs to
|
||||
StartLocation = type("StartLocation", (Choice,), {"__module__": __name__, "auto_display_name": False, **locations,
|
||||
@@ -36,6 +24,8 @@ option_docstrings = {
|
||||
"randomization.",
|
||||
"RandomizeSkills": "Allow for Skills, such as Mantis Claw or Shade Soul, to be randomized into the item pool. "
|
||||
"Also opens their locations for receiving randomized items.",
|
||||
"RandomizeFocus": "Removes the ability to focus and randomizes it into the item pool.",
|
||||
"RandomizeSwim": "Removes the ability to swim in water and randomizes it into the item pool.",
|
||||
"RandomizeCharms": "Allow for Charms to be randomized into the item pool and open their locations for "
|
||||
"randomization. Includes Charms sold in shops.",
|
||||
"RandomizeKeys": "Allow for Keys to be randomized into the item pool. Includes those sold in shops.",
|
||||
@@ -59,6 +49,8 @@ option_docstrings = {
|
||||
"RandomizeBossEssence": "Randomize boss essence drops, such as those for defeating Warrior Dreams, into the item "
|
||||
"pool and open their locations for randomization.",
|
||||
"RandomizeGrubs": "Randomize Grubs into the item pool and open their locations for randomization.",
|
||||
"RandomizeMimics": "Randomize Mimic Grubs into the item pool and open their locations for randomization."
|
||||
"Mimic Grubs are always placed in your own game.",
|
||||
"RandomizeMaps": "Randomize Maps into the item pool. This causes Cornifer to give you a message allowing you to see"
|
||||
" and buy an item that is randomized into that location as well.",
|
||||
"RandomizeStags": "Randomize Stag Stations unlocks into the item pool as well as placing randomized items "
|
||||
@@ -70,6 +62,7 @@ option_docstrings = {
|
||||
"RandomizeJournalEntries": "Randomize the Hunter's Journal as well as the findable journal entries into the item "
|
||||
"pool, and open their locations for randomization. Does not include journal entries "
|
||||
"gained by killing enemies.",
|
||||
"RandomizeNail": "Removes the ability to swing the nail left, right and up, and shuffles these into the item pool.",
|
||||
"RandomizeGeoRocks": "Randomize Geo Rock rewards into the item pool and open their locations for randomization.",
|
||||
"RandomizeBossGeo": "Randomize boss Geo drops into the item pool and open those locations for randomization.",
|
||||
"RandomizeSoulTotems": "Randomize Soul Refill items into the item pool and open the Soul Totem locations for"
|
||||
@@ -110,12 +103,16 @@ default_on = {
|
||||
"RandomizeRelics"
|
||||
}
|
||||
|
||||
# not supported at this time
|
||||
disabled = {
|
||||
"RandomizeFocus",
|
||||
"RandomizeSwim",
|
||||
"RandomizeMimics",
|
||||
"RandomizeNail",
|
||||
shop_to_option = {
|
||||
"Seer": "SeerRewardSlots",
|
||||
"Grubfather": "GrubfatherRewardSlots",
|
||||
"Sly": "SlyShopSlots",
|
||||
"Sly_(Key)": "SlyKeyShopSlots",
|
||||
"Iselda": "IseldaShopSlots",
|
||||
"Salubra": "SalubraShopSlots",
|
||||
"Leg_Eater": "LegEaterShopSlots",
|
||||
"Salubra_(Requires_Charms)": "IseldaShopSlots",
|
||||
"Egg_Shop": "EggShopSlots",
|
||||
}
|
||||
|
||||
hollow_knight_randomize_options: typing.Dict[str, type(Option)] = {}
|
||||
@@ -124,9 +121,6 @@ for option_name, option_data in pool_options.items():
|
||||
extra_data = {"__module__": __name__, "items": option_data[0], "locations": option_data[1]}
|
||||
if option_name in option_docstrings:
|
||||
extra_data["__doc__"] = option_docstrings[option_name]
|
||||
if option_name in disabled:
|
||||
extra_data["__doc__"] = "Disabled Option. Not implemented."
|
||||
option = type(option_name, (Disabled,), extra_data)
|
||||
if option_name in default_on:
|
||||
option = type(option_name, (DefaultOnToggle,), extra_data)
|
||||
else:
|
||||
@@ -142,13 +136,36 @@ for option_name in logic_options.values():
|
||||
if option_name in option_docstrings:
|
||||
extra_data["__doc__"] = option_docstrings[option_name]
|
||||
option = type(option_name, (Toggle,), extra_data)
|
||||
if option_name in disabled:
|
||||
extra_data["__doc__"] = "Disabled Option. Not implemented."
|
||||
option = type(option_name, (Disabled,), extra_data)
|
||||
globals()[option.__name__] = option
|
||||
hollow_knight_logic_options[option.__name__] = option
|
||||
|
||||
|
||||
class RandomizeElevatorPass(Toggle):
|
||||
"""Adds an Elevator Pass item to the item pool, which is then required to use the large elevators connecting
|
||||
City of Tears to the Forgotten Crossroads and Resting Grounds."""
|
||||
display_name = "Randomize Elevator Pass"
|
||||
default = False
|
||||
|
||||
|
||||
class SplitMothwingCloak(Toggle):
|
||||
"""Splits the Mothwing Cloak into left- and right-only versions of the item. Randomly adds a second left or
|
||||
right Mothwing cloak item which functions as the upgrade to Shade Cloak."""
|
||||
display_name = "Split Mothwing Cloak"
|
||||
default = False
|
||||
|
||||
|
||||
class SplitMantisClaw(Toggle):
|
||||
"""Splits the Mantis Claw into left- and right-only versions of the item."""
|
||||
display_name = "Split Mantis Claw"
|
||||
default = False
|
||||
|
||||
|
||||
class SplitCrystalHeart(Toggle):
|
||||
"""Splits the Crystal Heart into left- and right-only versions of the item."""
|
||||
display_name = "Split Crystal Heart"
|
||||
default = False
|
||||
|
||||
|
||||
class MinimumGrubPrice(Range):
|
||||
"""The minimum grub price in the range of prices that an item should cost from Grubfather."""
|
||||
display_name = "Minimum Grub Price"
|
||||
@@ -178,7 +195,7 @@ class MaximumEssencePrice(MinimumEssencePrice):
|
||||
|
||||
|
||||
class MinimumEggPrice(Range):
|
||||
"""The minimum rancid egg price in the range of prices that an item should cost from Ijii.
|
||||
"""The minimum rancid egg price in the range of prices that an item should cost from Jiji.
|
||||
Only takes effect if the EggSlotShops option is greater than 0."""
|
||||
display_name = "Minimum Egg Price"
|
||||
range_start = 1
|
||||
@@ -187,7 +204,7 @@ class MinimumEggPrice(Range):
|
||||
|
||||
|
||||
class MaximumEggPrice(MinimumEggPrice):
|
||||
"""The maximum rancid egg price in the range of prices that an item should cost from Ijii.
|
||||
"""The maximum rancid egg price in the range of prices that an item should cost from Jiji.
|
||||
Only takes effect if the EggSlotShops option is greater than 0."""
|
||||
display_name = "Maximum Egg Price"
|
||||
default = 10
|
||||
@@ -205,9 +222,26 @@ class MinimumCharmPrice(Range):
|
||||
class MaximumCharmPrice(MinimumCharmPrice):
|
||||
"""The maximum charm price in the range of prices that an item should cost for Salubra's shop item which also
|
||||
carry a charm cost."""
|
||||
display_name = "Maximum Charm Requirement"
|
||||
default = 20
|
||||
|
||||
|
||||
class MinimumGeoPrice(Range):
|
||||
"""The minimum geo price for items in geo shops."""
|
||||
display_name = "Minimum Geo Price"
|
||||
range_start = 1
|
||||
range_end = 200
|
||||
default = 1
|
||||
|
||||
|
||||
class MaximumGeoPrice(Range):
|
||||
"""The maximum geo price for items in geo shops."""
|
||||
display_name = "Maximum Geo Price"
|
||||
range_start = 1
|
||||
range_end = 2000
|
||||
default = 400
|
||||
|
||||
|
||||
class RandomCharmCosts(SpecialRange):
|
||||
"""Total Notch Cost of all Charms together. Vanilla sums to 90.
|
||||
This value is distributed among all charms in a random fashion.
|
||||
@@ -256,13 +290,91 @@ class PlandoCharmCosts(OptionDict):
|
||||
return charm_costs
|
||||
|
||||
|
||||
class SlyShopSlots(Range):
|
||||
"""For each extra slot, add a location to the Sly Shop and a filler item to the item pool."""
|
||||
|
||||
display_name = "Sly Shop Slots"
|
||||
default = 8
|
||||
range_end = 16
|
||||
|
||||
|
||||
class SlyKeyShopSlots(Range):
|
||||
"""For each extra slot, add a location to the Sly Shop (requiring Shopkeeper's Key) and a filler item to the item pool."""
|
||||
|
||||
display_name = "Sly Key Shop Slots"
|
||||
default = 6
|
||||
range_end = 16
|
||||
|
||||
|
||||
class IseldaShopSlots(Range):
|
||||
"""For each extra slot, add a location to the Iselda Shop and a filler item to the item pool."""
|
||||
|
||||
display_name = "Iselda Shop Slots"
|
||||
default = 2
|
||||
range_end = 16
|
||||
|
||||
|
||||
class SalubraShopSlots(Range):
|
||||
"""For each extra slot, add a location to the Salubra Shop, and a filler item to the item pool."""
|
||||
|
||||
display_name = "Salubra Shop Slots"
|
||||
default = 5
|
||||
range_start = 0
|
||||
range_end = 16
|
||||
|
||||
|
||||
class SalubraCharmShopSlots(Range):
|
||||
"""For each extra slot, add a location to the Salubra Shop (requiring Charms), and a filler item to the item pool."""
|
||||
|
||||
display_name = "Salubra Charm Shop Slots"
|
||||
default = 5
|
||||
range_end = 16
|
||||
|
||||
|
||||
class LegEaterShopSlots(Range):
|
||||
"""For each extra slot, add a location to the Leg Eater Shop and a filler item to the item pool."""
|
||||
|
||||
display_name = "Leg Eater Shop Slots"
|
||||
default = 3
|
||||
range_end = 16
|
||||
|
||||
|
||||
class GrubfatherRewardSlots(Range):
|
||||
"""For each extra slot, add a location to the Grubfather and a filler item to the item pool."""
|
||||
|
||||
display_name = "Grubfather Reward Slots"
|
||||
default = 7
|
||||
range_end = 16
|
||||
|
||||
|
||||
class SeerRewardSlots(Range):
|
||||
"""For each extra slot, add a location to the Seer and a filler item to the item pool."""
|
||||
|
||||
display_name = "Seer Reward Reward Slots"
|
||||
default = 8
|
||||
range_end = 16
|
||||
|
||||
|
||||
class EggShopSlots(Range):
|
||||
"""For each slot, add a location to the Egg Shop and a Geo drop to the item pool."""
|
||||
"""For each slot, add a location to the Egg Shop and a filler item to the item pool."""
|
||||
|
||||
display_name = "Egg Shop Item Slots"
|
||||
range_end = 16
|
||||
|
||||
|
||||
class ExtraShopSlots(Range):
|
||||
"""For each extra slot, add a location to a randomly chosen shop a filler item to the item pool.
|
||||
|
||||
The Egg Shop will be excluded from this list unless it has at least one item.
|
||||
|
||||
Shops are capped at 16 items each.
|
||||
"""
|
||||
|
||||
display_name = "Additional Shop Slots"
|
||||
default = 0
|
||||
range_end = 9 * 16 # Number of shops x max slots per shop.
|
||||
|
||||
|
||||
class Goal(Choice):
|
||||
"""The goal required of you in order to complete your run in Archipelago."""
|
||||
display_name = "Goal"
|
||||
@@ -288,6 +400,25 @@ class WhitePalace(Choice):
|
||||
default = 0
|
||||
|
||||
|
||||
class DeathLink(Choice):
|
||||
"""
|
||||
When you die, everyone dies. Of course the reverse is true too.
|
||||
When enabled, choose how incoming deathlinks are handled:
|
||||
vanilla: DeathLink kills you and is just like any other death. RIP your previous shade and geo.
|
||||
shadeless: DeathLink kills you, but no shade spawns and no geo is lost. Your previous shade, if any, is untouched.
|
||||
shade: DeathLink functions like a normal death if you do not already have a shade, shadeless otherwise.
|
||||
"""
|
||||
option_off = 0
|
||||
alias_false = 0
|
||||
alias_no = 0
|
||||
alias_true = 1
|
||||
alias_on = 1
|
||||
alias_yes = 1
|
||||
option_shadeless = 1
|
||||
option_vanilla = 2
|
||||
option_shade = 3
|
||||
|
||||
|
||||
class StartingGeo(Range):
|
||||
"""The amount of starting geo you have."""
|
||||
display_name = "Starting Geo"
|
||||
@@ -296,22 +427,70 @@ class StartingGeo(Range):
|
||||
default = 0
|
||||
|
||||
|
||||
class CostSanity(Choice):
|
||||
"""If enabled, most locations with costs (like stag stations) will have randomly determined costs.
|
||||
If set to shopsonly, CostSanity will only apply to shops (including Grubfather, Seer and Egg Shop).
|
||||
If set to notshops, CostSanity will only apply to non-shops (e.g. Stag stations and Cornifer locations)
|
||||
|
||||
These costs can be in Geo (except Grubfather, Seer and Eggshop), Grubs, Charms, Essence and/or Rancid Eggs
|
||||
"""
|
||||
option_off = 0
|
||||
alias_false = 0
|
||||
alias_no = 0
|
||||
option_on = 1
|
||||
alias_true = 1
|
||||
alias_yes = 1
|
||||
option_shopsonly = 2
|
||||
option_notshops = 3
|
||||
display_name = "Cost Sanity"
|
||||
|
||||
|
||||
class CostSanityHybridChance(Range):
|
||||
"""The chance that a CostSanity cost will include two components instead of one, e.g. Grubs + Essence"""
|
||||
range_end = 100
|
||||
default = 10
|
||||
|
||||
|
||||
cost_sanity_weights: typing.Dict[str, type(Option)] = {}
|
||||
for term, cost in cost_terms.items():
|
||||
option_name = f"CostSanity{cost.option}Weight"
|
||||
extra_data = {
|
||||
"__module__": __name__, "range_end": 1000,
|
||||
"__doc__": (
|
||||
f"The likelihood of Costsanity choosing a {cost.option} cost."
|
||||
" Chosen as a sum of all weights from other types."
|
||||
),
|
||||
"default": cost.weight
|
||||
}
|
||||
if cost == 'GEO':
|
||||
extra_data["__doc__"] += " Geo costs will never be chosen for Grubfather, Seer, or Egg Shop."
|
||||
|
||||
option = type(option_name, (Range,), extra_data)
|
||||
globals()[option.__name__] = option
|
||||
cost_sanity_weights[option.__name__] = option
|
||||
|
||||
|
||||
hollow_knight_options: typing.Dict[str, type(Option)] = {
|
||||
**hollow_knight_randomize_options,
|
||||
RandomizeElevatorPass.__name__: RandomizeElevatorPass,
|
||||
**hollow_knight_logic_options,
|
||||
StartLocation.__name__: StartLocation,
|
||||
MinimumGrubPrice.__name__: MinimumGrubPrice,
|
||||
MaximumGrubPrice.__name__: MaximumGrubPrice,
|
||||
MinimumEssencePrice.__name__: MinimumEssencePrice,
|
||||
MaximumEssencePrice.__name__: MaximumEssencePrice,
|
||||
MinimumCharmPrice.__name__: MinimumCharmPrice,
|
||||
MaximumCharmPrice.__name__: MaximumCharmPrice,
|
||||
RandomCharmCosts.__name__: RandomCharmCosts,
|
||||
PlandoCharmCosts.__name__: PlandoCharmCosts,
|
||||
MinimumEggPrice.__name__: MinimumEggPrice,
|
||||
MaximumEggPrice.__name__: MaximumEggPrice,
|
||||
EggShopSlots.__name__: EggShopSlots,
|
||||
Goal.__name__: Goal,
|
||||
WhitePalace.__name__: WhitePalace,
|
||||
StartingGeo.__name__: StartingGeo,
|
||||
**{
|
||||
option.__name__: option
|
||||
for option in (
|
||||
StartLocation, Goal, WhitePalace, StartingGeo, DeathLink,
|
||||
MinimumGeoPrice, MaximumGeoPrice,
|
||||
MinimumGrubPrice, MaximumGrubPrice,
|
||||
MinimumEssencePrice, MaximumEssencePrice,
|
||||
MinimumCharmPrice, MaximumCharmPrice,
|
||||
RandomCharmCosts, PlandoCharmCosts,
|
||||
MinimumEggPrice, MaximumEggPrice, EggShopSlots,
|
||||
SlyShopSlots, SlyKeyShopSlots, IseldaShopSlots,
|
||||
SalubraShopSlots, SalubraCharmShopSlots,
|
||||
LegEaterShopSlots, GrubfatherRewardSlots,
|
||||
SeerRewardSlots, ExtraShopSlots,
|
||||
SplitCrystalHeart, SplitMothwingCloak, SplitMantisClaw,
|
||||
CostSanity, CostSanityHybridChance,
|
||||
)
|
||||
},
|
||||
**cost_sanity_weights
|
||||
}
|
||||
|
||||
1759
worlds/hk/Rules.py
1759
worlds/hk/Rules.py
File diff suppressed because it is too large
Load Diff
@@ -2,16 +2,19 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import typing
|
||||
from collections import Counter
|
||||
from copy import deepcopy
|
||||
import itertools
|
||||
import operator
|
||||
|
||||
logger = logging.getLogger("Hollow Knight")
|
||||
|
||||
from .Items import item_table, lookup_type_to_names, item_name_groups
|
||||
from .Regions import create_regions
|
||||
from .Rules import set_rules
|
||||
from .Options import hollow_knight_options, hollow_knight_randomize_options, disabled, Goal, WhitePalace
|
||||
from .Rules import set_rules, cost_terms
|
||||
from .Options import hollow_knight_options, hollow_knight_randomize_options, Goal, WhitePalace, CostSanity, \
|
||||
shop_to_option
|
||||
from .ExtractedData import locations, starts, multi_locations, location_to_region_lookup, \
|
||||
event_names, item_effects, connectors, one_ways
|
||||
event_names, item_effects, connectors, one_ways, vanilla_shop_costs, vanilla_location_costs
|
||||
from .Charms import names as charm_names
|
||||
|
||||
from BaseClasses import Region, Entrance, Location, MultiWorld, Item, RegionType, LocationProgressType, Tutorial, ItemClassification
|
||||
@@ -98,6 +101,26 @@ logicless_options = {
|
||||
"RandomizeLoreTablets", "RandomizeSoulTotems",
|
||||
}
|
||||
|
||||
# Options that affect vanilla starting items
|
||||
randomizable_starting_items: typing.Dict[str, typing.Tuple[str, ...]] = {
|
||||
"RandomizeFocus": ("Focus",),
|
||||
"RandomizeSwim": ("Swim",),
|
||||
"RandomizeNail": ('Upslash', 'Leftslash', 'Rightslash')
|
||||
}
|
||||
|
||||
# Shop cost types.
|
||||
shop_cost_types: typing.Dict[str, typing.Tuple[str, ...]] = {
|
||||
"Egg_Shop": ("RANCIDEGGS",),
|
||||
"Grubfather": ("GRUBS",),
|
||||
"Seer": ("ESSENCE",),
|
||||
"Salubra_(Requires_Charms)": ("CHARMS", "GEO"),
|
||||
"Sly": ("GEO",),
|
||||
"Sly_(Key)": ("GEO",),
|
||||
"Iselda": ("GEO",),
|
||||
"Salubra": ("GEO",),
|
||||
"Leg_Eater": ("GEO",),
|
||||
}
|
||||
|
||||
|
||||
class HKWeb(WebWorld):
|
||||
tutorials = [Tutorial(
|
||||
@@ -109,6 +132,8 @@ class HKWeb(WebWorld):
|
||||
["Ijwu"]
|
||||
)]
|
||||
|
||||
bug_report_page = "https://github.com/Ijwu/Archipelago.HollowKnight/issues/new?assignees=&labels=bug%2C+needs+investigation&template=bug_report.md&title="
|
||||
|
||||
|
||||
class HKWorld(World):
|
||||
"""Beneath the fading town of Dirtmouth sleeps a vast, ancient kingdom. Many are drawn beneath the surface,
|
||||
@@ -127,19 +152,18 @@ class HKWorld(World):
|
||||
item_name_groups = item_name_groups
|
||||
|
||||
ranges: typing.Dict[str, typing.Tuple[int, int]]
|
||||
shops: typing.Dict[str, str] = {
|
||||
"Egg_Shop": "Egg",
|
||||
"Grubfather": "Grub",
|
||||
"Seer": "Essence",
|
||||
"Salubra_(Requires_Charms)": "Charm"
|
||||
}
|
||||
charm_costs: typing.List[int]
|
||||
cached_filler_items = {}
|
||||
data_version = 2
|
||||
|
||||
def __init__(self, world, player):
|
||||
super(HKWorld, self).__init__(world, player)
|
||||
self.created_multi_locations: typing.Dict[str, int] = Counter()
|
||||
self.created_multi_locations: typing.Dict[str, typing.List[HKLocation]] = {
|
||||
location: list() for location in multi_locations
|
||||
}
|
||||
self.ranges = {}
|
||||
self.created_shop_items = 0
|
||||
self.vanilla_shop_costs = deepcopy(vanilla_shop_costs)
|
||||
|
||||
def generate_early(self):
|
||||
world = self.world
|
||||
@@ -147,16 +171,14 @@ class HKWorld(World):
|
||||
self.charm_costs = world.PlandoCharmCosts[self.player].get_costs(charm_costs)
|
||||
# world.exclude_locations[self.player].value.update(white_palace_locations)
|
||||
world.local_items[self.player].value.add("Mimic_Grub")
|
||||
for vendor, unit in self.shops.items():
|
||||
mini = getattr(world, f"Minimum{unit}Price")[self.player]
|
||||
maxi = getattr(world, f"Maximum{unit}Price")[self.player]
|
||||
for term, data in cost_terms.items():
|
||||
mini = getattr(world, f"Minimum{data.option}Price")[self.player]
|
||||
maxi = getattr(world, f"Maximum{data.option}Price")[self.player]
|
||||
# if minimum > maximum, set minimum to maximum
|
||||
mini.value = min(mini.value, maxi.value)
|
||||
self.ranges[unit] = mini.value, maxi.value
|
||||
self.ranges[term] = mini.value, maxi.value
|
||||
world.push_precollected(HKItem(starts[world.StartLocation[self.player].current_key],
|
||||
True, None, "Event", self.player))
|
||||
for option_name in disabled:
|
||||
getattr(world, option_name)[self.player].value = 0
|
||||
|
||||
def white_palace_exclusions(self):
|
||||
exclusions = set()
|
||||
@@ -199,55 +221,200 @@ class HKWorld(World):
|
||||
menu_region.locations.append(loc)
|
||||
|
||||
def create_items(self):
|
||||
unfilled_locations = 0
|
||||
# Generate item pool and associated locations (paired in HK)
|
||||
pool: typing.List[HKItem] = []
|
||||
geo_replace: typing.Set[str] = set()
|
||||
if self.world.RemoveSpellUpgrades[self.player]:
|
||||
geo_replace.add("Abyss_Shriek")
|
||||
geo_replace.add("Shade_Soul")
|
||||
geo_replace.add("Descending_Dark")
|
||||
|
||||
wp_exclusions = self.white_palace_exclusions()
|
||||
junk_replace: typing.Set[str] = set()
|
||||
if self.world.RemoveSpellUpgrades[self.player]:
|
||||
junk_replace.update(("Abyss_Shriek", "Shade_Soul", "Descending_Dark"))
|
||||
|
||||
randomized_starting_items = set()
|
||||
for attr, items in randomizable_starting_items.items():
|
||||
if getattr(self.world, attr)[self.player]:
|
||||
randomized_starting_items.update(items)
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
def _add(item_name: str, location_name: str):
|
||||
"""
|
||||
Adds a pairing of an item and location, doing appropriate checks to see if it should be vanilla or not.
|
||||
"""
|
||||
nonlocal unfilled_locations
|
||||
|
||||
vanilla = not randomized
|
||||
excluded = False
|
||||
|
||||
if not vanilla and location_name in wp_exclusions:
|
||||
if location_name == 'King_Fragment':
|
||||
excluded = True
|
||||
else:
|
||||
vanilla = True
|
||||
|
||||
if item_name in junk_replace:
|
||||
item_name = self.get_filler_item_name()
|
||||
|
||||
item = self.create_item(item_name)
|
||||
|
||||
if location_name == "Start":
|
||||
if item_name in randomized_starting_items:
|
||||
if item_name == "Focus":
|
||||
self.create_location("Focus")
|
||||
unfilled_locations += 1
|
||||
pool.append(item)
|
||||
else:
|
||||
self.world.push_precollected(item)
|
||||
return
|
||||
|
||||
if vanilla:
|
||||
location = self.create_vanilla_location(location_name, item)
|
||||
else:
|
||||
pool.append(item)
|
||||
if location_name in multi_locations: # Create shop locations later.
|
||||
return
|
||||
location = self.create_location(location_name)
|
||||
unfilled_locations += 1
|
||||
if excluded:
|
||||
location.progress_type = LocationProgressType.EXCLUDED
|
||||
|
||||
for option_key, option in hollow_knight_randomize_options.items():
|
||||
randomized = getattr(self.world, option_key)[self.player]
|
||||
for item_name, location_name in zip(option.items, option.locations):
|
||||
vanilla = not randomized
|
||||
excluded = False
|
||||
if item_name in geo_replace:
|
||||
item_name = "Geo_Rock-Default"
|
||||
item = self.create_item(item_name)
|
||||
if location_name == "Start":
|
||||
self.world.push_precollected(item)
|
||||
if item_name in junk_replace:
|
||||
item_name = self.get_filler_item_name()
|
||||
|
||||
if (item_name == "Crystal_Heart" and self.world.SplitCrystalHeart[self.player]) or \
|
||||
(item_name == "Mothwing_Cloak" and self.world.SplitMothwingCloak[self.player]):
|
||||
_add("Left_" + item_name, location_name)
|
||||
_add("Right_" + item_name, "Split_" + location_name)
|
||||
continue
|
||||
|
||||
location = self.create_location(location_name)
|
||||
if not vanilla and location_name in wp_exclusions:
|
||||
if location_name == 'King_Fragment':
|
||||
excluded = True
|
||||
if item_name == "Mantis_Claw" and self.world.SplitMantisClaw[self.player]:
|
||||
_add("Left_" + item_name, "Left_" + location_name)
|
||||
_add("Right_" + item_name, "Right_" + location_name)
|
||||
continue
|
||||
if item_name == "Shade_Cloak" and self.world.SplitMothwingCloak[self.player]:
|
||||
if self.world.random.randint(0, 1):
|
||||
item_name = "Left_Mothwing_Cloak"
|
||||
else:
|
||||
vanilla = True
|
||||
if excluded:
|
||||
location.progress_type = LocationProgressType.EXCLUDED
|
||||
if vanilla:
|
||||
location.place_locked_item(item)
|
||||
else:
|
||||
pool.append(item)
|
||||
item_name = "Right_Mothwing_Cloak"
|
||||
|
||||
for i in range(self.world.EggShopSlots[self.player].value):
|
||||
self.create_location("Egg_Shop")
|
||||
pool.append(self.create_item("Geo_Rock-Default"))
|
||||
_add(item_name, location_name)
|
||||
|
||||
if self.world.RandomizeElevatorPass[self.player]:
|
||||
randomized = True
|
||||
_add("Elevator_Pass", "Elevator_Pass")
|
||||
|
||||
for shop, locations in self.created_multi_locations.items():
|
||||
for _ in range(len(locations), getattr(self.world, shop_to_option[shop])[self.player].value):
|
||||
loc = self.create_location(shop)
|
||||
unfilled_locations += 1
|
||||
|
||||
# Balance the pool
|
||||
item_count = len(pool)
|
||||
additional_shop_items = max(item_count - unfilled_locations, self.world.ExtraShopSlots[self.player].value)
|
||||
|
||||
# Add additional shop items, as needed.
|
||||
if additional_shop_items > 0:
|
||||
shops = list(shop for shop, locations in self.created_multi_locations.items() if len(locations) < 16)
|
||||
if not self.world.EggShopSlots[self.player].value: # No eggshop, so don't place items there
|
||||
shops.remove('Egg_Shop')
|
||||
|
||||
for _ in range(additional_shop_items):
|
||||
shop = self.world.random.choice(shops)
|
||||
loc = self.create_location(shop)
|
||||
unfilled_locations += 1
|
||||
if len(self.created_multi_locations[shop]) >= 16:
|
||||
shops.remove(shop)
|
||||
if not shops:
|
||||
break
|
||||
|
||||
# Create filler items, if needed
|
||||
if item_count < unfilled_locations:
|
||||
pool.extend(self.create_item(self.get_filler_item_name()) for _ in range(unfilled_locations - item_count))
|
||||
self.world.itempool += pool
|
||||
self.apply_costsanity()
|
||||
self.sort_shops_by_cost()
|
||||
|
||||
for shopname in self.shops:
|
||||
prices: typing.List[int] = []
|
||||
locations: typing.List[HKLocation] = []
|
||||
for x in range(1, self.created_multi_locations[shopname]+1):
|
||||
loc = self.world.get_location(self.get_multi_location_name(shopname, x), self.player)
|
||||
locations.append(loc)
|
||||
prices.append(loc.cost)
|
||||
prices.sort()
|
||||
for loc, price in zip(locations, prices):
|
||||
loc.cost = price
|
||||
def sort_shops_by_cost(self):
|
||||
for shop, locations in self.created_multi_locations.items():
|
||||
randomized_locations = list(loc for loc in locations if not loc.vanilla)
|
||||
prices = sorted(
|
||||
(loc.costs for loc in randomized_locations),
|
||||
key=lambda costs: (len(costs),) + tuple(costs.values())
|
||||
)
|
||||
for loc, costs in zip(randomized_locations, prices):
|
||||
loc.costs = costs
|
||||
|
||||
def apply_costsanity(self):
|
||||
setting = self.world.CostSanity[self.player].value
|
||||
if not setting:
|
||||
return # noop
|
||||
|
||||
def _compute_weights(weights: dict, desc: str) -> typing.Dict[str, int]:
|
||||
if all(x == 0 for x in weights.values()):
|
||||
logger.warning(
|
||||
f"All {desc} weights were zero for {self.world.player_name[self.player]}."
|
||||
f" Setting them to one instead."
|
||||
)
|
||||
weights = {k: 1 for k in weights}
|
||||
|
||||
return {k: v for k, v in weights.items() if v}
|
||||
|
||||
random = self.world.random
|
||||
hybrid_chance = getattr(self.world, f"CostSanityHybridChance")[self.player].value
|
||||
weights = {
|
||||
data.term: getattr(self.world, f"CostSanity{data.option}Weight")[self.player].value
|
||||
for data in cost_terms.values()
|
||||
}
|
||||
weights_geoless = dict(weights)
|
||||
del weights_geoless["GEO"]
|
||||
|
||||
weights = _compute_weights(weights, "CostSanity")
|
||||
weights_geoless = _compute_weights(weights_geoless, "Geoless CostSanity")
|
||||
|
||||
if hybrid_chance > 0:
|
||||
if len(weights) == 1:
|
||||
logger.warning(
|
||||
f"Only one cost type is available for CostSanity in {self.world.player_name[self.player]}'s world."
|
||||
f" CostSanityHybridChance will not trigger."
|
||||
)
|
||||
if len(weights_geoless) == 1:
|
||||
logger.warning(
|
||||
f"Only one cost type is available for CostSanity in {self.world.player_name[self.player]}'s world."
|
||||
f" CostSanityHybridChance will not trigger in geoless locations."
|
||||
)
|
||||
|
||||
for region in self.world.get_regions(self.player):
|
||||
for location in region.locations:
|
||||
if location.vanilla:
|
||||
continue
|
||||
if not location.costs:
|
||||
continue
|
||||
if location.name == "Vessel_Fragment-Basin":
|
||||
continue
|
||||
if setting == CostSanity.option_notshops and location.basename in multi_locations:
|
||||
continue
|
||||
if setting == CostSanity.option_shopsonly and location.basename not in multi_locations:
|
||||
continue
|
||||
if location.basename in {'Grubfather', 'Seer', 'Eggshop'}:
|
||||
our_weights = dict(weights_geoless)
|
||||
else:
|
||||
our_weights = dict(weights)
|
||||
|
||||
rolls = 1
|
||||
if random.randrange(100) < hybrid_chance:
|
||||
rolls = 2
|
||||
|
||||
if rolls > len(our_weights):
|
||||
terms = list(our_weights.keys()) # Can't randomly choose cost types, using all of them.
|
||||
else:
|
||||
terms = []
|
||||
for _ in range(rolls):
|
||||
term = random.choices(list(our_weights.keys()), list(our_weights.values()))[0]
|
||||
del our_weights[term]
|
||||
terms.append(term)
|
||||
|
||||
location.costs = {term: random.randint(*self.ranges[term]) for term in terms}
|
||||
location.sort_costs()
|
||||
|
||||
def set_rules(self):
|
||||
world = self.world
|
||||
@@ -280,12 +447,24 @@ class HKWorld(World):
|
||||
# 32 bit int
|
||||
slot_data["seed"] = self.world.slot_seeds[self.player].randint(-2147483647, 2147483646)
|
||||
|
||||
for shop, unit in self.shops.items():
|
||||
slot_data[f"{unit}_costs"] = {
|
||||
f"{shop}_{i}":
|
||||
self.world.get_location(f"{shop}_{i}", self.player).cost
|
||||
for i in range(1, 1 + self.created_multi_locations[shop])
|
||||
}
|
||||
# Backwards compatibility for shop cost data (HKAP < 0.1.0)
|
||||
if not self.world.CostSanity[self.player]:
|
||||
for shop, terms in shop_cost_types.items():
|
||||
unit = cost_terms[next(iter(terms))].option
|
||||
if unit == "Geo":
|
||||
continue
|
||||
slot_data[f"{unit}_costs"] = {
|
||||
loc.name: next(iter(loc.costs.values()))
|
||||
for loc in self.created_multi_locations[shop]
|
||||
}
|
||||
|
||||
# HKAP 0.1.0 and later cost data.
|
||||
location_costs = {}
|
||||
for region in self.world.get_regions(self.player):
|
||||
for location in region.locations:
|
||||
if location.costs:
|
||||
location_costs[location.name] = location.costs
|
||||
slot_data["location_costs"] = location_costs
|
||||
|
||||
slot_data["notch_costs"] = self.charm_costs
|
||||
|
||||
@@ -295,30 +474,52 @@ class HKWorld(World):
|
||||
item_data = item_table[name]
|
||||
return HKItem(name, item_data.advancement, item_data.id, item_data.type, self.player)
|
||||
|
||||
def create_location(self, name: str) -> HKLocation:
|
||||
unit = self.shops.get(name, None)
|
||||
if unit:
|
||||
cost = self.world.random.randint(*self.ranges[unit])
|
||||
else:
|
||||
cost = 0
|
||||
if name in multi_locations:
|
||||
self.created_multi_locations[name] += 1
|
||||
name = self.get_multi_location_name(name, self.created_multi_locations[name])
|
||||
def create_location(self, name: str, vanilla=False) -> HKLocation:
|
||||
costs = None
|
||||
basename = name
|
||||
if name in shop_cost_types:
|
||||
costs = {
|
||||
term: self.world.random.randint(*self.ranges[term])
|
||||
for term in shop_cost_types[name]
|
||||
}
|
||||
elif name in vanilla_location_costs:
|
||||
costs = vanilla_location_costs[name]
|
||||
|
||||
multi = self.created_multi_locations.get(name)
|
||||
|
||||
if multi is not None:
|
||||
i = len(multi) + 1
|
||||
name = f"{name}_{i}"
|
||||
|
||||
region = self.world.get_region("Menu", self.player)
|
||||
loc = HKLocation(self.player, name, self.location_name_to_id[name], region)
|
||||
if unit:
|
||||
loc.unit = unit
|
||||
loc.cost = cost
|
||||
loc = HKLocation(self.player, name,
|
||||
self.location_name_to_id[name], region, costs=costs, vanilla=vanilla,
|
||||
basename=basename)
|
||||
|
||||
if multi is not None:
|
||||
multi.append(loc)
|
||||
|
||||
region.locations.append(loc)
|
||||
return loc
|
||||
|
||||
def create_vanilla_location(self, location: str, item: Item):
|
||||
costs = self.vanilla_shop_costs.get((location, item.name))
|
||||
location = self.create_location(location, vanilla=True)
|
||||
location.place_locked_item(item)
|
||||
if costs:
|
||||
location.costs = costs.pop()
|
||||
return location
|
||||
|
||||
def collect(self, state, item: HKItem) -> bool:
|
||||
change = super(HKWorld, self).collect(state, item)
|
||||
if change:
|
||||
for effect_name, effect_value in item_effects.get(item.name, {}).items():
|
||||
state.prog_items[effect_name, item.player] += effect_value
|
||||
|
||||
if item.name in {"Left_Mothwing_Cloak", "Right_Mothwing_Cloak"}:
|
||||
if state.prog_items.get(('RIGHTDASH', item.player), 0) and \
|
||||
state.prog_items.get(('LEFTDASH', item.player), 0):
|
||||
(state.prog_items["RIGHTDASH", item.player], state.prog_items["LEFTDASH", item.player]) = \
|
||||
([max(state.prog_items["RIGHTDASH", item.player], state.prog_items["LEFTDASH", item.player])] * 2)
|
||||
return change
|
||||
|
||||
def remove(self, state, item: HKItem) -> bool:
|
||||
@@ -348,17 +549,40 @@ class HKWorld(World):
|
||||
name = world.get_player_name(player)
|
||||
spoiler_handle.write(f'\n{name}\n')
|
||||
hk_world: HKWorld = world.worlds[player]
|
||||
for shop_name, unit_name in cls.shops.items():
|
||||
for x in range(1, hk_world.created_multi_locations[shop_name]+1):
|
||||
loc = world.get_location(hk_world.get_multi_location_name(shop_name, x), player)
|
||||
spoiler_handle.write(f"\n{loc}: {loc.item} costing {loc.cost} {unit_name}")
|
||||
|
||||
if world.CostSanity[player].value:
|
||||
for loc in sorted(
|
||||
(
|
||||
loc for loc in itertools.chain(*(region.locations for region in world.get_regions(player)))
|
||||
if loc.costs
|
||||
), key=operator.attrgetter('name')
|
||||
):
|
||||
spoiler_handle.write(f"\n{loc}: {loc.item} costing {loc.cost_text()}")
|
||||
else:
|
||||
for shop_name, locations in hk_world.created_multi_locations.items():
|
||||
for loc in locations:
|
||||
spoiler_handle.write(f"\n{loc}: {loc.item} costing {loc.cost_text()}")
|
||||
|
||||
def get_multi_location_name(self, base: str, i: typing.Optional[int]) -> str:
|
||||
if i is None:
|
||||
i = self.created_multi_locations[base]
|
||||
assert 0 < i < 18, "limited number of multi location IDs reserved."
|
||||
i = len(self.created_multi_locations[base]) + 1
|
||||
assert 1 <= 16, "limited number of multi location IDs reserved."
|
||||
return f"{base}_{i}"
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
if self.player not in self.cached_filler_items:
|
||||
fillers = ["One_Geo", "Soul_Refill"]
|
||||
exclusions = self.white_palace_exclusions()
|
||||
for group in (
|
||||
'RandomizeGeoRocks', 'RandomizeSoulTotems', 'RandomizeLoreTablets', 'RandomizeJunkPitChests',
|
||||
'RandomizeRancidEggs'
|
||||
):
|
||||
if getattr(self.world, group):
|
||||
fillers.extend(item for item in hollow_knight_randomize_options[group].items if item not in
|
||||
exclusions)
|
||||
self.cached_filler_items[self.player] = fillers
|
||||
return self.world.random.choice(self.cached_filler_items[self.player])
|
||||
|
||||
|
||||
def create_region(world: MultiWorld, player: int, name: str, location_names=None, exits=None) -> Region:
|
||||
ret = Region(name, RegionType.Generic, name, player)
|
||||
@@ -376,11 +600,34 @@ def create_region(world: MultiWorld, player: int, name: str, location_names=None
|
||||
|
||||
class HKLocation(Location):
|
||||
game: str = "Hollow Knight"
|
||||
cost: int = 0
|
||||
costs: typing.Dict[str, int] = None
|
||||
unit: typing.Optional[str] = None
|
||||
vanilla = False
|
||||
basename: str
|
||||
|
||||
def __init__(self, player: int, name: str, code=None, parent=None):
|
||||
def sort_costs(self):
|
||||
if self.costs is None:
|
||||
return
|
||||
self.costs = {k: self.costs[k] for k in sorted(self.costs.keys(), key=lambda x: cost_terms[x].sort)}
|
||||
|
||||
def __init__(
|
||||
self, player: int, name: str, code=None, parent=None,
|
||||
costs: typing.Dict[str, int] = None, vanilla: bool = False, basename: str = None
|
||||
):
|
||||
self.basename = basename or name
|
||||
super(HKLocation, self).__init__(player, name, code if code else None, parent)
|
||||
self.vanilla = vanilla
|
||||
if costs:
|
||||
self.costs = dict(costs)
|
||||
self.sort_costs()
|
||||
|
||||
def cost_text(self, separator=" and "):
|
||||
if self.costs is None:
|
||||
return None
|
||||
return separator.join(
|
||||
f"{value} {cost_terms[term].singular if value == 1 else cost_terms[term].plural}"
|
||||
for term, value in self.costs.items()
|
||||
)
|
||||
|
||||
|
||||
class HKItem(Item):
|
||||
@@ -393,6 +640,10 @@ class HKItem(Item):
|
||||
classification = ItemClassification.progression_skip_balancing
|
||||
elif type == "Charm" and name not in progression_charms:
|
||||
classification = ItemClassification.progression_skip_balancing
|
||||
elif type in ("Map", "Journal"):
|
||||
classification = ItemClassification.filler
|
||||
elif type in ("Mask", "Ore", "Vessel"):
|
||||
classification = ItemClassification.useful
|
||||
elif advancement:
|
||||
classification = ItemClassification.progression
|
||||
else:
|
||||
|
||||
@@ -1,50 +1,20 @@
|
||||
from ..generic.Rules import set_rule, add_rule
|
||||
# This module is written by Extractor.py, do not edit manually!.
|
||||
from functools import partial
|
||||
|
||||
units = {
|
||||
"Egg": "RANCIDEGGS",
|
||||
"Grub": "GRUBS",
|
||||
"Essence": "ESSENCE",
|
||||
"Charm": "CHARMS",
|
||||
}
|
||||
|
||||
|
||||
def hk_set_rule(hk_world, location: str, rule):
|
||||
count = hk_world.created_multi_locations[location]
|
||||
if count:
|
||||
locations = [f"{location}_{x}" for x in range(1, count+1)]
|
||||
elif (location, hk_world.player) in hk_world.world._location_cache:
|
||||
locations = [location]
|
||||
else:
|
||||
return
|
||||
for location in locations:
|
||||
set_rule(hk_world.world.get_location(location, hk_world.player), rule)
|
||||
|
||||
|
||||
def set_shop_prices(hk_world):
|
||||
def set_generated_rules(hk_world, hk_set_rule):
|
||||
player = hk_world.player
|
||||
for shop, unit in hk_world.shops.items():
|
||||
for i in range(1, 1 + hk_world.created_multi_locations[shop]):
|
||||
loc = hk_world.world.get_location(f"{shop}_{i}", hk_world.player)
|
||||
add_rule(loc, lambda state, unit=units[unit], cost=loc.cost: state.count(unit, player) >= cost)
|
||||
|
||||
|
||||
def set_rules(hk_world):
|
||||
player = hk_world.player
|
||||
world = hk_world.world
|
||||
fn = partial(hk_set_rule, hk_world)
|
||||
|
||||
# Events
|
||||
{% for location, rule_text in event_rules.items() %}
|
||||
hk_set_rule(hk_world, "{{location}}", lambda state: {{rule_text}})
|
||||
fn("{{location}}", lambda state: {{rule_text}})
|
||||
{%- endfor %}
|
||||
|
||||
# Locations
|
||||
{% for location, rule_text in location_rules.items() %}
|
||||
hk_set_rule(hk_world, "{{location}}", lambda state: {{rule_text}})
|
||||
fn("{{location}}", lambda state: {{rule_text}})
|
||||
{%- endfor %}
|
||||
|
||||
# Shop prices
|
||||
set_shop_prices(hk_world)
|
||||
|
||||
# Connectors
|
||||
{% for entrance, rule_text in connectors_rules.items() %}
|
||||
rule = lambda state: {{rule_text}}
|
||||
@@ -54,4 +24,4 @@ def set_rules(hk_world):
|
||||
world.get_entrance("{{entrance}}_R", player).access_rule = lambda state, entrance= entrance: \
|
||||
rule(state) and entrance.can_reach(state)
|
||||
{%- endif %}
|
||||
{% endfor %}
|
||||
{%- endfor %}
|
||||
@@ -47,16 +47,29 @@ When the console tells you that you have joined the room, you're all set. Congra
|
||||
multiworld game! At this point any additional minecraft players may connect to your forge server. To start the game once
|
||||
everyone is ready use the command `/start`.
|
||||
|
||||
## Manual Installation
|
||||
## Non-Windows Installation
|
||||
|
||||
The Minecraft Client will install forge and the mod for other operating systems but Java has to be provided by the
|
||||
user. Head to [minecraft_versions.json on the MC AP GitHub](https://raw.githubusercontent.com/KonoTyran/Minecraft_AP_Randomizer/master/versions/minecraft_versions.json)
|
||||
to see which java version is required. New installations will default to the topmost "release" version.
|
||||
- Install the matching Amazon Corretto JDK
|
||||
- see [Manual Installation Software Links](#manual-installation-software-links)
|
||||
- or package manager provided by your OS / distribution
|
||||
- Open your `host.yaml` and add the path to your Java below the `minecraft_options` key
|
||||
- ` java: "path/to/java-xx-amazon-corretto/bin/java"`
|
||||
- Run the Minecraft Client and select your .apmc file
|
||||
|
||||
## Full Manual Installation
|
||||
|
||||
It is highly recommended to ues the Archipelago installer to handle the installation of the forge server for you.
|
||||
support will not be given for those wishing to manually install forge. For those of you who know how, and wish to do so,
|
||||
Support will not be given for those wishing to manually install forge. For those of you who know how, and wish to do so,
|
||||
the following links are the versions of the software we use.
|
||||
|
||||
### Manual install Software links
|
||||
### Manual Installation Software Links
|
||||
|
||||
- [Minecraft Forge Download Page](https://files.minecraftforge.net/net/minecraftforge/forge/index_1.18.2.html)
|
||||
- [Minecraft Forge Download Page](https://files.minecraftforge.net/net/minecraftforge/forge/)
|
||||
- [Minecraft Archipelago Randomizer Mod Releases Page](https://github.com/KonoTyran/Minecraft_AP_Randomizer/releases)
|
||||
- **DO NOT INSTALL THIS ON YOUR CLIENT**
|
||||
- [Amazon Corretto Java 17 Download Page](https://docs.aws.amazon.com/corretto/latest/corretto-17-ug/downloads-list.html)
|
||||
- [Amazon Corretto](https://docs.aws.amazon.com/corretto/)
|
||||
- pick the matching version and select "Downloads" on the left
|
||||
|
||||
|
||||
@@ -1088,10 +1088,10 @@ def get_pool_core(world):
|
||||
placed_items['Hideout Jail Guard (4 Torches)'] = 'Recovery Heart'
|
||||
skip_in_spoiler_locations.extend(['Hideout Jail Guard (2 Torches)', 'Hideout Jail Guard (3 Torches)', 'Hideout Jail Guard (4 Torches)'])
|
||||
else:
|
||||
placed_items['Hideout Jail Guard (1 Torch)'] = 'Small Key (Gerudo Fortress)'
|
||||
placed_items['Hideout Jail Guard (2 Torches)'] = 'Small Key (Gerudo Fortress)'
|
||||
placed_items['Hideout Jail Guard (3 Torches)'] = 'Small Key (Gerudo Fortress)'
|
||||
placed_items['Hideout Jail Guard (4 Torches)'] = 'Small Key (Gerudo Fortress)'
|
||||
placed_items['Hideout Jail Guard (1 Torch)'] = 'Small Key (Thieves Hideout)'
|
||||
placed_items['Hideout Jail Guard (2 Torches)'] = 'Small Key (Thieves Hideout)'
|
||||
placed_items['Hideout Jail Guard (3 Torches)'] = 'Small Key (Thieves Hideout)'
|
||||
placed_items['Hideout Jail Guard (4 Torches)'] = 'Small Key (Thieves Hideout)'
|
||||
|
||||
if world.shuffle_gerudo_card and world.gerudo_fortress != 'open':
|
||||
pool.append('Gerudo Membership Card')
|
||||
|
||||
@@ -51,20 +51,20 @@ known_logic_tricks = {
|
||||
Can be reached by side-hopping off
|
||||
the watchtower.
|
||||
'''},
|
||||
'Dodongo\'s Cavern Staircase with Bow': {
|
||||
"Dodongo's Cavern Staircase with Bow": {
|
||||
'name' : 'logic_dc_staircase',
|
||||
'tags' : ("Dodongo's Cavern",),
|
||||
'tooltip' : '''\
|
||||
The Bow can be used to knock down the stairs
|
||||
with two well-timed shots.
|
||||
'''},
|
||||
'Dodongo\'s Cavern Spike Trap Room Jump without Hover Boots': {
|
||||
"Dodongo's Cavern Spike Trap Room Jump without Hover Boots": {
|
||||
'name' : 'logic_dc_jump',
|
||||
'tags' : ("Dodongo's Cavern",),
|
||||
'tooltip' : '''\
|
||||
Jump is adult only.
|
||||
'''},
|
||||
'Dodongo\'s Cavern Vines GS from Below with Longshot': {
|
||||
"Dodongo's Cavern Vines GS from Below with Longshot": {
|
||||
'name' : 'logic_dc_vines_gs',
|
||||
'tags' : ("Dodongo's Cavern", "Skulltulas",),
|
||||
'tooltip' : '''\
|
||||
@@ -73,7 +73,7 @@ known_logic_tricks = {
|
||||
from below, by shooting it through the vines,
|
||||
bypassing the need to lower the staircase.
|
||||
'''},
|
||||
'Thieves\' Hideout "Kitchen" with No Additional Items': {
|
||||
'''Thieves' Hideout "Kitchen" with No Additional Items''': {
|
||||
'name' : 'logic_gerudo_kitchen',
|
||||
'tags' : ("Thieves' Hideout", "Gerudo's Fortress"),
|
||||
'tooltip' : '''\
|
||||
@@ -157,7 +157,7 @@ known_logic_tricks = {
|
||||
Can jump up to the spinning platform from
|
||||
below as adult.
|
||||
'''},
|
||||
'Crater\'s Bean PoH with Hover Boots': {
|
||||
"Crater's Bean PoH with Hover Boots": {
|
||||
'name' : 'logic_crater_bean_poh_with_hovers',
|
||||
'tags' : ("Death Mountain Crater",),
|
||||
'tooltip' : '''\
|
||||
@@ -165,7 +165,7 @@ known_logic_tricks = {
|
||||
near Goron City and walk up the
|
||||
very steep slope.
|
||||
'''},
|
||||
'Zora\'s Domain Entry with Cucco': {
|
||||
"Zora's Domain Entry with Cucco": {
|
||||
'name' : 'logic_zora_with_cucco',
|
||||
'tags' : ("Zora's River",),
|
||||
'tooltip' : '''\
|
||||
@@ -404,7 +404,7 @@ known_logic_tricks = {
|
||||
Longshot can be shot through the ceiling to obtain
|
||||
the token with two fewer small keys than normal.
|
||||
'''},
|
||||
'Zora\'s River Lower Freestanding PoH as Adult with Nothing': {
|
||||
"Zora's River Lower Freestanding PoH as Adult with Nothing": {
|
||||
'name' : 'logic_zora_river_lower',
|
||||
'tags' : ("Zora's River",),
|
||||
'tooltip' : '''\
|
||||
@@ -502,7 +502,7 @@ known_logic_tricks = {
|
||||
you can get enough of a break to take some time
|
||||
to aim more carefully.
|
||||
'''},
|
||||
'Dodongo\'s Cavern Scarecrow GS with Armos Statue': {
|
||||
"Dodongo's Cavern Scarecrow GS with Armos Statue": {
|
||||
'name' : 'logic_dc_scarecrow_gs',
|
||||
'tags' : ("Dodongo's Cavern", "Skulltulas",),
|
||||
'tooltip' : '''\
|
||||
@@ -541,7 +541,7 @@ known_logic_tricks = {
|
||||
'name' : 'logic_spirit_mq_lower_adult',
|
||||
'tags' : ("Spirit Temple",),
|
||||
'tooltip' : '''\
|
||||
It can be done with Din\'s Fire and Bow.
|
||||
It can be done with Din's Fire and Bow.
|
||||
Whenever an arrow passes through a lit torch, it
|
||||
resets the timer. It's finicky but it's also
|
||||
possible to stand on the pillar next to the center
|
||||
@@ -704,13 +704,13 @@ known_logic_tricks = {
|
||||
in the same jump in order to destroy it before you
|
||||
fall into the lava.
|
||||
'''},
|
||||
'Zora\'s Domain Entry with Hover Boots': {
|
||||
"Zora's Domain Entry with Hover Boots": {
|
||||
'name' : 'logic_zora_with_hovers',
|
||||
'tags' : ("Zora's River",),
|
||||
'tooltip' : '''\
|
||||
Can hover behind the waterfall as adult.
|
||||
'''},
|
||||
'Zora\'s Domain GS with No Additional Items': {
|
||||
"Zora's Domain GS with No Additional Items": {
|
||||
'name' : 'logic_domain_gs',
|
||||
'tags' : ("Zora's Domain", "Skulltulas",),
|
||||
'tooltip' : '''\
|
||||
@@ -736,7 +736,7 @@ known_logic_tricks = {
|
||||
needing a Bow.
|
||||
Applies in both vanilla and MQ Shadow.
|
||||
'''},
|
||||
'Stop Link the Goron with Din\'s Fire': {
|
||||
"Stop Link the Goron with Din's Fire": {
|
||||
'name' : 'logic_link_goron_dins',
|
||||
'tags' : ("Goron City",),
|
||||
'tooltip' : '''\
|
||||
@@ -825,7 +825,7 @@ known_logic_tricks = {
|
||||
Link will not be expected to do anything at Gerudo's
|
||||
Fortress.
|
||||
'''},
|
||||
'Zora\'s River Upper Freestanding PoH as Adult with Nothing': {
|
||||
"Zora's River Upper Freestanding PoH as Adult with Nothing": {
|
||||
'name' : 'logic_zora_river_upper',
|
||||
'tags' : ("Zora's River",),
|
||||
'tooltip' : '''\
|
||||
@@ -971,7 +971,7 @@ known_logic_tricks = {
|
||||
in the Water Temple are not going to be relevant unless this
|
||||
trick is first enabled.
|
||||
'''},
|
||||
'Water Temple Central Pillar GS with Farore\'s Wind': {
|
||||
"Water Temple Central Pillar GS with Farore's Wind": {
|
||||
'name' : 'logic_water_central_gs_fw',
|
||||
'tags' : ("Water Temple", "Skulltulas",),
|
||||
'tooltip' : '''\
|
||||
@@ -1104,7 +1104,7 @@ known_logic_tricks = {
|
||||
this allows you to obtain the GS on the door frame
|
||||
as adult without Hookshot or Song of Time.
|
||||
'''},
|
||||
'Dodongo\'s Cavern MQ Early Bomb Bag Area as Child': {
|
||||
"Dodongo's Cavern MQ Early Bomb Bag Area as Child": {
|
||||
'name' : 'logic_dc_mq_child_bombs',
|
||||
'tags' : ("Dodongo's Cavern",),
|
||||
'tooltip' : '''\
|
||||
@@ -1113,7 +1113,7 @@ known_logic_tricks = {
|
||||
without needing a Slingshot. You will
|
||||
take fall damage.
|
||||
'''},
|
||||
'Dodongo\'s Cavern Two Scrub Room with Strength': {
|
||||
"Dodongo's Cavern Two Scrub Room with Strength": {
|
||||
'name' : 'logic_dc_scrub_room',
|
||||
'tags' : ("Dodongo's Cavern",),
|
||||
'tooltip' : '''\
|
||||
@@ -1122,7 +1122,7 @@ known_logic_tricks = {
|
||||
destroy the mud wall blocking the room with two
|
||||
Deku Scrubs.
|
||||
'''},
|
||||
'Dodongo\'s Cavern Child Slingshot Skips': {
|
||||
"Dodongo's Cavern Child Slingshot Skips": {
|
||||
'name' : 'logic_dc_slingshot_skip',
|
||||
'tags' : ("Dodongo's Cavern",),
|
||||
'tooltip' : '''\
|
||||
@@ -1132,7 +1132,7 @@ known_logic_tricks = {
|
||||
you also enable the Adult variant: "Dodongo's
|
||||
Cavern Spike Trap Room Jump without Hover Boots".
|
||||
'''},
|
||||
'Dodongo\'s Cavern MQ Light the Eyes with Strength': {
|
||||
"Dodongo's Cavern MQ Light the Eyes with Strength": {
|
||||
'name' : 'logic_dc_mq_eyes',
|
||||
'tags' : ("Dodongo's Cavern",),
|
||||
'tooltip' : '''\
|
||||
@@ -1145,7 +1145,7 @@ known_logic_tricks = {
|
||||
Also, the bombable floor before King Dodongo can be
|
||||
destroyed with Hammer if hit in the very center.
|
||||
'''},
|
||||
'Dodongo\'s Cavern MQ Back Areas as Child without Explosives': {
|
||||
"Dodongo's Cavern MQ Back Areas as Child without Explosives": {
|
||||
'name' : 'logic_dc_mq_child_back',
|
||||
'tags' : ("Dodongo's Cavern",),
|
||||
'tooltip' : '''\
|
||||
@@ -1232,7 +1232,7 @@ known_logic_tricks = {
|
||||
It can also be done as child, using the
|
||||
Slingshot instead of the Bow.
|
||||
'''},
|
||||
'Fire Temple East Tower without Scarecrow\'s Song': {
|
||||
"Fire Temple East Tower without Scarecrow's Song": {
|
||||
'name' : 'logic_fire_scarecrow',
|
||||
'tags' : ("Fire Temple",),
|
||||
'tooltip' : '''\
|
||||
@@ -1277,14 +1277,14 @@ known_logic_tricks = {
|
||||
Removes the requirements for the Lens of Truth
|
||||
in Bottom of the Well.
|
||||
'''},
|
||||
'Ganon\'s Castle MQ without Lens of Truth': {
|
||||
"Ganon's Castle MQ without Lens of Truth": {
|
||||
'name' : 'logic_lens_castle_mq',
|
||||
'tags' : ("Lens of Truth","Ganon's Castle",),
|
||||
'tooltip' : '''\
|
||||
Removes the requirements for the Lens of Truth
|
||||
in Ganon's Castle MQ.
|
||||
'''},
|
||||
'Ganon\'s Castle without Lens of Truth': {
|
||||
"Ganon's Castle without Lens of Truth": {
|
||||
'name' : 'logic_lens_castle',
|
||||
'tags' : ("Lens of Truth","Ganon's Castle",),
|
||||
'tooltip' : '''\
|
||||
|
||||
@@ -8,5 +8,5 @@ lookup_id_to_name = {}
|
||||
for item in location_table:
|
||||
lookup_id_to_name[item["id"]] = item["name"]
|
||||
|
||||
lookup_id_to_name[None] = "Tangaroa Next Frequency"
|
||||
lookup_id_to_name[None] = "Utopia Complete"
|
||||
lookup_name_to_id = {name: id for id, name in lookup_id_to_name.items()}
|
||||
@@ -37,6 +37,20 @@ class IslandFrequencyLocations(Choice):
|
||||
option_anywhere = 3
|
||||
default = 1
|
||||
|
||||
class IslandGenerationDistance(Choice):
|
||||
"""Sets how far away islands spawn from you when you input their coordinates into the Receiver."""
|
||||
display_name = "Island distance"
|
||||
option_quarter = 2
|
||||
option_half = 4
|
||||
option_vanilla = 8
|
||||
option_double = 16
|
||||
option_quadrouple = 32
|
||||
default = 8
|
||||
|
||||
class ExpensiveResearch(Toggle):
|
||||
"""Makes unlocking items in the Crafting Table consume the researched items."""
|
||||
display_name = "Expensive research"
|
||||
|
||||
class ProgressiveItems(DefaultOnToggle):
|
||||
"""Makes some items, like the Bow and Arrow, progressive rather than raw unlocks."""
|
||||
display_name = "Progressive items"
|
||||
@@ -55,6 +69,8 @@ raft_options = {
|
||||
"maximum_resource_pack_amount": MaximumResourcePackAmount,
|
||||
"duplicate_items": DuplicateItems,
|
||||
"island_frequency_locations": IslandFrequencyLocations,
|
||||
"island_generation_distance": IslandGenerationDistance,
|
||||
"expensive_research": ExpensiveResearch,
|
||||
"progressive_items": ProgressiveItems,
|
||||
"big_island_early_crafting": BigIslandEarlyCrafting,
|
||||
"paddleboard_mode": PaddleboardMode
|
||||
|
||||
@@ -12,6 +12,9 @@ class RaftLogic(LogicMixin):
|
||||
|
||||
def raft_can_smelt_items(self, player):
|
||||
return self.has("Smelter", player)
|
||||
|
||||
def raft_can_find_titanium(self, player):
|
||||
return self.has("Metal detector", player)
|
||||
|
||||
def raft_can_craft_bolt(self, player):
|
||||
return self.raft_can_smelt_items(player) and self.has("Bolt", player)
|
||||
@@ -76,7 +79,7 @@ class RaftLogic(LogicMixin):
|
||||
return self.raft_can_craft_battery(player) and self.raft_can_craft_reciever(player) and self.raft_can_craft_antenna(player)
|
||||
|
||||
def raft_can_drive(self, player): # The player can go wherever they want with the engine
|
||||
return self.raft_can_craft_engine(player) and self.raft_can_craft_steeringWheel(player)
|
||||
return (self.raft_can_craft_engine(player) and self.raft_can_craft_steeringWheel(player)) or self.raft_paddleboard_mode_enabled(player)
|
||||
|
||||
def raft_can_access_radio_tower(self, player):
|
||||
return self.raft_can_navigate(player)
|
||||
@@ -92,24 +95,42 @@ class RaftLogic(LogicMixin):
|
||||
|
||||
def raft_can_access_balboa_island(self, player):
|
||||
return (self.raft_can_complete_vasagatan(player)
|
||||
and (self.raft_can_drive(player) or self.raft_paddleboard_mode_enabled(player))
|
||||
and self.raft_can_drive(player)
|
||||
and self.has("Balboa Island Frequency", player))
|
||||
|
||||
def raft_can_complete_balboa_island(self, player):
|
||||
return self.raft_can_access_balboa_island(player) and self.raft_can_craft_machete(player) and self.raft_can_fire_bow(player)
|
||||
return self.raft_can_access_balboa_island(player) and self.raft_can_craft_machete(player)
|
||||
|
||||
def raft_can_access_caravan_island(self, player):
|
||||
return self.raft_can_complete_balboa_island(player) and (self.raft_can_drive(player) or self.raft_paddleboard_mode_enabled(player)) and self.has("Caravan Island Frequency", player)
|
||||
return self.raft_can_complete_balboa_island(player) and self.raft_can_drive(player) and self.has("Caravan Island Frequency", player)
|
||||
|
||||
def raft_can_complete_caravan_island(self, player):
|
||||
return self.raft_can_access_caravan_island(player) and self.raft_can_craft_ziplineTool(player)
|
||||
|
||||
def raft_can_access_tangaroa(self, player):
|
||||
return self.raft_can_complete_caravan_island(player) and (self.raft_can_drive(player) or self.raft_paddleboard_mode_enabled(player)) and self.has("Tangaroa Frequency", player)
|
||||
return self.raft_can_complete_caravan_island(player) and self.raft_can_drive(player) and self.has("Tangaroa Frequency", player)
|
||||
|
||||
def raft_can_complete_tangaroa(self, player):
|
||||
return self.raft_can_access_tangaroa(player)
|
||||
|
||||
def raft_can_access_varuna_point(self, player):
|
||||
return self.raft_can_complete_tangaroa(player) and self.raft_can_drive(player) and self.has("Varuna Point Frequency", player)
|
||||
|
||||
def raft_can_complete_varuna_point(self, player):
|
||||
return self.raft_can_access_varuna_point(player)
|
||||
|
||||
def raft_can_access_temperance(self, player):
|
||||
return self.raft_can_complete_varuna_point(player) and self.raft_can_drive(player) and self.has("Temperance Frequency", player)
|
||||
|
||||
def raft_can_complete_temperance(self, player):
|
||||
return self.raft_can_access_temperance(player)
|
||||
|
||||
def raft_can_access_utopia(self, player):
|
||||
return self.raft_can_complete_temperance(player) and self.raft_can_drive(player) and self.has("Utopia Frequency", player)
|
||||
|
||||
def raft_can_complete_utopia(self, player):
|
||||
return self.raft_can_access_utopia(player)
|
||||
|
||||
def set_rules(world, player):
|
||||
regionChecks = {
|
||||
"Raft": lambda state: True,
|
||||
@@ -118,7 +139,10 @@ def set_rules(world, player):
|
||||
"Vasagatan": lambda state: state.raft_can_complete_radio_tower(player) and state.raft_can_access_vasagatan(player),
|
||||
"BalboaIsland": lambda state: state.raft_can_complete_vasagatan(player) and state.raft_can_access_balboa_island(player),
|
||||
"CaravanIsland": lambda state: state.raft_can_complete_balboa_island(player) and state.raft_can_access_caravan_island(player),
|
||||
"Tangaroa": lambda state: state.raft_can_complete_caravan_island(player) and state.raft_can_access_tangaroa(player)
|
||||
"Tangaroa": lambda state: state.raft_can_complete_caravan_island(player) and state.raft_can_access_tangaroa(player),
|
||||
"Varuna Point": lambda state: state.raft_can_complete_tangaroa(player) and state.raft_can_access_varuna_point(player),
|
||||
"Temperance": lambda state: state.raft_can_complete_varuna_point(player) and state.raft_can_access_temperance(player),
|
||||
"Utopia": lambda state: state.raft_can_complete_temperance(player) and state.raft_can_access_utopia(player)
|
||||
}
|
||||
itemChecks = {
|
||||
"Plank": lambda state: True,
|
||||
@@ -143,15 +167,14 @@ def set_rules(world, player):
|
||||
"Hinge": lambda state: state.raft_can_craft_hinge(player),
|
||||
"CircuitBoard": lambda state: state.raft_can_craft_circuitBoard(player),
|
||||
"PlasticBottle_Empty": lambda state: state.raft_can_craft_plasticBottle(player),
|
||||
"Shear": lambda state: state.raft_can_craft_shears(player),
|
||||
"Wool": lambda state: state.raft_can_capture_animals(player) and state.raft_can_craft_shears(player),
|
||||
"HoneyComb": lambda state: state.raft_can_access_balboa_island(player),
|
||||
"Jar_Bee": lambda state: state.raft_can_access_balboa_island(player) and state.raft_can_smelt_items(player),
|
||||
"Dirt": lambda state: state.raft_can_get_dirt(player),
|
||||
"Egg": lambda state: state.raft_can_capture_animals(player),
|
||||
"TitaniumIngot": lambda state: state.raft_can_smelt_items(player) and state.raft_can_find_titanium(player),
|
||||
# Specific items for story island location checks
|
||||
"Machete": lambda state: state.raft_can_craft_machete(player),
|
||||
"BowAndArrow": lambda state: state.raft_can_fire_bow(player),
|
||||
"Zipline tool": lambda state: state.raft_can_craft_ziplineTool(player)
|
||||
}
|
||||
|
||||
|
||||
@@ -39,8 +39,8 @@ class RaftWorld(World):
|
||||
location_name_to_id = locations_lookup_name_to_id
|
||||
options = raft_options
|
||||
|
||||
data_version = 1
|
||||
required_client_version = (0, 2, 0)
|
||||
data_version = 2
|
||||
required_client_version = (0, 3, 4)
|
||||
|
||||
def generate_basic(self):
|
||||
minRPSpecified = self.world.minimum_resource_pack_amount[self.player].value
|
||||
@@ -96,6 +96,11 @@ class RaftWorld(World):
|
||||
slot_data = {}
|
||||
return slot_data
|
||||
|
||||
def get_pre_fill_items(self):
|
||||
if self.world.island_frequency_locations[self.player] in [0, 1]:
|
||||
return [loc.item for loc in self.world.get_filled_locations()]
|
||||
return []
|
||||
|
||||
def create_item_replaceAsNecessary(self, name: str) -> Item:
|
||||
isFrequency = "Frequency" in name
|
||||
shouldUseProgressive = ((isFrequency and self.world.island_frequency_locations[self.player].value == 2)
|
||||
@@ -132,13 +137,19 @@ class RaftWorld(World):
|
||||
self.setLocationItem("Vasagatan Frequency to Balboa", "Balboa Island Frequency")
|
||||
self.setLocationItem("Relay Station quest", "Caravan Island Frequency")
|
||||
self.setLocationItem("Caravan Island Frequency to Tangaroa", "Tangaroa Frequency")
|
||||
self.setLocationItem("Tangaroa Frequency to Varuna Point", "Varuna Point Frequency")
|
||||
self.setLocationItem("Varuna Point Frequency to Temperance", "Temperance Frequency")
|
||||
self.setLocationItem("Temperance Frequency to Utopia", "Utopia Frequency")
|
||||
elif self.world.island_frequency_locations[self.player] == 1:
|
||||
self.setLocationItemFromRegion("RadioTower", "Vasagatan Frequency")
|
||||
self.setLocationItemFromRegion("Vasagatan", "Balboa Island Frequency")
|
||||
self.setLocationItemFromRegion("BalboaIsland", "Caravan Island Frequency")
|
||||
self.setLocationItemFromRegion("CaravanIsland", "Tangaroa Frequency")
|
||||
self.setLocationItemFromRegion("Tangaroa", "Varuna Point Frequency")
|
||||
self.setLocationItemFromRegion("Varuna Point", "Temperance Frequency")
|
||||
self.setLocationItemFromRegion("Temperance", "Utopia Frequency")
|
||||
# Victory item
|
||||
self.world.get_location("Tangaroa Next Frequency", self.player).place_locked_item(
|
||||
self.world.get_location("Utopia Complete", self.player).place_locked_item(
|
||||
RaftItem("Victory", ItemClassification.progression, None, player=self.player))
|
||||
|
||||
def setLocationItem(self, location: str, itemName: str):
|
||||
@@ -151,6 +162,12 @@ class RaftWorld(World):
|
||||
self.world.itempool.remove(itemToUse)
|
||||
location = random.choice(list(loc for loc in location_table if loc["region"] == region))
|
||||
self.world.get_location(location["name"], self.player).place_locked_item(itemToUse)
|
||||
|
||||
def fill_slot_data(self):
|
||||
return {
|
||||
"IslandGenerationDistance": self.world.island_generation_distance[self.player].value,
|
||||
"ExpensiveResearch": self.world.expensive_research[self.player].value
|
||||
}
|
||||
|
||||
def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
|
||||
ret = Region(name, RegionType.Generic, name, player)
|
||||
|
||||
@@ -5,10 +5,10 @@ The player settings page for this game is located <a href="../player-settings">h
|
||||
you need to configure and export a config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
All of the items from the Research Table, as well as all the note/blueprint pickups from story islands, are changed to location checks. Blueprint items themselves are never given. The Research Table recipes will *remove* the researched items for that recipe once learned, meaning many more resources must be put into the Research Table to get all the unlocks from it.
|
||||
All of the items from the Research Table, as well as all the note/blueprint/character pickups from story islands, are changed to location checks. Blueprint items themselves are never given when receiving a blueprint.
|
||||
|
||||
## What is the goal of Raft when randomized?
|
||||
The goal remains the same: To pick up the note that has the frequency for the next unreleased story island from Tangaroa.
|
||||
The goal remains the same: To complete the game by getting to the end of the game and finishing Utopia.
|
||||
|
||||
## Which items can be in another player's world?
|
||||
All of the craftable items from the Research Table and Blueprints, as well as frequencies. Since there are more locations in Raft than there are items to receive, Resource Packs with basic earlygame materials and/or duplicate items may be added to the item pool (configurable).
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user