Compare commits

..

3 Commits

Author SHA1 Message Date
NewSoupVi
e92bd65f08 Simplify sentence 2025-09-09 01:00:23 +02:00
NewSoupVi
3c7331b206 Reword 2025-09-09 00:58:55 +02:00
NewSoupVi
71475230ba Core: Only error in playthrough generation if game is not beatable
The current flow of accessibility works like this:

```
if fulfills_accessibility fails:
    if multiworld can be beaten:
        log a warning
    else:
        raise Exception

if playthrough is enabled:
    if any progression items are not reachable:
        raise Exception
```

This means that if you do a generation where the game is beatable but some full players' items are not reachable, it doesn't crash on accessibility check, but then crashes on playthrough. This means that **whether it crashes depends on whether you have playthrough enabled or not**.

Imo, erroring on something accessibility-related is outside of the scope of create_playthrough. Create_playthrough only needs to care about whether it can fulfill its own goal - Building a minimal playthrough to everyone's victory.
The actual accessibility check should take care of the accessibility.
2025-09-08 23:22:22 +02:00
182 changed files with 4456 additions and 9185 deletions

View File

@@ -9,14 +9,12 @@ on:
- 'setup.py'
- 'requirements.txt'
- '*.iss'
- 'worlds/*/archipelago.json'
pull_request:
paths:
- '.github/workflows/build.yml'
- 'setup.py'
- 'requirements.txt'
- '*.iss'
- 'worlds/*/archipelago.json'
workflow_dispatch:
env:

View File

@@ -1,154 +0,0 @@
name: Build and Publish Docker Images
on:
push:
paths:
- "**"
- "!docs/**"
- "!deploy/**"
- "!setup.py"
- "!.gitignore"
- "!.github/workflows/**"
- ".github/workflows/docker.yml"
branches:
- "*"
tags:
- "v?[0-9]+.[0-9]+.[0-9]*"
workflow_dispatch:
env:
REGISTRY: ghcr.io
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
image-name: ${{ steps.image.outputs.name }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
package-name: ${{ steps.package.outputs.name }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set lowercase image name
id: image
run: |
echo "name=${GITHUB_REPOSITORY,,}" >> $GITHUB_OUTPUT
- name: Set package name
id: package
run: |
echo "name=$(basename ${GITHUB_REPOSITORY,,})" >> $GITHUB_OUTPUT
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ steps.image.outputs.name }}
tags: |
type=ref,event=branch,enable={{is_not_default_branch}}
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=nightly,enable={{is_default_branch}}
- name: Compute final tags
id: final-tags
run: |
readarray -t tags <<< "${{ steps.meta.outputs.tags }}"
if [[ "${{ github.ref_type }}" == "tag" ]]; then
tag="${{ github.ref_name }}"
if [[ "$tag" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
full_latest="${{ env.REGISTRY }}/${{ steps.image.outputs.name }}:latest"
# Check if latest is already in tags to avoid duplicates
if ! printf '%s\n' "${tags[@]}" | grep -q "^$full_latest$"; then
tags+=("$full_latest")
fi
fi
fi
# Set multiline output
echo "tags<<EOF" >> $GITHUB_OUTPUT
printf '%s\n' "${tags[@]}" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
build:
needs: prepare
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
strategy:
matrix:
include:
- platform: amd64
runner: ubuntu-latest
suffix: amd64
cache-scope: amd64
- platform: arm64
runner: ubuntu-24.04-arm
suffix: arm64
cache-scope: arm64
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Compute suffixed tags
id: tags
run: |
readarray -t tags <<< "${{ needs.prepare.outputs.tags }}"
suffixed=()
for t in "${tags[@]}"; do
suffixed+=("$t-${{ matrix.suffix }}")
done
echo "tags=$(IFS=','; echo "${suffixed[*]}")" >> $GITHUB_OUTPUT
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: linux/${{ matrix.platform }}
push: true
tags: ${{ steps.tags.outputs.tags }}
labels: ${{ needs.prepare.outputs.labels }}
cache-from: type=gha,scope=${{ matrix.cache-scope }}
cache-to: type=gha,mode=max,scope=${{ matrix.cache-scope }}
provenance: false
manifest:
needs: [prepare, build]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create and push multi-arch manifest
run: |
readarray -t tag_array <<< "${{ needs.prepare.outputs.tags }}"
for tag in "${tag_array[@]}"; do
docker manifest create "$tag" \
"$tag-amd64" \
"$tag-arm64"
docker manifest push "$tag"
done

View File

@@ -12,6 +12,7 @@ env:
jobs:
labeler:
name: 'Apply content-based labels'
if: github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'synchronize'
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v5

View File

@@ -1,24 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Build APWorld" type="PythonConfigurationType" factoryName="Python">
<module name="Archipelago" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<option name="SCRIPT_NAME" value="$ContentRoot$/Launcher.py" />
<option name="PARAMETERS" value="\&quot;Build APWorlds\&quot;" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
</component>

View File

@@ -261,7 +261,6 @@ class MultiWorld():
"local_items": set(item_link.get("local_items", [])),
"non_local_items": set(item_link.get("non_local_items", [])),
"link_replacement": replacement_prio.index(item_link["link_replacement"]),
"skip_if_solo": item_link.get("skip_if_solo", False),
}
for _name, item_link in item_links.items():
@@ -285,8 +284,6 @@ class MultiWorld():
for group_name, item_link in item_links.items():
game = item_link["game"]
if item_link["skip_if_solo"] and len(item_link["players"]) == 1:
continue
group_id, group = self.add_group(group_name, game, set(item_link["players"]))
group["item_pool"] = item_link["item_pool"]
@@ -1346,7 +1343,8 @@ class Region:
for entrance in self.entrances: # BFS might be better here, trying DFS for now.
return entrance.parent_region.get_connecting_entrance(is_main_entrance)
def add_locations(self, locations: Mapping[str, int | None], location_type: type[Location] | None = None) -> None:
def add_locations(self, locations: Dict[str, Optional[int]],
location_type: Optional[type[Location]] = None) -> None:
"""
Adds locations to the Region object, where location_type is your Location class and locations is a dict of
location names to address.
@@ -1434,8 +1432,8 @@ class Region:
entrance.connect(self)
return entrance
def add_exits(self, exits: Iterable[str] | Mapping[str, str | None],
rules: Mapping[str, Callable[[CollectionState], bool]] | None = None) -> List[Entrance]:
def add_exits(self, exits: Union[Iterable[str], Dict[str, Optional[str]]],
rules: Dict[str, Callable[[CollectionState], bool]] = None) -> List[Entrance]:
"""
Connects current region to regions in exit dictionary. Passed region names must exist first.
@@ -1443,7 +1441,7 @@ class Region:
created entrances will be named "self.name -> connecting_region"
:param rules: rules for the exits from this region. format is {"connecting_region": rule}
"""
if not isinstance(exits, Mapping):
if not isinstance(exits, Dict):
exits = dict.fromkeys(exits)
return [
self.connect(
@@ -1721,9 +1719,10 @@ class Spoiler:
logging.debug('The following items could not be reached: %s', ['%s (Player %d) at %s (Player %d)' % (
location.item.name, location.item.player, location.name, location.player) for location in
sphere_candidates])
if any([multiworld.worlds[location.item.player].options.accessibility != 'minimal' for location in sphere_candidates]):
raise RuntimeError(f'Not all progression items reachable ({sphere_candidates}). '
f'Something went terribly wrong here.')
if not multiworld.has_beaten_game(state):
raise RuntimeError("During playthrough generation, the game was determined to be unbeatable. "
"Something went terribly wrong here. "
f"Unreachable progression items: {sphere_candidates}")
else:
self.unreachables = sphere_candidates
break
@@ -1857,9 +1856,6 @@ class Spoiler:
Utils.__version__, self.multiworld.seed))
outfile.write('Filling Algorithm: %s\n' % self.multiworld.algorithm)
outfile.write('Players: %d\n' % self.multiworld.players)
if self.multiworld.players > 1:
loc_count = len([loc for loc in self.multiworld.get_locations() if not loc.is_event])
outfile.write('Total Location Count: %d\n' % loc_count)
outfile.write(f'Plando Options: {self.multiworld.plando_options}\n')
AutoWorld.call_stage(self.multiworld, "write_spoiler_header", outfile)
@@ -1868,9 +1864,6 @@ class Spoiler:
outfile.write('\nPlayer %d: %s\n' % (player, self.multiworld.get_player_name(player)))
outfile.write('Game: %s\n' % self.multiworld.game[player])
loc_count = len([loc for loc in self.multiworld.get_locations(player) if not loc.is_event])
outfile.write('Location Count: %d\n' % loc_count)
for f_option, option in self.multiworld.worlds[player].options_dataclass.type_hints.items():
write_option(f_option, option)

View File

@@ -856,9 +856,9 @@ async def server_loop(ctx: CommonContext, address: typing.Optional[str] = None)
server_url = urllib.parse.urlparse(address)
if server_url.username:
ctx.username = urllib.parse.unquote(server_url.username)
ctx.username = server_url.username
if server_url.password:
ctx.password = urllib.parse.unquote(server_url.password)
ctx.password = server_url.password
def reconnect_hint() -> str:
return ", type /connect to reconnect" if ctx.server_address else ""

View File

@@ -129,10 +129,6 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
for i, location in enumerate(placements))
for (i, location, unsafe) in swap_attempts:
placed_item = location.item
if item_to_place == placed_item:
# The number of allowed swaps is limited, so do not allow a swap of an item with a copy of
# itself.
continue
# Unplaceable items can sometimes be swapped infinitely. Limit the
# number of times we will swap an individual item to prevent this
swap_count = swapped_items[placed_item.player, placed_item.name, unsafe]

View File

@@ -486,22 +486,7 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
if required_plando_options:
raise Exception(f"Settings reports required plando module {str(required_plando_options)}, "
f"which is not enabled.")
games = requirements.get("game", {})
for game, version in games.items():
if game not in AutoWorldRegister.world_types:
continue
if not version:
raise Exception(f"Invalid version for game {game}: {version}.")
if isinstance(version, str):
version = {"min": version}
if "min" in version and tuplize_version(version["min"]) > AutoWorldRegister.world_types[game].world_version:
raise Exception(f"Settings reports required version of world \"{game}\" is at least {version['min']}, "
f"however world is of version "
f"{AutoWorldRegister.world_types[game].world_version.as_simple_string()}.")
if "max" in version and tuplize_version(version["max"]) < AutoWorldRegister.world_types[game].world_version:
raise Exception(f"Settings reports required version of world \"{game}\" is no later than {version['max']}, "
f"however world is of version "
f"{AutoWorldRegister.world_types[game].world_version.as_simple_string()}.")
ret = argparse.Namespace()
for option_key in Options.PerGameCommonOptions.type_hints:
if option_key in weights and option_key not in Options.CommonOptions.type_hints:

9
KH1Client.py Normal file
View File

@@ -0,0 +1,9 @@
if __name__ == '__main__':
import ModuleUpdate
ModuleUpdate.update()
import Utils
Utils.init_logging("KH1Client", exception_logger="Client")
from worlds.kh1.Client import launch
launch()

8
KH2Client.py Normal file
View File

@@ -0,0 +1,8 @@
import ModuleUpdate
import Utils
from worlds.kh2.Client import launch
ModuleUpdate.update()
if __name__ == '__main__':
Utils.init_logging("KH2Client", exception_logger="Client")
launch()

View File

@@ -3,6 +3,9 @@ ModuleUpdate.update()
import Utils
if __name__ == "__main__":
Utils.init_logging("LinksAwakeningContext", exception_logger="Client")
import asyncio
import base64
import binascii
@@ -23,14 +26,16 @@ import typing
from CommonClient import (CommonContext, get_base_parser, gui_enabled, logger,
server_loop)
from NetUtils import ClientStatus
from . import LinksAwakeningWorld
from .Common import BASE_ID as LABaseID
from .GpsTracker import GpsTracker
from .TrackerConsts import storage_key
from .ItemTracker import ItemTracker
from .LADXR.checkMetadata import checkMetadataTable
from .Locations import get_locations_to_id, meta_to_name
from .Tracker import LocationTracker, MagpieBridge, Check
from worlds.ladx import LinksAwakeningWorld
from worlds.ladx.Common import BASE_ID as LABaseID
from worlds.ladx.GpsTracker import GpsTracker
from worlds.ladx.TrackerConsts import storage_key
from worlds.ladx.ItemTracker import ItemTracker
from worlds.ladx.LADXR.checkMetadata import checkMetadataTable
from worlds.ladx.Locations import get_locations_to_id, meta_to_name
from worlds.ladx.Tracker import LocationTracker, MagpieBridge, Check
class GameboyException(Exception):
pass
@@ -407,10 +412,10 @@ class LinksAwakeningClient():
status = (await self.gameboy.async_read_memory_safe(LAClientConstants.wLinkStatusBits))[0]
item_id -= LABaseID
# The player name table only goes up to 101, so don't go past that
# The player name table only goes up to 100, so don't go past that
# Even if it didn't, the remote player _index_ byte is just a byte, so 255 max
if from_player > 101:
from_player = 101
if from_player > 100:
from_player = 100
next_index += 1
self.gameboy.write_memory(LAClientConstants.wLinkGiveItem, [
@@ -755,44 +760,42 @@ def run_game(romfile: str) -> None:
except FileNotFoundError:
logger.error(f"Couldn't launch ROM, {args[0]} is missing")
def launch(*launch_args):
async def main():
parser = get_base_parser(description="Link's Awakening Client.")
parser.add_argument("--url", help="Archipelago connection url")
parser.add_argument("--no-magpie", dest='magpie', default=True, action='store_false', help="Disable magpie bridge")
parser.add_argument('diff_file', default="", type=str, nargs="?",
help='Path to a .apladx Archipelago Binary Patch file')
async def main():
parser = get_base_parser(description="Link's Awakening Client.")
parser.add_argument("--url", help="Archipelago connection url")
parser.add_argument("--no-magpie", dest='magpie', default=True, action='store_false', help="Disable magpie bridge")
parser.add_argument('diff_file', default="", type=str, nargs="?",
help='Path to a .apladx Archipelago Binary Patch file')
args = parser.parse_args(launch_args)
args = parser.parse_args()
if args.diff_file:
import Patch
logger.info("patch file was supplied - creating rom...")
meta, rom_file = Patch.create_rom_file(args.diff_file)
if "server" in meta and not args.connect:
args.connect = meta["server"]
logger.info(f"wrote rom file to {rom_file}")
if args.diff_file:
import Patch
logger.info("patch file was supplied - creating rom...")
meta, rom_file = Patch.create_rom_file(args.diff_file)
if "server" in meta and not args.connect:
args.connect = meta["server"]
logger.info(f"wrote rom file to {rom_file}")
ctx = LinksAwakeningContext(args.connect, args.password, args.magpie)
ctx = LinksAwakeningContext(args.connect, args.password, args.magpie)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
# TODO: nothing about the lambda about has to be in a lambda
ctx.la_task = create_task_log_exception(ctx.run_game_loop())
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
# TODO: nothing about the lambda about has to be in a lambda
ctx.la_task = create_task_log_exception(ctx.run_game_loop())
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
# Down below run_gui so that we get errors out of the process
if args.diff_file:
run_game(rom_file)
# Down below run_gui so that we get errors out of the process
if args.diff_file:
run_game(rom_file)
await ctx.exit_event.wait()
await ctx.shutdown()
Utils.init_logging("LinksAwakeningContext", exception_logger="Client")
await ctx.exit_event.wait()
await ctx.shutdown()
if __name__ == '__main__':
colorama.just_fix_windows_console()
asyncio.run(main())
colorama.deinit()

11
Main.py
View File

@@ -54,17 +54,12 @@ def main(args, seed=None, baked_server_options: dict[str, object] | None = None)
logger.info(f"Found {len(AutoWorld.AutoWorldRegister.world_types)} World Types:")
longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types)
world_classes = AutoWorld.AutoWorldRegister.world_types.values()
version_count = max(len(cls.world_version.as_simple_string()) for cls in world_classes)
item_count = len(str(max(len(cls.item_names) for cls in world_classes)))
location_count = len(str(max(len(cls.location_names) for cls in world_classes)))
item_count = len(str(max(len(cls.item_names) for cls in AutoWorld.AutoWorldRegister.world_types.values())))
location_count = len(str(max(len(cls.location_names) for cls in AutoWorld.AutoWorldRegister.world_types.values())))
for name, cls in AutoWorld.AutoWorldRegister.world_types.items():
if not cls.hidden and len(cls.item_names) > 0:
logger.info(f" {name:{longest_name}}: "
f"v{cls.world_version.as_simple_string():{version_count}} | "
f"Items: {len(cls.item_names):{item_count}} | "
logger.info(f" {name:{longest_name}}: Items: {len(cls.item_names):{item_count}} | "
f"Locations: {len(cls.location_names):{location_count}}")
del item_count, location_count

View File

@@ -32,7 +32,7 @@ if typing.TYPE_CHECKING:
import colorama
import websockets
from websockets.extensions.permessage_deflate import PerMessageDeflate, ServerPerMessageDeflateFactory
from websockets.extensions.permessage_deflate import PerMessageDeflate
try:
# ponyorm is a requirement for webhost, not default server, so may not be importable
from pony.orm.dbapiprovider import OperationalError
@@ -50,15 +50,6 @@ from BaseClasses import ItemClassification
min_client_version = Version(0, 5, 0)
colorama.just_fix_windows_console()
no_version = Version(0, 0, 0)
assert isinstance(no_version, tuple) # assert immutable
server_per_message_deflate_factory = ServerPerMessageDeflateFactory(
server_max_window_bits=11,
client_max_window_bits=11,
compress_settings={"memLevel": 4},
)
def remove_from_list(container, value):
try:
@@ -134,31 +125,8 @@ def get_saving_second(seed_name: str, interval: int = 60) -> int:
class Client(Endpoint):
__slots__ = (
"__weakref__",
"version",
"auth",
"team",
"slot",
"send_index",
"tags",
"messageprocessor",
"ctx",
"remote_items",
"remote_start_inventory",
"no_items",
"no_locations",
"no_text",
)
version: Version
auth: bool
team: int | None
slot: int | None
send_index: int
tags: list[str]
messageprocessor: ClientMessageProcessor
ctx: weakref.ref[Context]
version = Version(0, 0, 0)
tags: typing.List[str]
remote_items: bool
remote_start_inventory: bool
no_items: bool
@@ -167,7 +135,6 @@ class Client(Endpoint):
def __init__(self, socket: "ServerConnection", ctx: Context) -> None:
super().__init__(socket)
self.version = no_version
self.auth = False
self.team = None
self.slot = None
@@ -175,11 +142,6 @@ class Client(Endpoint):
self.tags = []
self.messageprocessor = client_message_processor(ctx, self)
self.ctx = weakref.ref(ctx)
self.remote_items = False
self.remote_start_inventory = False
self.no_items = False
self.no_locations = False
self.no_text = False
@property
def items_handling(self):
@@ -217,7 +179,6 @@ class Context:
"release_mode": str,
"remaining_mode": str,
"collect_mode": str,
"countdown_mode": str,
"item_cheat": bool,
"compatibility": int}
# team -> slot id -> list of clients authenticated to slot.
@@ -247,8 +208,8 @@ class Context:
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
hint_cost: int, item_cheat: bool, release_mode: str = "disabled", collect_mode="disabled",
countdown_mode: str = "auto", remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0,
compatibility: int = 2, log_network: bool = False, logger: logging.Logger = logging.getLogger()):
remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2,
log_network: bool = False, logger: logging.Logger = logging.getLogger()):
self.logger = logger
super(Context, self).__init__()
self.slot_info = {}
@@ -281,7 +242,6 @@ class Context:
self.release_mode: str = release_mode
self.remaining_mode: str = remaining_mode
self.collect_mode: str = collect_mode
self.countdown_mode: str = countdown_mode
self.item_cheat = item_cheat
self.exit_event = asyncio.Event()
self.client_activity_timers: typing.Dict[
@@ -667,7 +627,6 @@ class Context:
"server_password": self.server_password, "password": self.password,
"release_mode": self.release_mode,
"remaining_mode": self.remaining_mode, "collect_mode": self.collect_mode,
"countdown_mode": self.countdown_mode,
"item_cheat": self.item_cheat, "compatibility": self.compatibility}
}
@@ -702,7 +661,6 @@ class Context:
self.release_mode = savedata["game_options"]["release_mode"]
self.remaining_mode = savedata["game_options"]["remaining_mode"]
self.collect_mode = savedata["game_options"]["collect_mode"]
self.countdown_mode = savedata["game_options"].get("countdown_mode", self.countdown_mode)
self.item_cheat = savedata["game_options"]["item_cheat"]
self.compatibility = savedata["game_options"]["compatibility"]
@@ -1177,13 +1135,8 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
ctx.save()
def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, str],
status: HintStatus | None = None) -> typing.List[Hint]:
"""
Collect a new hint for a given item id or name, with a given status.
If status is None (which is the default value), an automatic status will be determined from the item's quality.
"""
def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, str], auto_status: HintStatus) \
-> typing.List[Hint]:
hints = []
slots: typing.Set[int] = {slot}
for group_id, group in ctx.groups.items():
@@ -1199,39 +1152,25 @@ def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, st
else:
found = location_id in ctx.location_checks[team, finding_player]
entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "")
hint_status = status # Assign again because we're in a for loop
new_status = auto_status
if found:
hint_status = HintStatus.HINT_FOUND
elif hint_status is None:
if item_flags & ItemClassification.trap:
hint_status = HintStatus.HINT_AVOID
else:
hint_status = HintStatus.HINT_PRIORITY
hints.append(
Hint(receiving_player, finding_player, location_id, item_id, found, entrance, item_flags, hint_status)
)
new_status = HintStatus.HINT_FOUND
elif item_flags & ItemClassification.trap:
new_status = HintStatus.HINT_AVOID
hints.append(Hint(receiving_player, finding_player, location_id, item_id, found, entrance,
item_flags, new_status))
return hints
def collect_hint_location_name(ctx: Context, team: int, slot: int, location: str,
status: HintStatus | None = HintStatus.HINT_UNSPECIFIED) -> typing.List[Hint]:
"""
Collect a new hint for a given location name, with a given status (defaults to "unspecified").
If None is passed for the status, then an automatic status will be determined from the item's quality.
"""
def collect_hint_location_name(ctx: Context, team: int, slot: int, location: str, auto_status: HintStatus) \
-> typing.List[Hint]:
seeked_location: int = ctx.location_names_for_game(ctx.games[slot])[location]
return collect_hint_location_id(ctx, team, slot, seeked_location, status)
return collect_hint_location_id(ctx, team, slot, seeked_location, auto_status)
def collect_hint_location_id(ctx: Context, team: int, slot: int, seeked_location: int,
status: HintStatus | None = HintStatus.HINT_UNSPECIFIED) -> typing.List[Hint]:
"""
Collect a new hint for a given location id, with a given status (defaults to "unspecified").
If None is passed for the status, then an automatic status will be determined from the item's quality.
"""
def collect_hint_location_id(ctx: Context, team: int, slot: int, seeked_location: int, auto_status: HintStatus) \
-> typing.List[Hint]:
prev_hint = ctx.get_hint(team, slot, seeked_location)
if prev_hint:
return [prev_hint]
@@ -1241,16 +1180,13 @@ def collect_hint_location_id(ctx: Context, team: int, slot: int, seeked_location
found = seeked_location in ctx.location_checks[team, slot]
entrance = ctx.er_hint_data.get(slot, {}).get(seeked_location, "")
new_status = auto_status
if found:
status = HintStatus.HINT_FOUND
elif status is None:
if item_flags & ItemClassification.trap:
status = HintStatus.HINT_AVOID
else:
status = HintStatus.HINT_PRIORITY
return [Hint(receiving_player, slot, seeked_location, item_id, found, entrance, item_flags, status)]
new_status = HintStatus.HINT_FOUND
elif item_flags & ItemClassification.trap:
new_status = HintStatus.HINT_AVOID
return [Hint(receiving_player, slot, seeked_location, item_id, found, entrance, item_flags,
new_status)]
return []
@@ -1364,8 +1300,7 @@ class CommandProcessor(metaclass=CommandMeta):
argname += "=" + parameter.default
argtext += argname
argtext += " "
doctext = '\n '.join(inspect.getdoc(method).split('\n'))
s += f"{self.marker}{command} {argtext}\n {doctext}\n"
s += f"{self.marker}{command} {argtext}\n {method.__doc__}\n"
return s
def _cmd_help(self):
@@ -1394,6 +1329,19 @@ class CommandProcessor(metaclass=CommandMeta):
class CommonCommandProcessor(CommandProcessor):
ctx: Context
def _cmd_countdown(self, seconds: str = "10") -> bool:
"""Start a countdown in seconds"""
try:
timer = int(seconds, 10)
except ValueError:
timer = 10
else:
if timer > 60 * 60:
raise ValueError(f"{timer} is invalid. Maximum is 1 hour.")
async_start(countdown(self.ctx, timer))
return True
def _cmd_options(self):
"""List all current options. Warning: lists password."""
self.output("Current options:")
@@ -1535,23 +1483,6 @@ class ClientMessageProcessor(CommonCommandProcessor):
" You can ask the server admin for a /collect")
return False
def _cmd_countdown(self, seconds: str = "10") -> bool:
"""Start a countdown in seconds"""
if self.ctx.countdown_mode == "disabled" or \
self.ctx.countdown_mode == "auto" and len(self.ctx.player_names) >= 30:
self.output("Sorry, client countdowns have been disabled on this server. You can ask the server admin for a /countdown")
return False
try:
timer = int(seconds, 10)
except ValueError:
timer = 10
else:
if timer > 60 * 60:
raise ValueError(f"{timer} is invalid. Maximum is 1 hour.")
async_start(countdown(self.ctx, timer))
return True
def _cmd_remaining(self) -> bool:
"""List remaining items in your game, but not their location or recipient"""
if self.ctx.remaining_mode == "enabled":
@@ -1679,6 +1610,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
def get_hints(self, input_text: str, for_location: bool = False) -> bool:
points_available = get_client_points(self.ctx, self.client)
cost = self.ctx.get_hint_cost(self.client.slot)
auto_status = HintStatus.HINT_UNSPECIFIED if for_location else HintStatus.HINT_PRIORITY
if not input_text:
hints = {hint.re_check(self.ctx, self.client.team) for hint in
self.ctx.hints[self.client.team, self.client.slot]}
@@ -1704,9 +1636,9 @@ class ClientMessageProcessor(CommonCommandProcessor):
self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.")
hints = []
elif not for_location:
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_id)
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_id, auto_status)
else:
hints = collect_hint_location_id(self.ctx, self.client.team, self.client.slot, hint_id)
hints = collect_hint_location_id(self.ctx, self.client.team, self.client.slot, hint_id, auto_status)
else:
game = self.ctx.games[self.client.slot]
@@ -1726,18 +1658,16 @@ class ClientMessageProcessor(CommonCommandProcessor):
hints = []
for item_name in self.ctx.item_name_groups[game][hint_name]:
if item_name in self.ctx.item_names_for_game(game): # ensure item has an ID
hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item_name))
hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item_name, auto_status))
elif not for_location and hint_name in self.ctx.item_names_for_game(game): # item name
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name)
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name, auto_status)
elif hint_name in self.ctx.location_name_groups[game]: # location group name
hints = []
for loc_name in self.ctx.location_name_groups[game][hint_name]:
if loc_name in self.ctx.location_names_for_game(game):
hints.extend(
collect_hint_location_name(self.ctx, self.client.team, self.client.slot, loc_name)
)
hints.extend(collect_hint_location_name(self.ctx, self.client.team, self.client.slot, loc_name, auto_status))
else: # location name
hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name)
hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name, auto_status)
else:
self.output(response)
@@ -2015,7 +1945,8 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
target_item, target_player, flags = ctx.locations[client.slot][location]
if create_as_hint:
hints.extend(collect_hint_location_id(ctx, client.team, client.slot, location))
hints.extend(collect_hint_location_id(ctx, client.team, client.slot, location,
HintStatus.HINT_UNSPECIFIED))
locs.append(NetworkItem(target_item, location, target_player, flags))
ctx.notify_hints(client.team, hints, only_new=create_as_hint == 2, persist_even_if_found=True)
if locs and create_as_hint:
@@ -2307,19 +2238,6 @@ class ServerCommandProcessor(CommonCommandProcessor):
self.output(f"Could not find player {player_name} to collect")
return False
def _cmd_countdown(self, seconds: str = "10") -> bool:
"""Start a countdown in seconds"""
try:
timer = int(seconds, 10)
except ValueError:
timer = 10
else:
if timer > 60 * 60:
raise ValueError(f"{timer} is invalid. Maximum is 1 hour.")
async_start(countdown(self.ctx, timer))
return True
@mark_raw
def _cmd_release(self, player_name: str) -> bool:
"""Send out the remaining items from a player to their intended recipients."""
@@ -2441,9 +2359,9 @@ class ServerCommandProcessor(CommonCommandProcessor):
hints = []
for item_name_from_group in self.ctx.item_name_groups[game][item]:
if item_name_from_group in self.ctx.item_names_for_game(game): # ensure item has an ID
hints.extend(collect_hints(self.ctx, team, slot, item_name_from_group))
hints.extend(collect_hints(self.ctx, team, slot, item_name_from_group, HintStatus.HINT_PRIORITY))
else: # item name or id
hints = collect_hints(self.ctx, team, slot, item)
hints = collect_hints(self.ctx, team, slot, item, HintStatus.HINT_PRIORITY)
if hints:
self.ctx.notify_hints(team, hints)
@@ -2477,14 +2395,17 @@ class ServerCommandProcessor(CommonCommandProcessor):
if usable:
if isinstance(location, int):
hints = collect_hint_location_id(self.ctx, team, slot, location)
hints = collect_hint_location_id(self.ctx, team, slot, location,
HintStatus.HINT_UNSPECIFIED)
elif game in self.ctx.location_name_groups and location in self.ctx.location_name_groups[game]:
hints = []
for loc_name_from_group in self.ctx.location_name_groups[game][location]:
if loc_name_from_group in self.ctx.location_names_for_game(game):
hints.extend(collect_hint_location_name(self.ctx, team, slot, loc_name_from_group))
hints.extend(collect_hint_location_name(self.ctx, team, slot, loc_name_from_group,
HintStatus.HINT_UNSPECIFIED))
else:
hints = collect_hint_location_name(self.ctx, team, slot, location)
hints = collect_hint_location_name(self.ctx, team, slot, location,
HintStatus.HINT_UNSPECIFIED)
if hints:
self.ctx.notify_hints(team, hints)
else:
@@ -2512,11 +2433,6 @@ class ServerCommandProcessor(CommonCommandProcessor):
elif value_type == str and option_name.endswith("password"):
def value_type(input_text: str):
return None if input_text.lower() in {"null", "none", '""', "''"} else input_text
elif option_name == "countdown_mode":
valid_values = {"enabled", "disabled", "auto"}
if option_value.lower() not in valid_values:
self.output(f"Unrecognized {option_name} value '{option_value}', known: {', '.join(valid_values)}")
return False
elif value_type == str and option_name.endswith("mode"):
valid_values = {"goal", "enabled", "disabled"}
valid_values.update(("auto", "auto_enabled") if option_name != "remaining_mode" else [])
@@ -2604,13 +2520,6 @@ def parse_args() -> argparse.Namespace:
goal: !collect can be used after goal completion
auto-enabled: !collect is available and automatically triggered on goal completion
''')
parser.add_argument('--countdown_mode', default=defaults["countdown_mode"], nargs='?',
choices=['enabled', 'disabled', "auto"], help='''\
Select !countdown Accessibility. (default: %(default)s)
enabled: !countdown is always available
disabled: !countdown is never available
auto: !countdown is available for rooms with less than 30 players
''')
parser.add_argument('--remaining_mode', default=defaults["remaining_mode"], nargs='?',
choices=['enabled', 'disabled', "goal"], help='''\
Select !remaining Accessibility. (default: %(default)s)
@@ -2676,7 +2585,7 @@ async def main(args: argparse.Namespace):
ctx = Context(args.host, args.port, args.server_password, args.password, args.location_check_points,
args.hint_cost, not args.disable_item_cheat, args.release_mode, args.collect_mode,
args.countdown_mode, args.remaining_mode,
args.remaining_mode,
args.auto_shutdown, args.compatibility, args.log_network)
data_filename = args.multidata
@@ -2711,13 +2620,7 @@ async def main(args: argparse.Namespace):
ssl_context = load_server_cert(args.cert, args.cert_key) if args.cert else None
ctx.server = websockets.serve(
functools.partial(server, ctx=ctx),
host=ctx.host,
port=ctx.port,
ssl=ssl_context,
extensions=[server_per_message_deflate_factory],
)
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), host=ctx.host, port=ctx.port, ssl=ssl_context)
ip = args.host if args.host else Utils.get_public_ipv4()
logging.info('Hosting game at %s:%d (%s)' % (ip, ctx.port,
'No password' if not ctx.password else 'Password: %s' % ctx.password))

View File

@@ -174,8 +174,6 @@ decode = JSONDecoder(object_hook=_object_hook).decode
class Endpoint:
__slots__ = ("socket",)
socket: "ServerConnection"
def __init__(self, socket):

View File

@@ -1380,7 +1380,7 @@ class NonLocalItems(ItemSet):
class StartInventory(ItemDict):
"""Start with the specified amount of these items. Example: "Bomb: 1" """
"""Start with these items."""
verify_item_name = True
display_name = "Start Inventory"
rich_text_doc = True
@@ -1388,7 +1388,7 @@ class StartInventory(ItemDict):
class StartInventoryPool(StartInventory):
"""Start with the specified amount of these items and don't place them in the world. Example: "Bomb: 1"
"""Start with these items and don't place them in the world.
The game decides what the replacement items will be.
"""
@@ -1446,7 +1446,6 @@ class ItemLinks(OptionList):
Optional("local_items"): [And(str, len)],
Optional("non_local_items"): [And(str, len)],
Optional("link_replacement"): Or(None, bool),
Optional("skip_if_solo"): Or(None, bool),
}
])
@@ -1474,10 +1473,8 @@ class ItemLinks(OptionList):
super(ItemLinks, self).verify(world, player_name, plando_options)
existing_links = set()
for link in self.value:
link["name"] = link["name"].strip()[:16].strip()
if link["name"] in existing_links:
raise Exception(f"Item link names are limited to their first 16 characters and must be unique. "
f"You have more than one link named '{link['name']}'.")
raise Exception(f"You cannot have more than one link named {link['name']}.")
existing_links.add(link["name"])
pool = self.verify_items(link["item_pool"], link["name"], "item_pool", world)
@@ -1755,10 +1752,7 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
res = template.render(
option_groups=option_groups,
__version__=__version__,
game=game_name,
world_version=world.world_version.as_simple_string(),
yaml_dump=yaml_dump_scalar,
__version__=__version__, game=game_name, yaml_dump=yaml_dump_scalar,
dictify_range=dictify_range,
cleandoc=cleandoc,
)

View File

@@ -18,7 +18,7 @@ from json import loads, dumps
from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser
import Utils
import settings
from settings import Settings
from Utils import async_start
from MultiServer import mark_raw
if typing.TYPE_CHECKING:
@@ -286,7 +286,7 @@ class SNESState(enum.IntEnum):
def launch_sni() -> None:
sni_path = settings.get_settings().sni_options.sni_path
sni_path = Settings.sni_options.sni_path
if not os.path.isdir(sni_path):
sni_path = Utils.local_path(sni_path)
@@ -669,7 +669,7 @@ async def game_watcher(ctx: SNIContext) -> None:
async def run_game(romfile: str) -> None:
auto_start = settings.get_settings().sni_options.snes_rom_start
auto_start = Settings.sni_options.snes_rom_start
if auto_start is True:
import webbrowser
webbrowser.open(romfile)

View File

@@ -1,7 +1,6 @@
from __future__ import annotations
import asyncio
import concurrent.futures
import json
import typing
import builtins
@@ -36,7 +35,7 @@ if typing.TYPE_CHECKING:
def tuplize_version(version: str) -> Version:
return Version(*(int(piece) for piece in version.split(".")))
return Version(*(int(piece, 10) for piece in version.split(".")))
class Version(typing.NamedTuple):
@@ -323,13 +322,11 @@ def get_options() -> Settings:
return get_settings()
def persistent_store(category: str, key: str, value: typing.Any, force_store: bool = False):
def persistent_store(category: str, key: str, value: typing.Any):
path = user_path("_persistent_storage.yaml")
storage = persistent_load()
if not force_store and category in storage and key in storage[category] and storage[category][key] == value:
return # no changes necessary
category_dict = storage.setdefault(category, {})
category_dict[key] = value
path = user_path("_persistent_storage.yaml")
with open(path, "wt") as f:
f.write(dump(storage, Dumper=Dumper))
@@ -478,7 +475,7 @@ class RestrictedUnpickler(pickle.Unpickler):
mod = importlib.import_module(module)
obj = getattr(mod, name)
if issubclass(obj, (self.options_module.Option, self.options_module.PlandoConnection,
self.options_module.PlandoItem, self.options_module.PlandoText)):
self.options_module.PlandoText)):
return obj
# Forbid everything else.
raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden")
@@ -721,22 +718,13 @@ def get_intended_text(input_text: str, possible_answers) -> typing.Tuple[str, bo
def get_input_text_from_response(text: str, command: str) -> typing.Optional[str]:
"""
Parses the response text from `get_intended_text` to find the suggested input and autocomplete the command in
arguments with it.
:param text: The response text from `get_intended_text`.
:param command: The command to which the input text should be added. Must contain the prefix used by the command
(`!` or `/`).
:return: The command with the suggested input text appended, or None if no suggestion was found.
"""
if "did you mean " in text:
for question in ("Didn't find something that closely matches",
"Too many close matches"):
if text.startswith(question):
name = get_text_between(text, "did you mean '",
"'? (")
return f"{command} {name}"
return f"!{command} {name}"
elif text.startswith("Missing: "):
return text.replace("Missing: ", "!hint_location ")
return None
@@ -1139,40 +1127,3 @@ def is_iterable_except_str(obj: object) -> TypeGuard[typing.Iterable[typing.Any]
if isinstance(obj, str):
return False
return isinstance(obj, typing.Iterable)
class DaemonThreadPoolExecutor(concurrent.futures.ThreadPoolExecutor):
"""
ThreadPoolExecutor that uses daemonic threads that do not keep the program alive.
NOTE: use this with caution because killed threads will not properly clean up.
"""
def _adjust_thread_count(self):
# see upstream ThreadPoolExecutor for details
import threading
import weakref
from concurrent.futures.thread import _worker
if self._idle_semaphore.acquire(timeout=0):
return
def weakref_cb(_, q=self._work_queue):
q.put(None)
num_threads = len(self._threads)
if num_threads < self._max_workers:
thread_name = f"{self._thread_name_prefix or self}_{num_threads}"
t = threading.Thread(
name=thread_name,
target=_worker,
args=(
weakref.ref(self, weakref_cb),
self._work_queue,
self._initializer,
self._initargs,
),
daemon=True,
)
t.start()
self._threads.add(t)
# NOTE: don't add to _threads_queues so we don't block on shutdown

View File

@@ -109,13 +109,6 @@ if __name__ == "__main__":
logging.exception(e)
logging.warning("Could not update LttP sprites.")
app = get_app()
from worlds import AutoWorldRegister
# Update to only valid WebHost worlds
invalid_worlds = {name for name, world in AutoWorldRegister.world_types.items()
if not hasattr(world.web, "tutorials")}
if invalid_worlds:
logging.error(f"Following worlds not loaded as they are invalid for WebHost: {invalid_worlds}")
AutoWorldRegister.world_types = {k: v for k, v in AutoWorldRegister.world_types.items() if k not in invalid_worlds}
create_options_files()
copy_tutorials_files_to_static()
if app.config["SELFLAUNCH"]:

View File

@@ -11,53 +11,6 @@ from WebHostLib.models import Room
from WebHostLib.tracker import TrackerData
class PlayerAlias(TypedDict):
team: int
player: int
alias: str | None
class PlayerItemsReceived(TypedDict):
team: int
player: int
items: list[NetworkItem]
class PlayerChecksDone(TypedDict):
team: int
player: int
locations: list[int]
class TeamTotalChecks(TypedDict):
team: int
checks_done: int
class PlayerHints(TypedDict):
team: int
player: int
hints: list[Hint]
class PlayerTimer(TypedDict):
team: int
player: int
time: datetime | None
class PlayerStatus(TypedDict):
team: int
player: int
status: ClientStatus
class PlayerLocationsTotal(TypedDict):
team: int
player: int
total_locations: int
@api_endpoints.route("/tracker/<suuid:tracker>")
@cache.memoize(timeout=60)
def tracker_data(tracker: UUID) -> dict[str, Any]:
@@ -76,77 +29,122 @@ def tracker_data(tracker: UUID) -> dict[str, Any]:
all_players: dict[int, list[int]] = tracker_data.get_all_players()
player_aliases: list[PlayerAlias] = []
class PlayerAlias(TypedDict):
player: int
name: str | None
player_aliases: list[dict[str, int | list[PlayerAlias]]] = []
"""Slot aliases of all players."""
for team, players in all_players.items():
team_player_aliases: list[PlayerAlias] = []
team_aliases = {"team": team, "players": team_player_aliases}
player_aliases.append(team_aliases)
for player in players:
player_aliases.append({"team": team, "player": player, "alias": tracker_data.get_player_alias(team, player)})
team_player_aliases.append({"player": player, "alias": tracker_data.get_player_alias(team, player)})
player_items_received: list[PlayerItemsReceived] = []
class PlayerItemsReceived(TypedDict):
player: int
items: list[NetworkItem]
player_items_received: list[dict[str, int | list[PlayerItemsReceived]]] = []
"""Items received by each player."""
for team, players in all_players.items():
player_received_items: list[PlayerItemsReceived] = []
team_items_received = {"team": team, "players": player_received_items}
player_items_received.append(team_items_received)
for player in players:
player_items_received.append(
{"team": team, "player": player, "items": tracker_data.get_player_received_items(team, player)})
player_received_items.append(
{"player": player, "items": tracker_data.get_player_received_items(team, player)})
player_checks_done: list[PlayerChecksDone] = []
class PlayerChecksDone(TypedDict):
player: int
locations: list[int]
player_checks_done: list[dict[str, int | list[PlayerChecksDone]]] = []
"""ID of all locations checked by each player."""
for team, players in all_players.items():
per_player_checks: list[PlayerChecksDone] = []
team_checks_done = {"team": team, "players": per_player_checks}
player_checks_done.append(team_checks_done)
for player in players:
player_checks_done.append(
{"team": team, "player": player, "locations": sorted(tracker_data.get_player_checked_locations(team, player))})
per_player_checks.append(
{"player": player, "locations": sorted(tracker_data.get_player_checked_locations(team, player))})
total_checks_done: list[TeamTotalChecks] = [
total_checks_done: list[dict[str, int]] = [
{"team": team, "checks_done": checks_done}
for team, checks_done in tracker_data.get_team_locations_checked_count().items()
]
"""Total number of locations checked for the entire multiworld per team."""
hints: list[PlayerHints] = []
class PlayerHints(TypedDict):
player: int
hints: list[Hint]
hints: list[dict[str, int | list[PlayerHints]]] = []
"""Hints that all players have used or received."""
for team, players in tracker_data.get_all_slots().items():
per_player_hints: list[PlayerHints] = []
team_hints = {"team": team, "players": per_player_hints}
hints.append(team_hints)
for player in players:
player_hints = sorted(tracker_data.get_player_hints(team, player))
hints.append({"team": team, "player": player, "hints": player_hints})
slot_info = tracker_data.get_slot_info(player)
per_player_hints.append({"player": player, "hints": player_hints})
slot_info = tracker_data.get_slot_info(team, player)
# this assumes groups are always after players
if slot_info.type != SlotType.group:
continue
for member in slot_info.group_members:
hints[member - 1]["hints"] += player_hints
team_hints[member]["hints"] += player_hints
activity_timers: list[PlayerTimer] = []
class PlayerTimer(TypedDict):
player: int
time: datetime | None
activity_timers: list[dict[str, int | list[PlayerTimer]]] = []
"""Time of last activity per player. Returned as RFC 1123 format and null if no connection has been made."""
for team, players in all_players.items():
player_timers: list[PlayerTimer] = []
team_timers = {"team": team, "players": player_timers}
activity_timers.append(team_timers)
for player in players:
activity_timers.append({"team": team, "player": player, "time": None})
player_timers.append({"player": player, "time": None})
for (team, player), timestamp in tracker_data._multisave.get("client_activity_timers", []):
for entry in activity_timers:
if entry["team"] == team and entry["player"] == player:
entry["time"] = datetime.fromtimestamp(timestamp, timezone.utc)
break
client_activity_timers: tuple[tuple[int, int], float] = tracker_data._multisave.get("client_activity_timers", ())
for (team, player), timestamp in client_activity_timers:
# use index since we can rely on order
# FIX: key is "players" (not "player_timers")
activity_timers[team]["players"][player - 1]["time"] = datetime.fromtimestamp(timestamp, timezone.utc)
connection_timers: list[PlayerTimer] = []
connection_timers: list[dict[str, int | list[PlayerTimer]]] = []
"""Time of last connection per player. Returned as RFC 1123 format and null if no connection has been made."""
for team, players in all_players.items():
player_timers: list[PlayerTimer] = []
team_connection_timers = {"team": team, "players": player_timers}
connection_timers.append(team_connection_timers)
for player in players:
connection_timers.append({"team": team, "player": player, "time": None})
player_timers.append({"player": player, "time": None})
for (team, player), timestamp in tracker_data._multisave.get("client_connection_timers", []):
# find the matching entry
for entry in connection_timers:
if entry["team"] == team and entry["player"] == player:
entry["time"] = datetime.fromtimestamp(timestamp, timezone.utc)
break
client_connection_timers: tuple[tuple[int, int], float] = tracker_data._multisave.get(
"client_connection_timers", ())
for (team, player), timestamp in client_connection_timers:
connection_timers[team]["players"][player - 1]["time"] = datetime.fromtimestamp(timestamp, timezone.utc)
player_status: list[PlayerStatus] = []
class PlayerStatus(TypedDict):
player: int
status: ClientStatus
player_status: list[dict[str, int | list[PlayerStatus]]] = []
"""The current client status for each player."""
for team, players in all_players.items():
player_statuses: list[PlayerStatus] = []
team_status = {"team": team, "players": player_statuses}
player_status.append(team_status)
for player in players:
player_status.append({"team": team, "player": player, "status": tracker_data.get_player_client_status(team, player)})
player_statuses.append({"player": player, "status": tracker_data.get_player_client_status(team, player)})
return {
**get_static_tracker_data(room),
"aliases": player_aliases,
"player_items_received": player_items_received,
"player_checks_done": player_checks_done,
@@ -155,87 +153,80 @@ def tracker_data(tracker: UUID) -> dict[str, Any]:
"activity_timers": activity_timers,
"connection_timers": connection_timers,
"player_status": player_status,
"datapackage": tracker_data._multidata["datapackage"],
}
class PlayerGroups(TypedDict):
slot: int
name: str
members: list[int]
class PlayerSlotData(TypedDict):
player: int
slot_data: dict[str, Any]
@api_endpoints.route("/static_tracker/<suuid:tracker>")
@cache.memoize(timeout=300)
def static_tracker_data(tracker: UUID) -> dict[str, Any]:
@cache.memoize()
def get_static_tracker_data(room: Room) -> dict[str, Any]:
"""
Outputs json data to <root_path>/api/static_tracker/<id of current session tracker>.
:param tracker: UUID of current session tracker.
:return: Static tracking data for all players in the room. Typing and docstrings describe the format of each value.
Builds and caches the static data for this active session tracker, so that it doesn't need to be recalculated.
"""
room: Room | None = Room.get(tracker=tracker)
if not room:
abort(404)
tracker_data = TrackerData(room)
all_players: dict[int, list[int]] = tracker_data.get_all_players()
groups: list[PlayerGroups] = []
class PlayerGroups(TypedDict):
slot: int
name: str
members: list[int]
groups: list[dict[str, int | list[PlayerGroups]]] = []
"""The Slot ID of groups and the IDs of the group's members."""
for team, players in tracker_data.get_all_slots().items():
groups_in_team: list[PlayerGroups] = []
team_groups = {"team": team, "groups": groups_in_team}
groups.append(team_groups)
for player in players:
slot_info = tracker_data.get_slot_info(player)
slot_info = tracker_data.get_slot_info(team, player)
if slot_info.type != SlotType.group or not slot_info.group_members:
continue
groups.append(
groups_in_team.append(
{
"slot": player,
"name": slot_info.name,
"members": list(slot_info.group_members),
})
break
class PlayerName(TypedDict):
player: int
name: str
player_locations_total: list[PlayerLocationsTotal] = []
player_names: list[dict[str, str | list[PlayerName]]] = []
"""Slot names of all players."""
for team, players in all_players.items():
per_team_player_names: list[PlayerName] = []
team_names = {"team": team, "players": per_team_player_names}
player_names.append(team_names)
for player in players:
player_locations_total.append(
{"team": team, "player": player, "total_locations": len(tracker_data.get_player_locations(player))})
per_team_player_names.append({"player": player, "name": tracker_data.get_player_name(team, player)})
class PlayerGame(TypedDict):
player: int
game: str
games: list[dict[str, int | list[PlayerGame]]] = []
"""The game each player is playing."""
for team, players in all_players.items():
player_games: list[PlayerGame] = []
team_games = {"team": team, "players": player_games}
games.append(team_games)
for player in players:
player_games.append({"player": player, "game": tracker_data.get_player_game(team, player)})
class PlayerSlotData(TypedDict):
player: int
slot_data: dict[str, Any]
slot_data: list[dict[str, int | list[PlayerSlotData]]] = []
"""Slot data for each player."""
for team, players in all_players.items():
player_slot_data: list[PlayerSlotData] = []
team_slot_data = {"team": team, "players": player_slot_data}
slot_data.append(team_slot_data)
for player in players:
player_slot_data.append({"player": player, "slot_data": tracker_data.get_slot_data(team, player)})
return {
"groups": groups,
"datapackage": tracker_data._multidata["datapackage"],
"player_locations_total": player_locations_total,
"slot_data": slot_data,
}
# It should be exceedingly rare that slot data is needed, so it's separated out.
@api_endpoints.route("/slot_data_tracker/<suuid:tracker>")
@cache.memoize(timeout=300)
def tracker_slot_data(tracker: UUID) -> list[PlayerSlotData]:
"""
Outputs json data to <root_path>/api/slot_data_tracker/<id of current session tracker>.
:param tracker: UUID of current session tracker.
:return: Slot data for all players in the room. Typing completely arbitrary per game.
"""
room: Room | None = Room.get(tracker=tracker)
if not room:
abort(404)
tracker_data = TrackerData(room)
all_players: dict[int, list[int]] = tracker_data.get_all_players()
slot_data: list[PlayerSlotData] = []
"""Slot data for each player."""
for team, players in all_players.items():
for player in players:
slot_data.append({"player": player, "slot_data": tracker_data.get_slot_data(player)})
break
return slot_data

View File

@@ -36,39 +36,25 @@ def handle_generation_failure(result: BaseException):
logging.exception(e)
def _mp_gen_game(
gen_options: dict,
meta: dict[str, Any] | None = None,
owner=None,
sid=None,
timeout: int|None = None,
) -> PrimaryKey | None:
def _mp_gen_game(gen_options: dict, meta: dict[str, Any] | None = None, owner=None, sid=None) -> PrimaryKey | None:
from setproctitle import setproctitle
setproctitle(f"Generator ({sid})")
try:
return gen_game(gen_options, meta=meta, owner=owner, sid=sid, timeout=timeout)
finally:
setproctitle(f"Generator (idle)")
res = gen_game(gen_options, meta=meta, owner=owner, sid=sid)
setproctitle(f"Generator (idle)")
return res
def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation, timeout: int|None) -> None:
def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation):
try:
meta = json.loads(generation.meta)
options = restricted_loads(generation.options)
logging.info(f"Generating {generation.id} for {len(options)} players")
pool.apply_async(
_mp_gen_game,
(options,),
{
"meta": meta,
"sid": generation.id,
"owner": generation.owner,
"timeout": timeout,
},
handle_generation_success,
handle_generation_failure,
)
pool.apply_async(_mp_gen_game, (options,),
{"meta": meta,
"sid": generation.id,
"owner": generation.owner},
handle_generation_success, handle_generation_failure)
except Exception as e:
generation.state = STATE_ERROR
commit()
@@ -149,7 +135,6 @@ def autogen(config: dict):
with multiprocessing.Pool(config["GENERATORS"], initializer=init_generator,
initargs=(config,), maxtasksperchild=10) as generator_pool:
job_time = config["JOB_TIME"]
with db_session:
to_start = select(generation for generation in Generation if generation.state == STATE_STARTED)
@@ -160,7 +145,7 @@ def autogen(config: dict):
if sid:
generation.delete()
else:
launch_generator(generator_pool, generation, timeout=job_time)
launch_generator(generator_pool, generation)
commit()
select(generation for generation in Generation if generation.state == STATE_ERROR).delete()
@@ -172,7 +157,7 @@ def autogen(config: dict):
generation for generation in Generation
if generation.state == STATE_QUEUED).for_update()
for generation in to_start:
launch_generator(generator_pool, generation, timeout=job_time)
launch_generator(generator_pool, generation)
except AlreadyRunningException:
logging.info("Autogen reports as already running, not starting another.")

View File

@@ -19,10 +19,7 @@ from pony.orm import commit, db_session, select
import Utils
from MultiServer import (
Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor, load_server_cert,
server_per_message_deflate_factory,
)
from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor, load_server_cert
from Utils import restricted_loads, cache_argsless
from .locker import Locker
from .models import Command, GameDataPackage, Room, db
@@ -100,7 +97,6 @@ class WebHostContext(Context):
self.main_loop.call_soon_threadsafe(cmdprocessor, command.commandtext)
command.delete()
commit()
del commands
time.sleep(5)
@db_session
@@ -150,13 +146,13 @@ class WebHostContext(Context):
self.location_name_groups = static_location_name_groups
return self._load(multidata, game_data_packages, True)
@db_session
def init_save(self, enabled: bool = True):
self.saving = enabled
if self.saving:
with db_session:
savegame_data = Room.get(id=self.room_id).multisave
if savegame_data:
self.set_save(restricted_loads(Room.get(id=self.room_id).multisave))
savegame_data = Room.get(id=self.room_id).multisave
if savegame_data:
self.set_save(restricted_loads(Room.get(id=self.room_id).multisave))
self._start_async_saving(atexit_save=False)
threading.Thread(target=self.listen_to_db_commands, daemon=True).start()
@@ -286,12 +282,8 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
assert ctx.server is None
try:
ctx.server = websockets.serve(
functools.partial(server, ctx=ctx),
ctx.host,
ctx.port,
ssl=get_ssl_context(),
extensions=[server_per_message_deflate_factory],
)
functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=get_ssl_context())
await ctx.server
except OSError: # likely port in use
ctx.server = websockets.serve(
@@ -312,7 +304,6 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
with db_session:
room = Room.get(id=ctx.room_id)
room.last_port = port
del room
else:
ctx.logger.exception("Could not determine port. Likely hosting failure.")
with db_session:
@@ -331,7 +322,6 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
with db_session:
room = Room.get(id=room_id)
room.last_port = -1
del room
logger.exception(e)
raise
else:
@@ -343,12 +333,11 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
ctx.save_dirty = False # make sure the saving thread does not write to DB after final wakeup
ctx.exit_event.set() # make sure the saving thread stops at some point
# NOTE: async saving should probably be an async task and could be merged with shutdown_task
with db_session:
with (db_session):
# ensure the Room does not spin up again on its own, minute of safety buffer
room = Room.get(id=room_id)
room.last_activity = datetime.datetime.utcnow() - \
datetime.timedelta(minutes=1, seconds=room.timeout)
del room
logging.info(f"Shutting down room {room_id} on {name}.")
finally:
await asyncio.sleep(5)

View File

@@ -14,7 +14,7 @@ from pony.orm import commit, db_session
from BaseClasses import get_seed, seeddigits
from Generate import PlandoOptions, handle_name, mystery_argparse
from Main import main as ERmain
from Utils import __version__, restricted_dumps, DaemonThreadPoolExecutor
from Utils import __version__, restricted_dumps
from WebHostLib import app
from settings import ServerOptions, GeneratorOptions
from .check import get_yaml_data, roll_options
@@ -33,7 +33,6 @@ def get_meta(options_source: dict, race: bool = False) -> dict[str, list[str] |
"release_mode": str(options_source.get("release_mode", ServerOptions.release_mode)),
"remaining_mode": str(options_source.get("remaining_mode", ServerOptions.remaining_mode)),
"collect_mode": str(options_source.get("collect_mode", ServerOptions.collect_mode)),
"countdown_mode": str(options_source.get("countdown_mode", ServerOptions.countdown_mode)),
"item_cheat": bool(int(options_source.get("item_cheat", not ServerOptions.disable_item_cheat))),
"server_password": str(options_source.get("server_password", None)),
}
@@ -73,10 +72,6 @@ def generate(race=False):
return render_template("generate.html", race=race, version=__version__)
def format_exception(e: BaseException) -> str:
return f"{e.__class__.__name__}: {e}"
def start_generation(options: dict[str, dict | str], meta: dict[str, Any]):
results, gen_options = roll_options(options, set(meta["plando_options"]))
@@ -97,9 +92,7 @@ def start_generation(options: dict[str, dict | str], meta: dict[str, Any]):
except PicklingError as e:
from .autolauncher import handle_generation_failure
handle_generation_failure(e)
meta["error"] = format_exception(e)
details = json.dumps(meta, indent=4).strip()
return render_template("seedError.html", seed_error=meta["error"], details=details)
return render_template("seedError.html", seed_error=("PicklingError: " + str(e)))
commit()
@@ -107,18 +100,16 @@ def start_generation(options: dict[str, dict | str], meta: dict[str, Any]):
else:
try:
seed_id = gen_game({name: vars(options) for name, options in gen_options.items()},
meta=meta, owner=session["_id"].int, timeout=app.config["JOB_TIME"])
meta=meta, owner=session["_id"].int)
except BaseException as e:
from .autolauncher import handle_generation_failure
handle_generation_failure(e)
meta["error"] = format_exception(e)
details = json.dumps(meta, indent=4).strip()
return render_template("seedError.html", seed_error=meta["error"], details=details)
return render_template("seedError.html", seed_error=(e.__class__.__name__ + ": " + str(e)))
return redirect(url_for("view_seed", seed=seed_id))
def gen_game(gen_options: dict, meta: dict[str, Any] | None = None, owner=None, sid=None, timeout: int|None = None):
def gen_game(gen_options: dict, meta: dict[str, Any] | None = None, owner=None, sid=None):
if meta is None:
meta = {}
@@ -172,12 +163,11 @@ def gen_game(gen_options: dict, meta: dict[str, Any] | None = None, owner=None,
ERmain(args, seed, baked_server_options=meta["server_options"])
return upload_to_db(target.name, sid, owner, race)
thread_pool = DaemonThreadPoolExecutor(max_workers=1)
thread_pool = concurrent.futures.ThreadPoolExecutor(max_workers=1)
thread = thread_pool.submit(task)
try:
return thread.result(timeout)
return thread.result(app.config["JOB_TIME"])
except concurrent.futures.TimeoutError as e:
if sid:
with db_session:
@@ -185,14 +175,11 @@ def gen_game(gen_options: dict, meta: dict[str, Any] | None = None, owner=None,
if gen is not None:
gen.state = STATE_ERROR
meta = json.loads(gen.meta)
meta["error"] = ("Allowed time for Generation exceeded, " +
"please consider generating locally instead. " +
format_exception(e))
meta["error"] = (
"Allowed time for Generation exceeded, please consider generating locally instead. " +
e.__class__.__name__ + ": " + str(e))
gen.meta = json.dumps(meta)
commit()
except (KeyboardInterrupt, SystemExit):
# don't update db, retry next time
raise
except BaseException as e:
if sid:
with db_session:
@@ -200,15 +187,10 @@ def gen_game(gen_options: dict, meta: dict[str, Any] | None = None, owner=None,
if gen is not None:
gen.state = STATE_ERROR
meta = json.loads(gen.meta)
meta["error"] = format_exception(e)
meta["error"] = (e.__class__.__name__ + ": " + str(e))
gen.meta = json.dumps(meta)
commit()
raise
finally:
# free resources claimed by thread pool, if possible
# NOTE: Timeout depends on the process being killed at some point
# since we can't actually cancel a running gen at the moment.
thread_pool.shutdown(wait=False, cancel_futures=True)
@app.route('/wait/<suuid:seed>')
@@ -222,9 +204,7 @@ def wait_seed(seed: UUID):
if not generation:
return "Generation not found."
elif generation.state == STATE_ERROR:
meta = json.loads(generation.meta)
details = json.dumps(meta, indent=4).strip()
return render_template("seedError.html", seed_error=meta["error"], details=details)
return render_template("seedError.html", seed_error=generation.meta)
return render_template("waitSeed.html", seed_id=seed_id)

View File

@@ -260,10 +260,7 @@ def host_room(room: UUID):
# indicate that the page should reload to get the assigned port
should_refresh = ((not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3))
or room.last_activity < now - datetime.timedelta(seconds=room.timeout))
if now - room.last_activity > datetime.timedelta(minutes=1):
# we only set last_activity if needed, otherwise parallel access on /room will cause an internal server error
# due to "pony.orm.core.OptimisticCheckError: Object Room was updated outside of current transaction"
with db_session:
room.last_activity = now # will trigger a spinup, if it's not already running
browser_tokens = "Mozilla", "Chrome", "Safari"
@@ -271,9 +268,9 @@ def host_room(room: UUID):
or "Discordbot" in request.user_agent.string
or not any(browser_token in request.user_agent.string for browser_token in browser_tokens))
def get_log(max_size: int = 0 if automated else 1024000) -> Tuple[str, int]:
def get_log(max_size: int = 0 if automated else 1024000) -> str:
if max_size == 0:
return "", 0
return ""
try:
with open(os.path.join("logs", str(room.id) + ".txt"), "rb") as log:
raw_size = 0
@@ -284,9 +281,9 @@ def host_room(room: UUID):
break
raw_size += len(block)
fragments.append(block.decode("utf-8"))
return "".join(fragments), raw_size
return "".join(fragments)
except FileNotFoundError:
return "", 0
return ""
return render_template("hostRoom.html", room=room, should_refresh=should_refresh, get_log=get_log)

View File

@@ -76,7 +76,7 @@ def filter_rst_to_html(text: str) -> str:
lines = text.splitlines()
text = lines[0] + "\n" + dedent("\n".join(lines[1:]))
return publish_parts(text, writer='html', settings=None, settings_overrides={
return publish_parts(text, writer_name='html', settings=None, settings_overrides={
'raw_enable': False,
'file_insertion_enabled': False,
'output_encoding': 'unicode'
@@ -231,7 +231,7 @@ def generate_yaml(game: str):
if key_parts[-1] == "qty":
if key_parts[0] not in options:
options[key_parts[0]] = {}
if val and val != "0":
if val != "0":
options[key_parts[0]][key_parts[1]] = int(val)
del options[key]

View File

@@ -4,8 +4,7 @@ pony>=0.7.19; python_version <= '3.12'
pony @ git+https://github.com/black-sliver/pony@7feb1221953b7fa4a6735466bf21a8b4d35e33ba#0.7.19; python_version >= '3.13'
waitress>=3.0.2
Flask-Caching>=2.3.0
Flask-Compress>=1.17; python_version >= '3.12'
Flask-Compress==1.18; python_version <= '3.11' # 3.11's pkg_resources can't resolve the new "backports.zstd" dependency
Flask-Compress>=1.17
Flask-Limiter>=3.12
bokeh>=3.6.3
markupsafe>=3.0.2

View File

@@ -66,7 +66,7 @@ is to ensure items necessary to complete the game will be accessible to the play
rules allowing certain items to be placed in normally unreachable locations, provided the player has indicated they are
comfortable exploiting certain glitches in the game.
## I want to develop a game implementation for Archipelago. How do I do that?
## 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:
[Archipelago GitHub Page](https://github.com/ArchipelagoMW/Archipelago).
@@ -77,5 +77,4 @@ There, you will find examples of games in the `worlds` folder:
You may also find developer documentation in the `docs` folder:
[/docs Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/docs).
If you have more questions regarding development of a game implementation, feel free to ask in the **#ap-world-dev**
channel on our Discord.
If you have more questions, feel free to ask in the **#ap-world-dev** channel on our Discord.

View File

@@ -72,13 +72,3 @@ code{
padding-right: 0.25rem;
color: #000000;
}
code.grassy {
background-color: #b5e9a4;
border: 1px solid #2a6c2f;
white-space: preserve;
text-align: left;
display: block;
font-size: 14px;
line-height: 20px;
}

View File

@@ -13,7 +13,3 @@
min-height: 360px;
text-align: center;
}
h2, h4 {
color: #ffffff;
}

View File

@@ -98,7 +98,7 @@
<td>
{% if hint.finding_player == player %}
<b>{{ player_names_with_alias[(team, hint.finding_player)] }}</b>
{% elif get_slot_info(hint.finding_player).type == 2 %}
{% elif get_slot_info(team, hint.finding_player).type == 2 %}
<i>{{ player_names_with_alias[(team, hint.finding_player)] }}</i>
{% else %}
<a href="{{ url_for("get_player_tracker", tracker=room.tracker, tracked_team=team, tracked_player=hint.finding_player) }}">
@@ -109,7 +109,7 @@
<td>
{% if hint.receiving_player == player %}
<b>{{ player_names_with_alias[(team, hint.receiving_player)] }}</b>
{% elif get_slot_info(hint.receiving_player).type == 2 %}
{% elif get_slot_info(team, hint.receiving_player).type == 2 %}
<i>{{ player_names_with_alias[(team, hint.receiving_player)] }}</i>
{% else %}
<a href="{{ url_for("get_player_tracker", tracker=room.tracker, tracked_team=team, tracked_player=hint.receiving_player) }}">

View File

@@ -58,7 +58,8 @@
Open Log File...
</a>
</div>
{% set log, log_len = get_log() -%}
{% set log = get_log() -%}
{%- set log_len = log | length - 1 if log.endswith("…") else log | length -%}
<div id="logger" style="white-space: pre">{{ log }}</div>
<script>
let url = '{{ url_for('display_log', room = room.id) }}';

View File

@@ -45,15 +45,15 @@
{%- set current_sphere = loop.index %}
{%- for player, sphere_location_ids in sphere.items() %}
{%- set checked_locations = tracker_data.get_player_checked_locations(team, player) %}
{%- set finder_game = tracker_data.get_player_game(player) %}
{%- set player_location_data = tracker_data.get_player_locations(player) %}
{%- set finder_game = tracker_data.get_player_game(team, player) %}
{%- set player_location_data = tracker_data.get_player_locations(team, player) %}
{%- for location_id in sphere_location_ids.intersection(checked_locations) %}
<tr>
{%- set item_id, receiver, item_flags = player_location_data[location_id] %}
{%- set receiver_game = tracker_data.get_player_game(receiver) %}
{%- set receiver_game = tracker_data.get_player_game(team, receiver) %}
<td>{{ current_sphere }}</td>
<td>{{ tracker_data.get_player_name(player) }}</td>
<td>{{ tracker_data.get_player_name(receiver) }}</td>
<td>{{ tracker_data.get_player_name(team, player) }}</td>
<td>{{ tracker_data.get_player_name(team, receiver) }}</td>
<td>{{ tracker_data.item_id_to_name[receiver_game][item_id] }}</td>
<td>{{ tracker_data.location_id_to_name[finder_game][location_id] }}</td>
<td>{{ finder_game }}</td>

View File

@@ -22,14 +22,14 @@
-%}
<tr>
<td>
{% if get_slot_info(hint.finding_player).type == 2 %}
{% if get_slot_info(team, hint.finding_player).type == 2 %}
<i>{{ player_names_with_alias[(team, hint.finding_player)] }}</i>
{% else %}
{{ player_names_with_alias[(team, hint.finding_player)] }}
{% endif %}
</td>
<td>
{% if get_slot_info(hint.receiving_player).type == 2 %}
{% if get_slot_info(team, hint.receiving_player).type == 2 %}
<i>{{ player_names_with_alias[(team, hint.receiving_player)] }}</i>
{% else %}
{{ player_names_with_alias[(team, hint.receiving_player)] }}

View File

@@ -4,20 +4,16 @@
{% block head %}
<title>Generation failed, please retry.</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/waitSeed.css') }}"/>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/waitSeed.css") }}"/>
{% endblock %}
{% block body %}
{% include 'header/oceanIslandHeader.html' %}
<div id="wait-seed-wrapper" class="grass-island">
<div id="wait-seed">
<h1>Generation Failed</h1>
<h2>Please try again!</h2>
<p>{{ seed_error }}</p>
<h4>More details:</h4>
<p>
<code class="grassy">{{ details }}</code>
</p>
<h1>Generation failed</h1>
<h2>please retry</h2>
{{ seed_error }}
</div>
</div>
{% endblock %}

View File

@@ -31,9 +31,6 @@
{% include 'header/oceanHeader.html' %}
<div id="games" class="markdown">
<h1>Currently Supported Games</h1>
<p>Below are the games that are currently included with the Archipelago software. To play a game that is not on
this page, please refer to the <a href="/tutorial/Archipelago/setup/en#playing-with-custom-worlds">playing with
custom worlds</a> section of the setup guide.</p>
<div class="js-only">
<label for="game-search">Search for your game below!</label><br />
<div class="page-controls">

View File

@@ -17,6 +17,7 @@ from .models import GameDataPackage, Room
# Multisave is currently updated, at most, every minute.
TRACKER_CACHE_TIMEOUT_IN_SECONDS = 60
_multidata_cache = {}
_multiworld_trackers: Dict[str, Callable] = {}
_player_trackers: Dict[str, Callable] = {}
@@ -84,27 +85,27 @@ class TrackerData:
"""Retrieves the seed name."""
return self._multidata["seed_name"]
def get_slot_data(self, player: int) -> Dict[str, Any]:
def get_slot_data(self, team: int, player: int) -> Dict[str, Any]:
"""Retrieves the slot data for a given player."""
return self._multidata["slot_data"][player]
def get_slot_info(self, player: int) -> NetworkSlot:
def get_slot_info(self, team: int, player: int) -> NetworkSlot:
"""Retrieves the NetworkSlot data for a given player."""
return self._multidata["slot_info"][player]
def get_player_name(self, player: int) -> str:
def get_player_name(self, team: int, player: int) -> str:
"""Retrieves the slot name for a given player."""
return self.get_slot_info(player).name
return self.get_slot_info(team, player).name
def get_player_game(self, player: int) -> str:
def get_player_game(self, team: int, player: int) -> str:
"""Retrieves the game for a given player."""
return self.get_slot_info(player).game
return self.get_slot_info(team, player).game
def get_player_locations(self, player: int) -> Dict[int, ItemMetadata]:
def get_player_locations(self, team: int, player: int) -> Dict[int, ItemMetadata]:
"""Retrieves all locations with their containing item's metadata for a given player."""
return self._multidata["locations"][player]
def get_player_starting_inventory(self, player: int) -> List[int]:
def get_player_starting_inventory(self, team: int, player: int) -> List[int]:
"""Retrieves a list of all item codes a given slot starts with."""
return self._multidata["precollected_items"][player]
@@ -115,7 +116,7 @@ class TrackerData:
@_cache_results
def get_player_missing_locations(self, team: int, player: int) -> Set[int]:
"""Retrieves the set of all locations not marked complete by this player."""
return set(self.get_player_locations(player)) - self.get_player_checked_locations(team, player)
return set(self.get_player_locations(team, player)) - self.get_player_checked_locations(team, player)
def get_player_received_items(self, team: int, player: int) -> List[NetworkItem]:
"""Returns all items received to this player in order of received."""
@@ -125,7 +126,7 @@ class TrackerData:
def get_player_inventory_counts(self, team: int, player: int) -> collections.Counter:
"""Retrieves a dictionary of all items received by their id and their received count."""
received_items = self.get_player_received_items(team, player)
starting_items = self.get_player_starting_inventory(player)
starting_items = self.get_player_starting_inventory(team, player)
inventory = collections.Counter()
for item in received_items:
inventory[item.item] += 1
@@ -178,7 +179,7 @@ class TrackerData:
def get_team_locations_total_count(self) -> Dict[int, int]:
"""Retrieves a dictionary of total player locations each team has."""
return {
team: sum(len(self.get_player_locations(player)) for player in players)
team: sum(len(self.get_player_locations(team, player)) for player in players)
for team, players in self.get_all_players().items()
}
@@ -209,7 +210,7 @@ class TrackerData:
return {
0: [
player for player, slot_info in self._multidata["slot_info"].items()
if self.get_slot_info(player).type == SlotType.player
if self.get_slot_info(0, player).type == SlotType.player
]
}
@@ -225,7 +226,7 @@ class TrackerData:
def get_room_locations(self) -> Dict[TeamPlayer, Dict[int, ItemMetadata]]:
"""Retrieves a dictionary of all locations and their associated item metadata per player."""
return {
(team, player): self.get_player_locations(player)
(team, player): self.get_player_locations(team, player)
for team, players in self.get_all_players().items() for player in players
}
@@ -233,7 +234,7 @@ class TrackerData:
def get_room_games(self) -> Dict[TeamPlayer, str]:
"""Retrieves a dictionary of games for each player."""
return {
(team, player): self.get_player_game(player)
(team, player): self.get_player_game(team, player)
for team, players in self.get_all_slots().items() for player in players
}
@@ -261,9 +262,9 @@ class TrackerData:
for player in players:
alias = self.get_player_alias(team, player)
if alias:
long_player_names[team, player] = f"{alias} ({self.get_player_name(player)})"
long_player_names[team, player] = f"{alias} ({self.get_player_name(team, player)})"
else:
long_player_names[team, player] = self.get_player_name(player)
long_player_names[team, player] = self.get_player_name(team, player)
return long_player_names
@@ -343,7 +344,7 @@ def get_timeout_and_player_tracker(room: Room, tracked_team: int, tracked_player
tracker_data = TrackerData(room)
# Load and render the game-specific player tracker, or fallback to generic tracker if none exists.
game_specific_tracker = _player_trackers.get(tracker_data.get_player_game(tracked_player), None)
game_specific_tracker = _player_trackers.get(tracker_data.get_player_game(tracked_team, tracked_player), None)
if game_specific_tracker and not generic:
tracker = game_specific_tracker(tracker_data, tracked_team, tracked_player)
else:
@@ -408,10 +409,10 @@ def get_enabled_multiworld_trackers(room: Room) -> Dict[str, Callable]:
def render_generic_tracker(tracker_data: TrackerData, team: int, player: int) -> str:
game = tracker_data.get_player_game(player)
game = tracker_data.get_player_game(team, player)
received_items_in_order = {}
starting_inventory = tracker_data.get_player_starting_inventory(player)
starting_inventory = tracker_data.get_player_starting_inventory(team, player)
for index, item in enumerate(starting_inventory):
received_items_in_order[item] = index
for index, network_item in enumerate(tracker_data.get_player_received_items(team, player),
@@ -427,7 +428,7 @@ def render_generic_tracker(tracker_data: TrackerData, team: int, player: int) ->
player=player,
player_name=tracker_data.get_room_long_player_names()[team, player],
inventory=tracker_data.get_player_inventory_counts(team, player),
locations=tracker_data.get_player_locations(player),
locations=tracker_data.get_player_locations(team, player),
checked_locations=tracker_data.get_player_checked_locations(team, player),
received_items=received_items_in_order,
saving_second=tracker_data.get_room_saving_second(),
@@ -499,7 +500,7 @@ if "Factorio" in network_data_package["games"]:
tracker_data.item_id_to_name["Factorio"][item_id]: count
for item_id, count in tracker_data.get_player_inventory_counts(team, player).items()
}) for team, players in tracker_data.get_all_players().items() for player in players
if tracker_data.get_player_game(player) == "Factorio"
if tracker_data.get_player_game(team, player) == "Factorio"
}
return render_template(
@@ -588,7 +589,7 @@ if "A Link to the Past" in network_data_package["games"]:
# Highlight 'bombs' if we received any bomb upgrades in bombless start.
# In race mode, we'll just assume bombless start for simplicity.
if tracker_data.get_slot_data(player).get("bombless_start", True):
if tracker_data.get_slot_data(team, player).get("bombless_start", True):
inventory["Bombs"] = sum(count for item, count in inventory.items() if item.startswith("Bomb Upgrade"))
else:
inventory["Bombs"] = 1
@@ -604,7 +605,7 @@ if "A Link to the Past" in network_data_package["games"]:
for code, count in tracker_data.get_player_inventory_counts(team, player).items()
})
for team, players in tracker_data.get_all_players().items()
for player in players if tracker_data.get_slot_info(player).game == "A Link to the Past"
for player in players if tracker_data.get_slot_info(team, player).game == "A Link to the Past"
}
# Translate non-progression items to progression items for tracker simplicity.
@@ -623,7 +624,7 @@ if "A Link to the Past" in network_data_package["games"]:
for region_name in known_regions
}
for team, players in tracker_data.get_all_players().items()
for player in players if tracker_data.get_slot_info(player).game == "A Link to the Past"
for player in players if tracker_data.get_slot_info(team, player).game == "A Link to the Past"
}
# Get a totals count.
@@ -697,7 +698,7 @@ if "A Link to the Past" in network_data_package["games"]:
team=team,
player=player,
inventory=inventory,
player_name=tracker_data.get_player_name(player),
player_name=tracker_data.get_player_name(team, player),
regions=regions,
known_regions=known_regions,
)
@@ -844,7 +845,7 @@ if "Ocarina of Time" in network_data_package["games"]:
return full_name[len(area):]
return full_name
locations = tracker_data.get_player_locations(player)
locations = tracker_data.get_player_locations(team, player)
checked_locations = tracker_data.get_player_checked_locations(team, player).intersection(set(locations))
location_info = {}
checks_done = {}
@@ -906,7 +907,7 @@ if "Ocarina of Time" in network_data_package["games"]:
player=player,
team=team,
room=tracker_data.room,
player_name=tracker_data.get_player_name(player),
player_name=tracker_data.get_player_name(team, player),
icons=icons,
acquired_items={lookup_any_item_id_to_name[id] for id, count in inventory.items() if count > 0},
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
@@ -953,37 +954,57 @@ if "Timespinner" in network_data_package["games"]:
"Lab Glasses": "https://timespinnerwiki.com/mediawiki/images/4/4a/Lab_Glasses.png",
"Eye Orb": "https://timespinnerwiki.com/mediawiki/images/a/a4/Eye_Orb.png",
"Lab Coat": "https://timespinnerwiki.com/mediawiki/images/5/51/Lab_Coat.png",
"Demon": "https://timespinnerwiki.com/mediawiki/images/f/f8/Familiar_Demon.png",
"Cube of Bodie": "https://timespinnerwiki.com/mediawiki/images/1/14/Menu_Icon_Stats.png"
"Demon": "https://timespinnerwiki.com/mediawiki/images/f/f8/Familiar_Demon.png",
}
timespinner_location_ids = {
"Present": list(range(1337000, 1337085)),
"Past": list(range(1337086, 1337175)),
"Present": [
1337000, 1337001, 1337002, 1337003, 1337004, 1337005, 1337006, 1337007, 1337008, 1337009,
1337010, 1337011, 1337012, 1337013, 1337014, 1337015, 1337016, 1337017, 1337018, 1337019,
1337020, 1337021, 1337022, 1337023, 1337024, 1337025, 1337026, 1337027, 1337028, 1337029,
1337030, 1337031, 1337032, 1337033, 1337034, 1337035, 1337036, 1337037, 1337038, 1337039,
1337040, 1337041, 1337042, 1337043, 1337044, 1337045, 1337046, 1337047, 1337048, 1337049,
1337050, 1337051, 1337052, 1337053, 1337054, 1337055, 1337056, 1337057, 1337058, 1337059,
1337060, 1337061, 1337062, 1337063, 1337064, 1337065, 1337066, 1337067, 1337068, 1337069,
1337070, 1337071, 1337072, 1337073, 1337074, 1337075, 1337076, 1337077, 1337078, 1337079,
1337080, 1337081, 1337082, 1337083, 1337084, 1337085],
"Past": [
1337086, 1337087, 1337088, 1337089,
1337090, 1337091, 1337092, 1337093, 1337094, 1337095, 1337096, 1337097, 1337098, 1337099,
1337100, 1337101, 1337102, 1337103, 1337104, 1337105, 1337106, 1337107, 1337108, 1337109,
1337110, 1337111, 1337112, 1337113, 1337114, 1337115, 1337116, 1337117, 1337118, 1337119,
1337120, 1337121, 1337122, 1337123, 1337124, 1337125, 1337126, 1337127, 1337128, 1337129,
1337130, 1337131, 1337132, 1337133, 1337134, 1337135, 1337136, 1337137, 1337138, 1337139,
1337140, 1337141, 1337142, 1337143, 1337144, 1337145, 1337146, 1337147, 1337148, 1337149,
1337150, 1337151, 1337152, 1337153, 1337154, 1337155,
1337171, 1337172, 1337173, 1337174, 1337175],
"Ancient Pyramid": [
1337236,
1337246, 1337247, 1337248, 1337249]
}
slot_data = tracker_data.get_slot_data(player)
slot_data = tracker_data.get_slot_data(team, player)
if (slot_data["DownloadableItems"]):
timespinner_location_ids["Present"] += [1337156, 1337157] + list(range(1337159, 1337170))
timespinner_location_ids["Present"] += [
1337156, 1337157, 1337159,
1337160, 1337161, 1337162, 1337163, 1337164, 1337165, 1337166, 1337167, 1337168, 1337169,
1337170]
if (slot_data["Cantoran"]):
timespinner_location_ids["Past"].append(1337176)
if (slot_data["LoreChecks"]):
timespinner_location_ids["Present"] += list(range(1337177, 1337187))
timespinner_location_ids["Past"] += list(range(1337188, 1337198))
timespinner_location_ids["Present"] += [
1337177, 1337178, 1337179,
1337180, 1337181, 1337182, 1337183, 1337184, 1337185, 1337186, 1337187]
timespinner_location_ids["Past"] += [
1337188, 1337189,
1337190, 1337191, 1337192, 1337193, 1337194, 1337195, 1337196, 1337197, 1337198]
if (slot_data["GyreArchives"]):
timespinner_location_ids["Ancient Pyramid"] += list(range(1337237, 1337245))
timespinner_location_ids["Ancient Pyramid"] += [
1337237, 1337238, 1337239,
1337240, 1337241, 1337242, 1337243, 1337244, 1337245]
if (slot_data["PyramidStart"]):
timespinner_location_ids["Ancient Pyramid"] += [
1337233, 1337234, 1337235]
if (slot_data["PureTorcher"]):
timespinner_location_ids["Present"] += list(range(1337250, 1337352)) + list(range(1337422, 1337496)) + [1337506] + list(range(1337712, 1337779)) + [1337781, 1337782]
timespinner_location_ids["Past"] += list(range(1337497, 1337505)) + list(range(1337507, 1337711)) + [1337780]
timespinner_location_ids["Ancient Pyramid"] += list(range(1337369, 1337421))
if (slot_data["GyreArchives"]):
timespinner_location_ids["Ancient Pyramid"] += list(range(1337353, 1337368))
display_data = {}
@@ -1014,7 +1035,7 @@ if "Timespinner" in network_data_package["games"]:
player=player,
team=team,
room=tracker_data.room,
player_name=tracker_data.get_player_name(player),
player_name=tracker_data.get_player_name(team, player),
checks_done=checks_done,
checks_in_area=checks_in_area,
location_info=location_info,
@@ -1123,7 +1144,7 @@ if "Super Metroid" in network_data_package["games"]:
player=player,
team=team,
room=tracker_data.room,
player_name=tracker_data.get_player_name(player),
player_name=tracker_data.get_player_name(team, player),
checks_done=checks_done,
checks_in_area=checks_in_area,
location_info=location_info,
@@ -1173,7 +1194,7 @@ if "ChecksFinder" in network_data_package["games"]:
display_data = {}
inventory = tracker_data.get_player_inventory_counts(team, player)
locations = tracker_data.get_player_locations(player)
locations = tracker_data.get_player_locations(team, player)
# Multi-items
multi_items = {
@@ -1215,7 +1236,7 @@ if "ChecksFinder" in network_data_package["games"]:
player=player,
team=team,
room=tracker_data.room,
player_name=tracker_data.get_player_name(player),
player_name=tracker_data.get_player_name(team, player),
checks_done=checks_done,
checks_in_area=checks_in_area,
location_info=location_info,
@@ -1243,7 +1264,7 @@ if "Starcraft 2" in network_data_package["games"]:
UPGRADE_RESEARCH_SPEED_ITEM_ID = 1807
UPGRADE_RESEARCH_COST_ITEM_ID = 1808
REDUCED_MAX_SUPPLY_ITEM_ID = 1850
slot_data = tracker_data.get_slot_data(player)
slot_data = tracker_data.get_slot_data(team, player)
inventory: collections.Counter[int] = tracker_data.get_player_inventory_counts(team, player)
item_id_to_name = tracker_data.item_id_to_name["Starcraft 2"]
location_id_to_name = tracker_data.location_id_to_name["Starcraft 2"]
@@ -1259,10 +1280,10 @@ if "Starcraft 2" in network_data_package["games"]:
display_data["shield_regen_count"] = inventory.get(SHIELD_REGENERATION_ITEM_ID, 0)
display_data["upgrade_speed_count"] = inventory.get(UPGRADE_RESEARCH_SPEED_ITEM_ID, 0)
display_data["research_cost_count"] = inventory.get(UPGRADE_RESEARCH_COST_ITEM_ID, 0)
# Locations
have_nco_locations = False
locations = tracker_data.get_player_locations(player)
locations = tracker_data.get_player_locations(team, player)
checked_locations = tracker_data.get_player_checked_locations(team, player)
missions: dict[str, list[tuple[str, bool]]] = {}
for location_id in locations:
@@ -1417,7 +1438,7 @@ if "Starcraft 2" in network_data_package["games"]:
# the maximum bundle contribution, not the sum
inventory[upgrade_id] = bundle_amount
# Victory condition
game_state = tracker_data.get_player_client_status(team, player)
display_data["game_finished"] = game_state == ClientStatus.CLIENT_GOAL
@@ -1435,7 +1456,7 @@ if "Starcraft 2" in network_data_package["games"]:
player=player,
team=team,
room=tracker_data.room,
player_name=tracker_data.get_player_name(player),
player_name=tracker_data.get_player_name(team, player),
missions=missions,
locations=locations,
checked_locations=checked_locations,

View File

@@ -33,10 +33,6 @@ description: {{ yaml_dump("Default %s Template" % game) }}
game: {{ yaml_dump(game) }}
requires:
version: {{ __version__ }} # Version of Archipelago required for this yaml to work as expected.
{%- if world_version != "0.0.0" %}
game:
{{ yaml_dump(game) }}: {{ world_version }} # Version of the world required for this yaml to work as expected.
{%- endif %}
{%- macro range_option(option) %}
# You can define additional values between the minimum and maximum values.

View File

@@ -72,9 +72,6 @@
# Faxanadu
/worlds/faxanadu/ @Daivuk
# Final Fantasy (1)
/worlds/ff1/ @Rosalie-A
# Final Fantasy Mystic Quest
/worlds/ffmq/ @Alchav @wildham0
@@ -244,6 +241,9 @@
# compatibility, these worlds may be deleted. If you are interested in stepping up as maintainer for
# any of these worlds, please review `/docs/world maintainer.md` documentation.
# Final Fantasy (1)
# /worlds/ff1/
# Ocarina of Time
# /worlds/oot/

View File

@@ -1,83 +1,26 @@
# APWorld Specification
# apworld Specification
Archipelago depends on worlds to provide game-specific details like items, locations and output generation.
These are called "APWorlds".
They are located in the `worlds/` folder (source) or `<install dir>/lib/worlds/` (when installed).
Those are located in the `worlds/` folder (source) or `<install dir>/lib/worlds/` (when installed).
See [world api.md](world%20api.md) for details.
APWorlds can either be a folder, or they can be packaged as an .apworld file.
## .apworld File Format
apworld provides a way to package and ship a world that is not part of the main distribution by placing a `*.apworld`
file into the worlds folder.
The `.apworld` file format provides a way to package and ship an APWorld that is not part of the main distribution
by placing a `*.apworld` file into the worlds folder.
**Warning:** apworlds have to be all lower case, otherwise they raise a bogus Exception when trying to import in frozen python 3.10+!
`.apworld` files are zip archives, all lower case, with the file ending `.apworld`.
## File Format
apworld files are zip archives, all lower case, with the file ending `.apworld`.
The zip has to contain a folder with the same name as the zip, case-sensitive, that contains what would normally be in
the world's folder in `worlds/`. I.e. `worlds/ror2.apworld` containing `ror2/__init__.py`.
**Warning:** `.apworld` files have to be all lower case,
otherwise they raise a bogus Exception when trying to import in frozen python 3.10+!
## Metadata
Metadata about the APWorld is defined in an `archipelago.json` file.
No metadata is specified yet.
If the APWorld is a folder, the only required field is "game":
```json
{
"game": "Game Name"
}
```
There are also the following optional fields:
* `minimum_ap_version` and `maximum_ap_version` - which if present will each be compared against the current
Archipelago version respectively to filter those files from being loaded.
* `world_version` - an arbitrary version for that world in order to only load the newest valid world.
An APWorld without a world_version is always treated as older than one with a version
(**Must** use exactly the format `"major.minor.build"`, e.g. `1.0.0`)
* `authors` - a list of authors, to eventually be displayed in various user-facing places such as WebHost and
package managers. Should always be a list of strings.
If the APWorld is packaged as an `.apworld` zip file, it also needs to have `version` and `compatible_version`,
which refer to the version of the APContainer packaging scheme defined in [Files.py](../worlds/Files.py).
These get automatically added to the `archipelago.json` of an .apworld if it is packaged using the
["Build apworlds" launcher component](#build-apworlds-launcher-component),
which is the correct way to package your `.apworld` as a world developer. Do not write these fields yourself.
### "Build apworlds" Launcher Component
In the Archipelago Launcher, there is a "Build apworlds" component that will package all world folders to `.apworld`,
and add `archipelago.json` manifest files to them.
These .apworld files will be output to `build/apworlds` (relative to the Archipelago root directory).
The `archipelago.json` file in each .apworld will automatically include the appropriate
`version` and `compatible_version`.
If a world folder has an `archipelago.json` in its root, any fields it contains will be carried over.
So, a world folder with an `archipelago.json` that looks like this:
```json
{
"game": "Game Name",
"minimum_ap_version": "0.6.4",
"world_version": "2.1.4",
"authors": ["NewSoupVi"]
}
```
will be packaged into an `.apworld` with a manifest file inside of it that looks like this:
```json
{
"minimum_ap_version": "0.6.4",
"world_version": "2.1.4",
"authors": ["NewSoupVi"],
"version": 7,
"compatible_version": 7,
"game": "Game Name"
}
```
This is the recommended workflow for packaging your world to an `.apworld`.
## Extra Data
@@ -86,7 +29,7 @@ The zip can contain arbitrary files in addition what was specified above.
## Caveats
Imports from other files inside the APWorld have to use relative imports. e.g. `from .options import MyGameOptions`
Imports from other files inside the apworld have to use relative imports. e.g. `from .options import MyGameOptions`
Imports from AP base have to use absolute imports, e.g. `from Options import Toggle` or
`from worlds.AutoWorld import World`

View File

@@ -352,14 +352,14 @@ direction_matching_group_lookup = {
Terrain matching or dungeon shuffle:
```python
def randomize_within_same_group(group: int) -> list[int]:
def randomize_within_same_group(group: int) -> List[int]:
return [group]
identity_group_lookup = bake_target_group_lookup(world, randomize_within_same_group)
```
Directional + area shuffle:
```python
def get_target_groups(group: int) -> list[int]:
def get_target_groups(group: int) -> List[int]:
# example group: LEFT | CAVE
# example result: [RIGHT | CAVE, DOOR | CAVE]
direction = group & Groups.DIRECTION_MASK

View File

@@ -79,7 +79,7 @@ Sent to clients when they connect to an Archipelago server.
| generator_version | [NetworkVersion](#NetworkVersion) | Object denoting the version of Archipelago which generated the multiworld. |
| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. Example: `WebHost` |
| password | bool | Denoted whether a password is required to join this room. |
| permissions | dict\[str, [Permission](#Permission)\] | Mapping of permission name to [Permission](#Permission), keys are: "release", "collect" and "remaining". |
| permissions | dict\[str, [Permission](#Permission)\[int\]\] | Mapping of permission name to [Permission](#Permission), keys are: "release", "collect" and "remaining". |
| hint_cost | int | The percentage of total locations that need to be checked to receive a hint from the server. |
| location_check_points | int | The amount of hint points you receive per item/location check completed. |
| games | list\[str\] | List of games present in this multiworld. |
@@ -662,14 +662,13 @@ class SlotType(enum.IntFlag):
An object representing static information about a slot.
```python
from collections.abc import Sequence
from typing import NamedTuple
import typing
from NetUtils import SlotType
class NetworkSlot(NamedTuple):
class NetworkSlot(typing.NamedTuple):
name: str
game: str
type: SlotType
group_members: Sequence[int] = [] # only populated if type == group
group_members: typing.List[int] = [] # only populated if type == group
```
### Permission
@@ -687,8 +686,8 @@ class Permission(enum.IntEnum):
### Hint
An object representing a Hint.
```python
from typing import NamedTuple
class Hint(NamedTuple):
import typing
class Hint(typing.NamedTuple):
receiving_player: int
finding_player: int
location: int

View File

@@ -28,7 +28,7 @@ if it does not exist.
## Global Settings
All non-world-specific settings are defined directly in settings.py.
Each value needs to have a default. If the default should be `None`, annotate it using `T | None = None`.
Each value needs to have a default. If the default should be `None`, define it as `typing.Optional` and assign `None`.
To access a "global" config value, with correct typing, use one of
```python

View File

@@ -15,10 +15,8 @@
* Prefer [format string literals](https://peps.python.org/pep-0498/) over string concatenation,
use single quotes inside them: `f"Like {dct['key']}"`
* Use type annotations where possible for function signatures and class members.
* Use type annotations where appropriate for local variables (e.g. `var: list[int] = []`, or when the
type is hard or impossible to deduce). Clear annotations help developers look up and validate API calls.
* Prefer new style type annotations for new code (e.g. `var: dict[str, str | int]` over
`var: Dict[str, Union[str, int]]`).
* Use type annotations where appropriate for local variables (e.g. `var: List[int] = []`, or when the
type is hard or impossible to deduce.) Clear annotations help developers look up and validate API calls.
* If a line ends with an open bracket/brace/parentheses, the matching closing bracket should be at the
beginning of a line at the same indentation as the beginning of the line with the open bracket.
```python
@@ -62,9 +60,3 @@
* Indent `case` inside `switch ` with 2 spaces.
* Use single quotes.
* Semicolons are required after every statement.
## KV
* Style should be defined in `.kv` as much as possible, only Python when unavailable.
* Should follow [our Python style](#python-code) where appropriate (quotation marks, indentation).
* When escaping a line break, add a space between code and backslash.

View File

@@ -18,8 +18,6 @@ Current endpoints:
- [`/room_status/<suuid:room_id>`](#roomstatus)
- Tracker API
- [`/tracker/<suuid:tracker>`](#tracker)
- [`/static_tracker/<suuid:tracker>`](#statictracker)
- [`/slot_data_tracker/<suuid:tracker>`](#slotdatatracker)
- User API
- [`/get_rooms`](#getrooms)
- [`/get_seeds`](#getseeds)
@@ -256,6 +254,8 @@ can either be viewed while on a room tracker page, or from the [room's endpoint]
<a name=tracker></a>
Will provide a dict of tracker data with the following keys:
- item_link groups and their players (`groups`)
- Each player's slot_data (`slot_data`)
- Each player's current alias (`aliases`)
- Will return the name if there is none
- A list of items each player has received as a NetworkItem (`player_items_received`)
@@ -265,55 +265,111 @@ Will provide a dict of tracker data with the following keys:
- The time of last activity of each player in RFC 1123 format (`activity_timers`)
- The time of last active connection of each player in RFC 1123 format (`connection_timers`)
- The current client status of each player (`player_status`)
- The datapackage hash for each player (`datapackage`)
- This hash can then be sent to the datapackage API to receive the appropriate datapackage as necessary
Example:
```json
{
"groups": [
{
"team": 0,
"groups": [
{
"slot": 5,
"name": "testGroup",
"members": [
1,
2
]
},
{
"slot": 6,
"name": "myCoolLink",
"members": [
3,
4
]
}
]
}
],
"slot_data": [
{
"team": 0,
"players": [
{
"player": 1,
"slot_data": {
"example_option": 1,
"other_option": 3
}
},
{
"player": 2,
"slot_data": {
"example_option": 1,
"other_option": 2
}
}
]
}
],
"aliases": [
{
"team": 0,
"player": 1,
"alias": "Incompetence"
},
{
"team": 0,
"player": 2,
"alias": "Slot_Name_2"
"players": [
{
"player": 1,
"alias": "Incompetence"
},
{
"player": 2,
"alias": "Slot_Name_2"
}
]
}
],
"player_items_received": [
{
"team": 0,
"player": 1,
"items": [
[1, 1, 1, 0],
[2, 2, 2, 1]
]
},
{
"team": 0,
"player": 2,
"items": [
[1, 1, 1, 2],
[2, 2, 2, 0]
"players": [
{
"player": 1,
"items": [
[1, 1, 1, 0],
[2, 2, 2, 1]
]
},
{
"player": 2,
"items": [
[1, 1, 1, 2],
[2, 2, 2, 0]
]
}
]
}
],
"player_checks_done": [
{
"team": 0,
"player": 1,
"locations": [
1,
2
]
},
{
"team": 0,
"player": 2,
"locations": [
1,
2
"players": [
{
"player": 1,
"locations": [
1,
2
]
},
{
"player": 2,
"locations": [
1,
2
]
}
]
}
],
@@ -326,132 +382,76 @@ Example:
"hints": [
{
"team": 0,
"player": 1,
"hints": [
[1, 2, 4, 6, 0, "", 4, 0]
"players": [
{
"player": 1,
"hints": [
[1, 2, 4, 6, 0, "", 4, 0]
]
},
{
"player": 2,
"hints": []
}
]
},
{
"team": 0,
"player": 2,
"hints": []
}
],
"activity_timers": [
{
"team": 0,
"player": 1,
"time": "Fri, 18 Apr 2025 20:35:45 GMT"
},
{
"team": 0,
"player": 2,
"time": "Fri, 18 Apr 2025 20:42:46 GMT"
"players": [
{
"player": 1,
"time": "Fri, 18 Apr 2025 20:35:45 GMT"
},
{
"player": 2,
"time": "Fri, 18 Apr 2025 20:42:46 GMT"
}
]
}
],
"connection_timers": [
{
"team": 0,
"player": 1,
"time": "Fri, 18 Apr 2025 20:38:25 GMT"
},
{
"team": 0,
"player": 2,
"time": "Fri, 18 Apr 2025 21:03:00 GMT"
"players": [
{
"player": 1,
"time": "Fri, 18 Apr 2025 20:38:25 GMT"
},
{
"player": 2,
"time": "Fri, 18 Apr 2025 21:03:00 GMT"
}
]
}
],
"player_status": [
{
"team": 0,
"player": 1,
"status": 0
},
{
"team": 0,
"player": 2,
"status": 0
}
]
}
```
### `/static_tracker/<suuid:tracker>`
<a name=statictracker></a>
Will provide a dict of static tracker data with the following keys:
- item_link groups and their players (`groups`)
- The datapackage hash for each game (`datapackage`)
- This hash can then be sent to the datapackage API to receive the appropriate datapackage as necessary
- The number of checks found vs. total checks available per player (`player_locations_total`)
- Same logic as the multitracker template: found = len(player_checks_done.locations) / total = player_locations_total.total_locations (all available checks).
Example:
```json
{
"groups": [
{
"slot": 5,
"name": "testGroup",
"members": [
1,
2
]
},
{
"slot": 6,
"name": "myCoolLink",
"members": [
3,
4
"players": [
{
"player": 1,
"status": 0
},
{
"player": 2,
"status": 0
}
]
}
],
"datapackage": {
"Archipelago": {
"checksum": "ac9141e9ad0318df2fa27da5f20c50a842afeecb",
"version": 0
},
"The Messenger": {
"checksum": "6991cbcda7316b65bcb072667f3ee4c4cae71c0b",
}
},
"player_locations_total": [
{
"player": 1,
"team" : 0,
"total_locations": 10
},
{
"player": 2,
"team" : 0,
"total_locations": 20
}
],
}
```
### `/slot_data_tracker/<suuid:tracker>`
<a name=slotdatatracker></a>
Will provide a list of each player's slot_data.
Example:
```json
[
{
"player": 1,
"slot_data": {
"example_option": 1,
"other_option": 3
}
},
{
"player": 2,
"slot_data": {
"example_option": 1,
"other_option": 2
"version": 0
}
}
]
}
```
## User Endpoints
@@ -554,4 +554,4 @@ Example:
"seed_id": "a528e34c-3b4f-42a9-9f8f-00a4fd40bacb"
}
]
```
```

View File

@@ -76,8 +76,8 @@ webhost:
* `game_info_languages` (optional) list of strings for defining the existing game info pages your game supports. The
documents must be prefixed with the same string as defined here. Default already has 'en'.
* `options_presets` (optional) `dict[str, dict[str, Any]]` where the keys are the names of the presets and the values
are the options to be set for that preset. The options are defined as a `dict[str, Any]` where the keys are the names
* `options_presets` (optional) `Dict[str, Dict[str, Any]]` where the keys are the names of the presets and the values
are the options to be set for that preset. The options are defined as a `Dict[str, Any]` where the keys are the names
of the options and the values are the values to be set for that option. These presets will be available for users to
select from on the game's options page.
@@ -753,7 +753,7 @@ from BaseClasses import CollectionState, MultiWorld
from worlds.AutoWorld import LogicMixin
class MyGameState(LogicMixin):
mygame_defeatable_enemies: dict[int, set[str]] # per player
mygame_defeatable_enemies: Dict[int, Set[str]] # per player
def init_mixin(self, multiworld: MultiWorld) -> None:
# Initialize per player with the corresponding "nothing" value, such as 0 or an empty set.
@@ -882,11 +882,11 @@ item/location pairs is unnecessary since the AP server already retains and freel
that request it. The most common usage of slot data is sending option results that the client needs to be aware of.
```python
def fill_slot_data(self) -> dict[str, Any]:
def fill_slot_data(self) -> Dict[str, Any]:
# In order for our game client to handle the generated seed correctly we need to know what the user selected
# for their difficulty and final boss HP.
# A dictionary returned from this method gets set as the slot_data and will be sent to the client after connecting.
# The options dataclass has a method to return a `dict[str, Any]` of each option name provided and the relevant
# The options dataclass has a method to return a `Dict[str, Any]` of each option name provided and the relevant
# option's value.
return self.options.as_dict("difficulty", "final_boss_hp")
```

View File

@@ -180,8 +180,8 @@ Root: HKCR; Subkey: "{#MyAppName}mm2patch\shell\open\command"; ValueData: """{a
Root: HKCR; Subkey: ".apladx"; ValueData: "{#MyAppName}ladxpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}ladxpatch"; ValueData: "Archipelago Links Awakening DX Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLauncher.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\shell\open\command"; ValueData: """{app}\ArchipelagoLauncher.exe"" ""%1"""; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLinksAwakeningClient.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\shell\open\command"; ValueData: """{app}\ArchipelagoLinksAwakeningClient.exe"" ""%1"""; ValueType: string; ValueName: "";
Root: HKCR; Subkey: ".aptloz"; ValueData: "{#MyAppName}tlozpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}tlozpatch"; ValueData: "Archipelago The Legend of Zelda Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";

10
kvui.py
View File

@@ -838,15 +838,15 @@ class GameManager(ThemedApp):
self.log_panels: typing.Dict[str, Widget] = {}
# keep track of last used command to autofill on click
self.last_autofillable_command = "!hint"
autofillable_commands = ("!hint_location", "!hint", "!getitem")
self.last_autofillable_command = "hint"
autofillable_commands = ("hint_location", "hint", "getitem")
original_say = ctx.on_user_say
def intercept_say(text):
text = original_say(text)
if text:
for command in autofillable_commands:
if text.startswith(command):
if text.startswith("!" + command):
self.last_autofillable_command = command
break
return text
@@ -1099,6 +1099,10 @@ class GameManager(ThemedApp):
hints = self.ctx.stored_data.get(f"_read_hints_{self.ctx.team}_{self.ctx.slot}", [])
self.hint_log.refresh_hints(hints)
# default F1 keybind, opens a settings menu, that seems to break the layout engine once closed
def open_settings(self, *largs):
pass
class LogtoUI(logging.Handler):
def __init__(self, on_log):

View File

@@ -579,17 +579,6 @@ class ServerOptions(Group):
"goal" -> Client can ask for remaining items after goal completion
"""
class CountdownMode(str):
"""
Countdown modes
Determines whether or not a player can initiate a countdown with !countdown
Note that /countdown is always available to the host.
"enabled" -> Client can always initiate a countdown with !countdown.
"disabled" -> Client can never initiate a countdown with !countdown.
"auto" -> !countdown will be available for any room with less than 30 slots.
"""
class AutoShutdown(int):
"""Automatically shut down the server after this many seconds without new location checks, 0 to keep running"""
@@ -624,7 +613,6 @@ class ServerOptions(Group):
release_mode: ReleaseMode = ReleaseMode("auto")
collect_mode: CollectMode = CollectMode("auto")
remaining_mode: RemainingMode = RemainingMode("goal")
countdown_mode: CountdownMode = CountdownMode("auto")
auto_shutdown: AutoShutdown = AutoShutdown(0)
compatibility: Compatibility = Compatibility(2)
log_network: LogNetwork = LogNetwork(0)

View File

@@ -22,7 +22,7 @@ SNI_VERSION = "v0.0.100" # change back to "latest" once tray icon issues are fi
# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it
requirement = 'cx-Freeze==8.4.0'
requirement = 'cx-Freeze==8.0.0'
try:
import pkg_resources
try:
@@ -371,7 +371,6 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
os.makedirs(self.buildfolder / "Players" / "Templates", exist_ok=True)
from Options import generate_yaml_templates
from worlds.AutoWorld import AutoWorldRegister
from worlds.Files import APWorldContainer
assert not non_apworlds - set(AutoWorldRegister.world_types), \
f"Unknown world {non_apworlds - set(AutoWorldRegister.world_types)} designated for .apworld"
folders_to_remove: list[str] = []
@@ -380,36 +379,13 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
if worldname not in non_apworlds:
file_name = os.path.split(os.path.dirname(worldtype.__file__))[1]
world_directory = self.libfolder / "worlds" / file_name
if os.path.isfile(world_directory / "archipelago.json"):
with open(os.path.join(world_directory, "archipelago.json"), mode="r", encoding="utf-8") as manifest_file:
manifest = json.load(manifest_file)
assert "game" in manifest, (
f"World directory {world_directory} has an archipelago.json manifest file, but it"
"does not define a \"game\"."
)
assert manifest["game"] == worldtype.game, (
f"World directory {world_directory} has an archipelago.json manifest file, but value of the"
f"\"game\" field ({manifest['game']} does not equal the World class's game ({worldtype.game})."
)
else:
manifest = {}
# this method creates an apworld that cannot be moved to a different OS or minor python version,
# which should be ok
zip_path = self.libfolder / "worlds" / (file_name + ".apworld")
apworld = APWorldContainer(str(zip_path))
apworld.minimum_ap_version = version_tuple
apworld.maximum_ap_version = version_tuple
apworld.game = worldtype.game
manifest.update(apworld.get_manifest())
apworld.manifest_path = f"{file_name}/archipelago.json"
with zipfile.ZipFile(zip_path, "x", zipfile.ZIP_DEFLATED,
with zipfile.ZipFile(self.libfolder / "worlds" / (file_name + ".apworld"), "x", zipfile.ZIP_DEFLATED,
compresslevel=9) as zf:
for path in world_directory.rglob("*.*"):
relative_path = os.path.join(*path.parts[path.parts.index("worlds")+1:])
if not relative_path.endswith("archipelago.json"):
zf.write(path, relative_path)
zf.writestr(apworld.manifest_path, json.dumps(manifest))
zf.write(path, relative_path)
folders_to_remove.append(file_name)
shutil.rmtree(world_directory)
shutil.copyfile("meta.yaml", self.buildfolder / "Players" / "Templates" / "meta.yaml")

View File

@@ -1,227 +0,0 @@
#!/usr/bin/env python
# based on python-websockets compression benchmark (c) Aymeric Augustin and contributors
# https://github.com/python-websockets/websockets/blob/main/experiments/compression/benchmark.py
import collections
import time
import zlib
from typing import Iterable
REPEAT = 10
WB, ML = 12, 5 # defaults used as a reference
WBITS = range(9, 16)
MEMLEVELS = range(1, 10)
def benchmark(data: Iterable[bytes]) -> None:
size: dict[int, dict[int, float]] = collections.defaultdict(dict)
duration: dict[int, dict[int, float]] = collections.defaultdict(dict)
for wbits in WBITS:
for memLevel in MEMLEVELS:
encoder = zlib.compressobj(wbits=-wbits, memLevel=memLevel)
encoded = []
print(f"Compressing {REPEAT} times with {wbits=} and {memLevel=}")
t0 = time.perf_counter()
for _ in range(REPEAT):
for item in data:
# Taken from PerMessageDeflate.encode
item = encoder.compress(item) + encoder.flush(zlib.Z_SYNC_FLUSH)
if item.endswith(b"\x00\x00\xff\xff"):
item = item[:-4]
encoded.append(item)
t1 = time.perf_counter()
size[wbits][memLevel] = sum(len(item) for item in encoded) / REPEAT
duration[wbits][memLevel] = (t1 - t0) / REPEAT
raw_size = sum(len(item) for item in data)
print("=" * 79)
print("Compression ratio")
print("=" * 79)
print("\t".join(["wb \\ ml"] + [str(memLevel) for memLevel in MEMLEVELS]))
for wbits in WBITS:
print(
"\t".join(
[str(wbits)]
+ [
f"{100 * (1 - size[wbits][memLevel] / raw_size):.1f}%"
for memLevel in MEMLEVELS
]
)
)
print("=" * 79)
print()
print("=" * 79)
print("CPU time")
print("=" * 79)
print("\t".join(["wb \\ ml"] + [str(memLevel) for memLevel in MEMLEVELS]))
for wbits in WBITS:
print(
"\t".join(
[str(wbits)]
+ [
f"{1000 * duration[wbits][memLevel]:.1f}ms"
for memLevel in MEMLEVELS
]
)
)
print("=" * 79)
print()
print("=" * 79)
print(f"Size vs. {WB} \\ {ML}")
print("=" * 79)
print("\t".join(["wb \\ ml"] + [str(memLevel) for memLevel in MEMLEVELS]))
for wbits in WBITS:
print(
"\t".join(
[str(wbits)]
+ [
f"{100 * (size[wbits][memLevel] / size[WB][ML] - 1):.1f}%"
for memLevel in MEMLEVELS
]
)
)
print("=" * 79)
print()
print("=" * 79)
print(f"Time vs. {WB} \\ {ML}")
print("=" * 79)
print("\t".join(["wb \\ ml"] + [str(memLevel) for memLevel in MEMLEVELS]))
for wbits in WBITS:
print(
"\t".join(
[str(wbits)]
+ [
f"{100 * (duration[wbits][memLevel] / duration[WB][ML] - 1):.1f}%"
for memLevel in MEMLEVELS
]
)
)
print("=" * 79)
print()
def generate_data_package_corpus() -> list[bytes]:
# compared to default 12, 5:
# 11, 4 saves 16K RAM, gives +4.6% size, -5.0% time .. +1.1% time
# 10, 4 saves 20K RAM, gives +10.2% size, -3.8% time .. +0.6% time
# 11, 3 saves 20K RAM, gives +6.5% size, +14.2% time
# 10, 3 saves 24K RAM, gives +12.8% size, +0.5% time .. +6.9% time
# NOTE: time delta is highly unstable; time is ~100ms
import warnings
with warnings.catch_warnings():
warnings.simplefilter("ignore")
from NetUtils import encode
from worlds import network_data_package
return [encode(network_data_package).encode("utf-8")]
def generate_solo_release_corpus() -> list[bytes]:
# compared to default 12, 5:
# 11, 4 saves 16K RAM, gives +0.9% size, +3.9% time
# 10, 4 saves 20K RAM, gives +1.4% size, +3.4% time
# 11, 3 saves 20K RAM, gives +1.8% size, +13.9% time
# 10, 3 saves 24K RAM, gives +2.1% size, +4.8% time
# NOTE: time delta is highly unstable; time is ~0.4ms
from random import Random
from MultiServer import json_format_send_event
from NetUtils import encode, NetworkItem
r = Random()
r.seed(0)
solo_release = []
solo_release_locations = [r.randint(1000, 1999) for _ in range(200)]
solo_release_items = sorted([r.randint(1000, 1999) for _ in range(200)]) # currently sorted by item
solo_player = 1
for location, item in zip(solo_release_locations, solo_release_items):
flags = r.choice((0, 0, 0, 0, 0, 0, 0, 1, 2, 3))
network_item = NetworkItem(item, location, solo_player, flags)
solo_release.append(json_format_send_event(network_item, solo_player))
solo_release.append({
"cmd": "ReceivedItems",
"index": 0,
"items": solo_release_items,
})
solo_release.append({
"cmd": "RoomUpdate",
"hint_points": 200,
"checked_locations": solo_release_locations,
})
return [encode(solo_release).encode("utf-8")]
def generate_gameplay_corpus() -> list[bytes]:
# compared to default 12, 5:
# 11, 4 saves 16K RAM, gives +13.6% size, +4.1% time
# 10, 4 saves 20K RAM, gives +22.3% size, +2.2% time
# 10, 3 saves 24K RAM, gives +26.2% size, +1.6% time
# NOTE: time delta is highly unstable; time is 4ms
from copy import copy
from random import Random
from MultiServer import json_format_send_event
from NetUtils import encode, NetworkItem
r = Random()
r.seed(0)
gameplay = []
observer = 1
hint_points = 0
index = 0
players = list(range(1, 10))
player_locations = {player: [r.randint(1000, 1999) for _ in range(200)] for player in players}
player_items = {player: [r.randint(1000, 1999) for _ in range(200)] for player in players}
player_receiver = {player: [r.randint(1, len(players)) for _ in range(200)] for player in players}
for i in range(0, len(player_locations[1])):
player_sequence = copy(players)
r.shuffle(player_sequence)
for finder in player_sequence:
flags = r.choice((0, 0, 0, 0, 0, 0, 0, 1, 2, 3))
receiver = player_receiver[finder][i]
item = player_items[finder][i]
location = player_locations[finder][i]
network_item = NetworkItem(item, location, receiver, flags)
gameplay.append(json_format_send_event(network_item, observer))
if finder == observer:
hint_points += 1
gameplay.append({
"cmd": "RoomUpdate",
"hint_points": hint_points,
"checked_locations": [location],
})
if receiver == observer:
gameplay.append({
"cmd": "ReceivedItems",
"index": index,
"items": [item],
})
index += 1
return [encode(gameplay).encode("utf-8")]
def main() -> None:
#corpus = generate_data_package_corpus()
#corpus = generate_solo_release_corpus()
#corpus = generate_gameplay_corpus()
corpus = generate_data_package_corpus() + generate_solo_release_corpus() + generate_gameplay_corpus()
benchmark(corpus)
print(f"raw size: {sum(len(data) for data in corpus)}")
if __name__ == "__main__":
main()

View File

@@ -1,12 +1,4 @@
def run_locations_benchmark(freeze_gc: bool = True) -> None:
"""
Run a benchmark of location access rule performance against an empty_state and an all_state.
:param freeze_gc: Whether to freeze gc before benchmarking and unfreeze gc afterward. Freezing gc moves all objects
tracked by the garbage collector to a permanent generation, ignoring them in all future collections. Freezing
greatly reduces the duration of running gc.collect() within benchmarks, which otherwise often takes much longer
than running all iterations for the location rule being benchmarked.
"""
def run_locations_benchmark():
import argparse
import logging
import gc
@@ -42,8 +34,6 @@ def run_locations_benchmark(freeze_gc: bool = True) -> None:
return "\n".join(f" {time:.4f} in {name}" for name, time in counter.most_common(top))
def location_test(self, test_location: Location, state: CollectionState, state_name: str) -> float:
if freeze_gc:
gc.freeze()
with TimeIt(f"{test_location.game} {self.rule_iterations} "
f"runs of {test_location}.access_rule({state_name})", logger) as t:
for _ in range(self.rule_iterations):
@@ -51,8 +41,6 @@ def run_locations_benchmark(freeze_gc: bool = True) -> None:
# if time is taken to disentangle complex ref chains,
# this time should be attributed to the rule.
gc.collect()
if freeze_gc:
gc.unfreeze()
return t.dif
def main(self):
@@ -76,13 +64,9 @@ def run_locations_benchmark(freeze_gc: bool = True) -> None:
gc.collect()
for step in self.gen_steps:
if freeze_gc:
gc.freeze()
with TimeIt(f"{game} step {step}", logger):
call_all(multiworld, step)
gc.collect()
if freeze_gc:
gc.unfreeze()
locations = sorted(multiworld.get_unfilled_locations())
if not locations:

View File

@@ -1,5 +1,5 @@
from argparse import Namespace
from typing import Any, List, Optional, Tuple, Type
from typing import List, Optional, Tuple, Type, Union
from BaseClasses import CollectionState, Item, ItemClassification, Location, MultiWorld, Region
from worlds import network_data_package
@@ -31,8 +31,8 @@ def setup_solo_multiworld(
return setup_multiworld(world_type, steps, seed)
def setup_multiworld(worlds: list[type[World]] | type[World], steps: tuple[str, ...] = gen_steps,
seed: int | None = None, options: dict[str, Any] | list[dict[str, Any]] = None) -> MultiWorld:
def setup_multiworld(worlds: Union[List[Type[World]], Type[World]], steps: Tuple[str, ...] = gen_steps,
seed: Optional[int] = None) -> MultiWorld:
"""
Creates a multiworld with a player for each provided world type, allowing duplicates, setting default options, and
calling the provided gen steps.
@@ -40,27 +40,20 @@ def setup_multiworld(worlds: list[type[World]] | type[World], steps: tuple[str,
:param worlds: Type/s of worlds to generate a multiworld for
:param steps: Gen steps that should be called before returning. Default calls through pre_fill
:param seed: The seed to be used when creating this multiworld
:param options: Options to set on each world. If just one dict of options is passed, it will be used for all worlds.
:return: The generated multiworld
"""
if not isinstance(worlds, list):
worlds = [worlds]
if options is None:
options = [{}] * len(worlds)
elif not isinstance(options, list):
options = [options] * len(worlds)
players = len(worlds)
multiworld = MultiWorld(players)
multiworld.game = {player: world_type.game for player, world_type in enumerate(worlds, 1)}
multiworld.player_name = {player: f"Tester{player}" for player in multiworld.player_ids}
multiworld.set_seed(seed)
args = Namespace()
for player, (world_type, option_overrides) in enumerate(zip(worlds, options), 1):
for player, world_type in enumerate(worlds, 1):
for key, option in world_type.options_dataclass.type_hints.items():
updated_options = getattr(args, key, {})
updated_options[player] = option.from_any(option_overrides.get(key, option.default))
updated_options[player] = option.from_any(option.default)
setattr(args, key, updated_options)
multiworld.set_options(args)
multiworld.state = CollectionState(multiworld)

View File

@@ -6,9 +6,9 @@ from Utils import get_intended_text, get_input_text_from_response
class TestClient(unittest.TestCase):
def test_autofill_hint_from_fuzzy_hint(self) -> None:
tests = (
("item", ["item1", "item2"]), # Multiple close matches
("itm", ["item1", "item21"]), # No close match, multiple option
("item", ["item1"]), # No close match, single option
("item", ["item1", "item2"]), # Multiple close matches
("itm", ["item1", "item21"]), # No close match, multiple option
("item", ["item1"]), # No close match, single option
("item", ["\"item\" 'item' (item)"]), # Testing different special characters
)
@@ -16,7 +16,7 @@ class TestClient(unittest.TestCase):
item_name, usable, response = get_intended_text(input_text, possible_answers)
self.assertFalse(usable, "This test must be updated, it seems get_fuzzy_results behavior changed")
hint_command = get_input_text_from_response(response, "!hint")
hint_command = get_input_text_from_response(response, "hint")
self.assertIsNotNone(hint_command,
"The response to fuzzy hints is no longer recognized by the hint autofill")
self.assertEqual(hint_command, f"!hint {item_name}",

View File

@@ -1,7 +1,7 @@
import unittest
from BaseClasses import PlandoOptions
from Options import Choice, ItemLinks, PlandoConnections, PlandoItems, PlandoTexts
from Options import ItemLinks, Choice
from Utils import restricted_dumps
from worlds.AutoWorld import AutoWorldRegister
@@ -72,8 +72,8 @@ class TestOptions(unittest.TestCase):
for link in item_links.values():
self.assertEqual(link.value[0], item_link_group[0])
def test_pickle_dumps_default(self):
"""Test that default option values can be pickled into database for WebHost generation"""
def test_pickle_dumps(self):
"""Test options can be pickled into database for WebHost generation"""
for gamename, world_type in AutoWorldRegister.world_types.items():
if not world_type.hidden:
for option_key, option in world_type.options_dataclass.type_hints.items():
@@ -81,23 +81,3 @@ class TestOptions(unittest.TestCase):
restricted_dumps(option.from_any(option.default))
if issubclass(option, Choice) and option.default in option.name_lookup:
restricted_dumps(option.from_text(option.name_lookup[option.default]))
def test_pickle_dumps_plando(self):
"""Test that plando options using containers of a custom type can be pickled"""
# The base PlandoConnections class can't be instantiated directly, create a subclass and then cast it
class TestPlandoConnections(PlandoConnections):
entrances = {"An Entrance"}
exits = {"An Exit"}
plando_connection_value = PlandoConnections(
TestPlandoConnections.from_any([{"entrance": "An Entrance", "exit": "An Exit"}])
)
plando_values = {
"PlandoConnections": plando_connection_value,
"PlandoItems": PlandoItems.from_any([{"item": "Something", "location": "Somewhere"}]),
"PlandoTexts": PlandoTexts.from_any([{"text": "Some text.", "at": "text_box"}]),
}
for option_key, value in plando_values.items():
with self.subTest(option=option_key):
restricted_dumps(value)

View File

@@ -1,14 +0,0 @@
import unittest
from Utils import DaemonThreadPoolExecutor
class DaemonThreadPoolExecutorTest(unittest.TestCase):
def test_is_daemon(self) -> None:
def run() -> None:
pass
with DaemonThreadPoolExecutor(1) as executor:
executor.submit(run)
self.assertTrue(next(iter(executor._threads)).daemon)

View File

@@ -93,13 +93,3 @@ class TestTracker(TestBase):
headers={"If-Modified-Since": "Wed, 21 Oct 2015 07:28:00"}, # missing timezone
)
self.assertEqual(response.status_code, 400)
def test_tracker_api(self) -> None:
"""Verify that tracker api gives a reply for the room."""
with self.app.test_request_context():
with self.client.open(url_for("api.tracker_data", tracker=self.tracker_uuid)) as response:
self.assertEqual(response.status_code, 200)
with self.client.open(url_for("api.static_tracker_data", tracker=self.tracker_uuid)) as response:
self.assertEqual(response.status_code, 200)
with self.client.open(url_for("api.tracker_slot_data", tracker=self.tracker_uuid)) as response:
self.assertEqual(response.status_code, 200)

View File

@@ -12,7 +12,7 @@ from typing import (Any, Callable, ClassVar, Dict, FrozenSet, Iterable, List, Ma
from Options import item_and_loc_options, ItemsAccessibility, OptionGroup, PerGameCommonOptions
from BaseClasses import CollectionState
from Utils import Version
from Utils import deprecate
if TYPE_CHECKING:
from BaseClasses import MultiWorld, Item, Location, Tutorial, Region, Entrance
@@ -75,10 +75,6 @@ class AutoWorldRegister(type):
if "required_client_version" in base.__dict__:
dct["required_client_version"] = max(dct["required_client_version"],
base.__dict__["required_client_version"])
if "world_version" in dct:
if dct["world_version"] != Version(0, 0, 0):
raise RuntimeError(f"{name} is attempting to set 'world_version' from within the class. world_version "
f"can only be set from manifest.")
# construct class
new_class = super().__new__(mcs, name, bases, dct)
@@ -341,8 +337,6 @@ class World(metaclass=AutoWorldRegister):
"""If loaded from a .apworld, this is the Path to it."""
__file__: ClassVar[str]
"""path it was loaded from"""
world_version: ClassVar[Version] = Version(0, 0, 0)
"""Optional world version loaded from archipelago.json"""
def __init__(self, multiworld: "MultiWorld", player: int):
assert multiworld is not None

View File

@@ -8,8 +8,7 @@ import os
import threading
from io import BytesIO
from typing import (ClassVar, Dict, List, Literal, Tuple, Any, Optional, Union, BinaryIO, overload, Sequence,
TYPE_CHECKING)
from typing import ClassVar, Dict, List, Literal, Tuple, Any, Optional, Union, BinaryIO, overload, Sequence
import bsdiff4
@@ -17,9 +16,6 @@ semaphore = threading.Semaphore(os.cpu_count() or 4)
del threading
if TYPE_CHECKING:
from Utils import Version
class AutoPatchRegister(abc.ABCMeta):
patch_types: ClassVar[Dict[str, AutoPatchRegister]] = {}
@@ -69,7 +65,7 @@ class AutoPatchExtensionRegister(abc.ABCMeta):
return handler
container_version: int = 7
container_version: int = 6
def is_ap_player_container(game: str, data: bytes, player: int):
@@ -96,7 +92,7 @@ class APContainer:
version: ClassVar[int] = container_version
compression_level: ClassVar[int] = 9
compression_method: ClassVar[int] = zipfile.ZIP_DEFLATED
manifest_path: str = "archipelago.json"
path: Optional[str]
def __init__(self, path: Optional[str] = None):
@@ -120,7 +116,7 @@ class APContainer:
except Exception as e:
raise Exception(f"Manifest {manifest} did not convert to json.") from e
else:
opened_zipfile.writestr(self.manifest_path, manifest_str)
opened_zipfile.writestr("archipelago.json", manifest_str)
def read(self, file: Optional[Union[str, BinaryIO]] = None) -> None:
"""Read data into patch object. file can be file-like, such as an outer zip file's stream."""
@@ -141,18 +137,7 @@ class APContainer:
raise InvalidDataError(f"{message}This might be the incorrect world version for this file") from e
def read_contents(self, opened_zipfile: zipfile.ZipFile) -> Dict[str, Any]:
try:
assert self.manifest_path.endswith("archipelago.json"), "Filename should be archipelago.json"
manifest_info = opened_zipfile.getinfo(self.manifest_path)
except KeyError as e:
for info in opened_zipfile.infolist():
if info.filename.endswith("archipelago.json"):
manifest_info = info
self.manifest_path = info.filename
break
else:
raise e
with opened_zipfile.open(manifest_info, "r") as f:
with opened_zipfile.open("archipelago.json", "r") as f:
manifest = json.load(f)
if manifest["compatible_version"] > self.version:
raise Exception(f"File (version: {manifest['compatible_version']}) too new "
@@ -167,33 +152,6 @@ class APContainer:
}
class APWorldContainer(APContainer):
"""A zipfile containing a world implementation."""
game: str | None = None
world_version: "Version | None" = None
minimum_ap_version: "Version | None" = None
maximum_ap_version: "Version | None" = None
def read_contents(self, opened_zipfile: zipfile.ZipFile) -> Dict[str, Any]:
from Utils import tuplize_version
manifest = super().read_contents(opened_zipfile)
self.game = manifest["game"]
for version_key in ("world_version", "minimum_ap_version", "maximum_ap_version"):
if version_key in manifest:
setattr(self, version_key, tuplize_version(manifest[version_key]))
return manifest
def get_manifest(self) -> Dict[str, Any]:
manifest = super().get_manifest()
manifest["game"] = self.game
manifest["compatible_version"] = 7
for version_key in ("world_version", "minimum_ap_version", "maximum_ap_version"):
version = getattr(self, version_key)
if version:
manifest[version_key] = version.as_simple_string()
return manifest
class APPlayerContainer(APContainer):
"""A zipfile containing at least archipelago.json meant for a player"""
game: ClassVar[Optional[str]] = None
@@ -290,8 +248,10 @@ class APProcedurePatch(APAutoPatchInterface):
manifest["compatible_version"] = 5
return manifest
def read_contents(self, opened_zipfile: zipfile.ZipFile) -> Dict[str, Any]:
manifest = super(APProcedurePatch, self).read_contents(opened_zipfile)
def read_contents(self, opened_zipfile: zipfile.ZipFile) -> None:
super(APProcedurePatch, self).read_contents(opened_zipfile)
with opened_zipfile.open("archipelago.json", "r") as f:
manifest = json.load(f)
if "procedure" not in manifest:
# support patching files made before moving to procedures
self.procedure = [("apply_bsdiff4", ["delta.bsdiff4"])]
@@ -300,7 +260,6 @@ class APProcedurePatch(APAutoPatchInterface):
for file in opened_zipfile.namelist():
if file not in ["archipelago.json"]:
self.files[file] = opened_zipfile.read(file)
return manifest
def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None:
super(APProcedurePatch, self).write_contents(opened_zipfile)

View File

@@ -5,7 +5,7 @@ import weakref
from enum import Enum, auto
from typing import Optional, Callable, List, Iterable, Tuple
from Utils import local_path, open_filename, is_frozen, is_kivy_running
from Utils import local_path, open_filename
class Type(Enum):
@@ -177,10 +177,11 @@ def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, path
if module_name == loaded_name:
found_already_loaded = True
break
if found_already_loaded and is_kivy_running():
raise Exception(f"Installed APWorld successfully, but '{module_name}' is already loaded, "
"so a Launcher restart is required to use the new installation.")
world_source = worlds.WorldSource(str(target), is_zip=True, relative=False)
if found_already_loaded:
raise Exception(f"Installed APWorld successfully, but '{module_name}' is already loaded,\n"
"so a Launcher restart is required to use the new installation.\n"
"If the Launcher is not open, no action needs to be taken.")
world_source = worlds.WorldSource(str(target), is_zip=True)
bisect.insort(worlds.world_sources, world_source)
world_source.load()
@@ -196,7 +197,7 @@ def install_apworld(apworld_path: str = "") -> None:
source, target = res
except Exception as e:
import Utils
Utils.messagebox("Notice", str(e), error=True)
Utils.messagebox(e.__class__.__name__, str(e), error=True)
logging.exception(e)
else:
import Utils
@@ -217,6 +218,8 @@ components: List[Component] = [
description="Install an APWorld to play games not included with Archipelago by default."),
Component('Text Client', 'CommonClient', 'ArchipelagoTextClient', func=launch_textclient,
description="Connect to a multiworld using the text client."),
Component('Links Awakening DX Client', 'LinksAwakeningClient',
file_identifier=SuffixIdentifier('.apladx')),
Component('LttP Adjuster', 'LttPAdjuster'),
# Ocarina of Time
Component('OoT Client', 'OoTClient',
@@ -240,67 +243,3 @@ icon_paths = {
'icon': local_path('data', 'icon.png'),
'discord': local_path('data', 'discord-mark-blue.png'),
}
if not is_frozen():
def _build_apworlds(*launch_args: str):
import json
import os
import zipfile
from worlds import AutoWorldRegister
from worlds.Files import APWorldContainer
from Launcher import open_folder
import argparse
parser = argparse.ArgumentParser("Build script for APWorlds")
parser.add_argument("worlds", type=str, default=(), nargs="*", help="Names of APWorlds to build.")
args = parser.parse_args(launch_args)
if args.worlds:
games = [(game, AutoWorldRegister.world_types.get(game, None)) for game in args.worlds]
else:
games = [(worldname, worldtype) for worldname, worldtype in AutoWorldRegister.world_types.items()
if not worldtype.zip_path]
apworlds_folder = os.path.join("build", "apworlds")
os.makedirs(apworlds_folder, exist_ok=True)
for worldname, worldtype in games:
if not worldtype:
logging.error(f"Requested APWorld \"{worldname}\" does not exist.")
continue
file_name = os.path.split(os.path.dirname(worldtype.__file__))[1]
world_directory = os.path.join("worlds", file_name)
if os.path.isfile(os.path.join(world_directory, "archipelago.json")):
with open(os.path.join(world_directory, "archipelago.json"), mode="r", encoding="utf-8") as manifest_file:
manifest = json.load(manifest_file)
assert "game" in manifest, (
f"World directory {world_directory} has an archipelago.json manifest file, but it"
"does not define a \"game\"."
)
assert manifest["game"] == worldtype.game, (
f"World directory {world_directory} has an archipelago.json manifest file, but value of the"
f"\"game\" field ({manifest['game']} does not equal the World class's game ({worldtype.game})."
)
else:
manifest = {}
zip_path = os.path.join(apworlds_folder, file_name + ".apworld")
apworld = APWorldContainer(str(zip_path))
apworld.game = worldtype.game
manifest.update(apworld.get_manifest())
apworld.manifest_path = f"{file_name}/archipelago.json"
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED,
compresslevel=9) as zf:
for path in pathlib.Path(world_directory).rglob("*"):
relative_path = os.path.join(*path.parts[path.parts.index("worlds") + 1:])
if "__MACOSX" in relative_path or ".DS_STORE" in relative_path or "__pycache__" in relative_path:
continue
if not relative_path.endswith("archipelago.json"):
zf.write(path, relative_path)
zf.writestr(apworld.manifest_path, json.dumps(manifest))
open_folder(apworlds_folder)
components.append(Component('Build APWorlds', func=_build_apworlds, cli=True,
description="Build APWorlds from loose-file world folders."))

View File

@@ -7,11 +7,10 @@ import warnings
import zipimport
import time
import dataclasses
import json
from typing import List
from NetUtils import DataPackage
from Utils import local_path, user_path, Version, version_tuple, tuplize_version
from Utils import local_path, user_path
local_folder = os.path.dirname(__file__)
user_folder = user_path("worlds") if user_path() != local_path() else user_path("custom_worlds")
@@ -39,7 +38,6 @@ class WorldSource:
is_zip: bool = False
relative: bool = True # relative to regular world import folder
time_taken: float = -1.0
version: Version = Version(0, 0, 0)
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.path}, is_zip={self.is_zip}, relative={self.relative})"
@@ -104,94 +102,12 @@ for folder in (folder for folder in (user_folder, local_folder) if folder):
# import all submodules to trigger AutoWorldRegister
world_sources.sort()
apworlds: list[WorldSource] = []
for world_source in world_sources:
# load all loose files first:
if world_source.is_zip:
apworlds.append(world_source)
else:
world_source.load()
from .AutoWorld import AutoWorldRegister
for world_source in world_sources:
if not world_source.is_zip:
# look for manifest
manifest = {}
for dirpath, dirnames, filenames in os.walk(world_source.resolved_path):
for file in filenames:
if file.endswith("archipelago.json"):
with open(os.path.join(dirpath, file), mode="r", encoding="utf-8") as manifest_file:
manifest = json.load(manifest_file)
break
if manifest:
break
game = manifest.get("game")
if game in AutoWorldRegister.world_types:
AutoWorldRegister.world_types[game].world_version = tuplize_version(manifest.get("world_version", "0.0.0"))
if apworlds:
# encapsulation for namespace / gc purposes
def load_apworlds() -> None:
global apworlds
from .Files import APWorldContainer, InvalidDataError
core_compatible: list[tuple[WorldSource, APWorldContainer]] = []
def fail_world(game_name: str, reason: str, add_as_failed_to_load: bool = True) -> None:
if add_as_failed_to_load:
failed_world_loads.append(game_name)
logging.warning(reason)
for apworld_source in apworlds:
apworld: APWorldContainer = APWorldContainer(apworld_source.resolved_path)
# populate metadata
try:
apworld.read()
except InvalidDataError as e:
if version_tuple < (0, 7, 0):
logging.error(
f"Invalid or missing manifest file for {apworld_source.resolved_path}. "
"This apworld will stop working with Archipelago 0.7.0."
)
logging.error(e)
else:
raise e
if apworld.minimum_ap_version and apworld.minimum_ap_version > version_tuple:
fail_world(apworld.game,
f"Did not load {apworld_source.path} "
f"as its minimum core version {apworld.minimum_ap_version} "
f"is higher than current core version {version_tuple}.")
elif apworld.maximum_ap_version and apworld.maximum_ap_version < version_tuple:
fail_world(apworld.game,
f"Did not load {apworld_source.path} "
f"as its maximum core version {apworld.maximum_ap_version} "
f"is lower than current core version {version_tuple}.")
else:
core_compatible.append((apworld_source, apworld))
# load highest version first
core_compatible.sort(
key=lambda element: element[1].world_version if element[1].world_version else Version(0, 0, 0),
reverse=True)
for apworld_source, apworld in core_compatible:
if apworld.game and apworld.game in AutoWorldRegister.world_types:
fail_world(apworld.game,
f"Did not load {apworld_source.path} "
f"as its game {apworld.game} is already loaded.",
add_as_failed_to_load=False)
else:
apworld_source.load()
if apworld.game in AutoWorldRegister.world_types:
# world could fail to load at this point
if apworld.world_version:
AutoWorldRegister.world_types[apworld.game].world_version = apworld.world_version
load_apworlds()
del load_apworlds
del apworlds
world_source.load()
# Build the data package for each game.
from .AutoWorld import AutoWorldRegister
network_data_package: DataPackage = {
"games": {world_name: world.get_data_package_data() for world_name, world in AutoWorldRegister.world_types.items()},
}

View File

@@ -23,7 +23,7 @@ game you play will make sure that every game has its own save game.
Unzip the Aquaria randomizer release and copy all unzipped files in the Aquaria game folder. The unzipped files are:
- aquaria_randomizer.exe
- OpenAL32.dll
- randomizer_files (directory)
- override (directory)
- SDL2.dll
- usersettings.xml
- wrap_oal.dll
@@ -32,10 +32,7 @@ Unzip the Aquaria randomizer release and copy all unzipped files in the Aquaria
If there is a conflict between files in the original game folder and the unzipped files, you should overwrite
the original files with the ones from the unzipped randomizer.
There is multiple way to start the game. The easiest one is using the launcher. To do that, just run
the `aquaria_randomizer.exe` file.
You can also launch the randomizer using the command line interface (you can open the command line interface
Finally, to launch the randomizer, you must use the command line interface (you can open the command line interface
by typing `cmd` in the address bar of the Windows File Explorer). Here is the command line used to start the
randomizer:
@@ -52,17 +49,15 @@ aquaria_randomizer.exe --name YourName --server theServer:thePort --password th
### Linux when using the AppImage
If you use the AppImage, just copy it into the Aquaria game folder. You then have to make it executable. You
can do that from the command line by using:
can do that from command line by using:
```bash
chmod +x Aquaria_Randomizer-*.AppImage
```
or by using the Graphical file Explorer of your system (the permission can generally be set in the file properties).
or by using the Graphical Explorer of your system.
To launch the randomizer using the integrated launcher, just execute the AppImage file.
You can also use command line arguments to set the server and slot of your game:
To launch the randomizer, just launch in command line:
```bash
./Aquaria_Randomizer-*.AppImage --name YourName --server theServer:thePort
@@ -84,7 +79,7 @@ the original game will stop working. Copying the folder will guarantee that the
Untar the Aquaria randomizer release and copy all extracted files in the Aquaria game folder. The extracted files are:
- aquaria_randomizer
- randomizer_files (directory)
- override (directory)
- usersettings.xml
- cacert.pem
@@ -92,7 +87,7 @@ If there is a conflict between files in the original game folder and the extract
the original files with the ones from the extracted randomizer files.
Then, you should use your system package manager to install `liblua5`, `libogg`, `libvorbis`, `libopenal` and `libsdl2`.
On Debian base systems (like Ubuntu), you can use the following command:
On Debian base system (like Ubuntu), you can use the following command:
```bash
sudo apt install liblua5.1-0-dev libogg-dev libvorbis-dev libopenal-dev libsdl2-dev
@@ -102,9 +97,7 @@ Also, if there are certain `.so` files in the original Aquaria game folder (`lib
`libSDL-1.2.so.0` and `libstdc++.so.6`), you should remove them from the Aquaria Randomizer game folder. Those are
old libraries that will not work on the recent build of the randomizer.
To launch the randomizer using the integrated launcher, just execute the `aquaria_randomizer` file.
You can also use command line arguments to set the server and slot of your game:
To launch the randomizer, just launch in command line:
```bash
./aquaria_randomizer --name YourName --server theServer:thePort
@@ -122,20 +115,6 @@ sure that your executable has executable permission:
```bash
chmod +x aquaria_randomizer
```
### Steam deck
On the Steamdeck, go in desktop mode and follow the same procedure as the Linux Appimage.
### No sound on Linux/Steam deck
If your game play without problems, but with no sound, the game probably does not use the correct
driver for the sound system. To fix that, you can use `ALSOFT_DRIVERS=pulse` before your command
line to make it work. Something like this (depending on the way you launch the randomizer):
```bash
ALSOFT_DRIVERS=pulse ./Aquaria_Randomizer-*.AppImage --name YourName --server theServer:thePort
```
## Auto-Tracking

View File

@@ -2,12 +2,12 @@
## Logiciels nécessaires
- Une copie du jeu Aquaria non modifiée (disponible sur la majorité des sites de ventes de jeux vidéos en ligne)
- Une copie du jeu Aquaria non-modifiée (disponible sur la majorité des sites de ventes de jeux vidéos en ligne)
- Le client du Randomizer d'Aquaria [Aquaria randomizer](https://github.com/tioui/Aquaria_Randomizer/releases/latest)
## Logiciels optionnels
- De manière optionnelle, pour pouvoir envoyer des [commandes](/tutorial/Archipelago/commands/en) comme `!hint`: utilisez le client texte de [la version la plus récente d'Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest)
- De manière optionnel, pour pouvoir envoyer des [commandes](/tutorial/Archipelago/commands/en) comme `!hint`: utilisez le client texte de [la version la plus récente d'Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest)
- [Aquaria AP Tracker](https://github.com/palex00/aquaria-ap-tracker/releases/latest), pour utiliser avec [PopTracker](https://github.com/black-sliver/PopTracker/releases/latest)
## Procédures d'installation et d'exécution
@@ -25,7 +25,7 @@ Désarchiver le randomizer d'Aquaria et copier tous les fichiers de l'archive da
fichier d'archive devrait contenir les fichiers suivants:
- aquaria_randomizer.exe
- OpenAL32.dll
- randomizer_files (directory)
- override (directory)
- SDL2.dll
- usersettings.xml
- wrap_oal.dll
@@ -34,10 +34,7 @@ fichier d'archive devrait contenir les fichiers suivants:
S'il y a des conflits entre les fichiers de l'archive zip et les fichiers du jeu original, vous devez utiliser
les fichiers contenus dans l'archive zip.
Il y a plusieurs manières de lancer le randomizer. Le plus simple consiste à utiliser le lanceur intégré en
exécutant simplement le fichier `aquaria_randomizer.exe`.
Il est également possible de lancer le randomizer en utilisant la ligne de commande (vous pouvez ouvrir une interface de
Finalement, pour lancer le randomizer, vous devez utiliser la ligne de commande (vous pouvez ouvrir une interface de
ligne de commande, entrez l'adresse `cmd` dans la barre d'adresse de l'explorateur de fichier de Windows). Voici
la ligne de commande à utiliser pour lancer le randomizer:
@@ -60,12 +57,9 @@ le mettre exécutable. Vous pouvez mettre le fichier exécutable avec la command
chmod +x Aquaria_Randomizer-*.AppImage
```
ou bien en utilisant l'explorateur de fichier graphique de votre système (la permission d'exécution est
généralement dans les propriétés du fichier).
ou bien en utilisant l'explorateur graphique de votre système.
Pour lancer le randomizer en utilisant le lanceur intégré, seulement exécuter le fichier AppImage.
Vous pouvez également lancer le randomizer en spécifiant les informations de connexion dans les arguments de la ligne de commande:
Pour lancer le randomizer, utiliser la commande suivante:
```bash
./Aquaria_Randomizer-*.AppImage --name VotreNom --server LeServeur:LePort
@@ -89,7 +83,7 @@ avant de déposer le randomizer à l'intérieur permet de vous assurer de garder
Désarchiver le fichier tar et copier tous les fichiers qu'il contient dans le répertoire du jeu d'origine d'Aquaria. Les
fichiers extraient du fichier tar devraient être les suivants:
- aquaria_randomizer
- randomizer_files (directory)
- override (directory)
- usersettings.xml
- cacert.pem
@@ -108,10 +102,7 @@ Notez également que s'il y a des fichiers ".so" dans le répertoire d'Aquaria (
`libSDL-1.2.so.0` and `libstdc++.so.6`), vous devriez les retirer. Il s'agit de vieille version des librairies qui
ne sont plus fonctionnelles dans les systèmes modernes et qui pourrait empêcher le randomizer de fonctionner.
Pour lancer le randomizer en utilisant le lanceur intégré, seulement exécuter le fichier `aquaria_randomizer`.
Vous pouvez également lancer le randomizer en spécifiant les information de connexion dans les arguments de la
ligne de commande:
Pour lancer le randomizer, utiliser la commande suivante:
```bash
./aquaria_randomizer --name VotreNom --server LeServeur:LePort
@@ -129,21 +120,6 @@ pour vous assurer que votre fichier est exécutable:
```bash
chmod +x aquaria_randomizer
```
### Steam Deck
Pour installer le randomizer sur la Steam Deck, seulement suivre la procédure pour les fichiers AppImage
indiquée précédemment.
### Aucun son sur Linux/Steam Deck
Si le jeu fonctionne sans problème, mais qu'il n'y a aucun son, c'est probablement parce que le jeu
n'arrive pas à utiliser le bon pilote de son. Généralement, le problème est réglé en ajoutant la
variable d'environnement `ALSOFT_DRIVERS=pulse`. Voici un exemple (peut varier en fonction de la manière
que le randomizer est lancé):
```bash
ALSOFT_DRIVERS=pulse ./Aquaria_Randomizer-*.AppImage --name VotreNom --server LeServeur:LePort
```
## Tracking automatique

View File

@@ -1,6 +0,0 @@
{
"game": "Bumper Stickers",
"authors": ["KewlioMZX"],
"world_version": "1.0.0",
"minimum_ap_version": "0.6.4"
}

View File

@@ -232,9 +232,11 @@ def create_regions(world: MultiWorld, options: CCCharlesOptions, player: int) ->
# Connect the Regions by named Entrances that must have access Rules
menu_region.connect(start_camp_region)
menu_region.connect(tony_tiddle_mission_region)
menu_region.connect(barn_region, "Barn Door")
menu_region.connect(barn_region)
tony_tiddle_mission_region.connect(barn_region, "Barn Door")
menu_region.connect(candice_mission_region)
menu_region.connect(tutorial_house_region, "Tutorial House Door")
menu_region.connect(tutorial_house_region)
candice_mission_region.connect(tutorial_house_region, "Tutorial House Door")
menu_region.connect(swamp_edges_region)
menu_region.connect(swamp_mission_region)
menu_region.connect(junkyard_area_region)
@@ -242,6 +244,7 @@ def create_regions(world: MultiWorld, options: CCCharlesOptions, player: int) ->
menu_region.connect(junkyard_shed_region)
menu_region.connect(military_base_region)
menu_region.connect(south_mine_outside_region)
menu_region.connect(south_mine_inside_region)
south_mine_outside_region.connect(south_mine_inside_region, "South Mine Gate")
menu_region.connect(middle_station_region)
menu_region.connect(canyon_region)
@@ -255,11 +258,13 @@ def create_regions(world: MultiWorld, options: CCCharlesOptions, player: int) ->
menu_region.connect(lost_stairs_region)
menu_region.connect(east_house_region)
menu_region.connect(rockets_testing_ground_region)
menu_region.connect(rockets_testing_bunker_region)
rockets_testing_ground_region.connect(rockets_testing_bunker_region, "Stuck Bunker Door")
menu_region.connect(workshop_region)
menu_region.connect(east_tower_region)
menu_region.connect(lighthouse_region)
menu_region.connect(north_mine_outside_region)
menu_region.connect(north_mine_inside_region)
north_mine_outside_region.connect(north_mine_inside_region, "North Mine Gate")
menu_region.connect(wood_bridge_region)
menu_region.connect(museum_region)
@@ -273,9 +278,11 @@ def create_regions(world: MultiWorld, options: CCCharlesOptions, player: int) ->
menu_region.connect(north_beach_region)
menu_region.connect(mine_shaft_region)
menu_region.connect(mob_camp_region)
menu_region.connect(mob_camp_locked_room_region)
mob_camp_region.connect(mob_camp_locked_room_region, "Mob Camp Locked Door")
menu_region.connect(mine_elevator_exit_region)
menu_region.connect(mountain_ruin_outside_region)
menu_region.connect(mountain_ruin_inside_region)
mountain_ruin_outside_region.connect(mountain_ruin_inside_region, "Mountain Ruin Gate")
menu_region.connect(prism_temple_region)
menu_region.connect(pickle_val_region)

View File

@@ -1,27 +0,0 @@
Modified MIT License
Copyright (c) 2025 PoryGone
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, and/or distribute copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to
the following conditions:
No copy or substantial portion of the Software shall be sublicensed or relicensed
without the express written permission of the copyright holder(s)
No copy or substantial portion of the Software shall be sold without the express
written permission of the copyright holder(s)
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,6 +0,0 @@
{
"game": "Celeste 64",
"authors": [ "PoryGone" ],
"minimum_ap_version": "0.6.3",
"world_version": "1.3.1"
}

View File

@@ -1,27 +0,0 @@
Modified MIT License
Copyright (c) 2025 PoryGone
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, and/or distribute copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to
the following conditions:
No copy or substantial portion of the Software shall be sublicensed or relicensed
without the express written permission of the copyright holder(s)
No copy or substantial portion of the Software shall be sold without the express
written permission of the copyright holder(s)
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,6 +0,0 @@
{
"game": "Celeste (Open World)",
"authors": [ "PoryGone" ],
"minimum_ap_version": "0.6.3",
"world_version": "1.0.5"
}

View File

@@ -20,7 +20,6 @@ class CivVIBoostData:
Prereq: List[str]
PrereqRequiredCount: int
Classification: str
EraRequired: bool = False
class GoodyHutRewardData(TypedDict):

View File

@@ -150,10 +150,7 @@ def generate_era_location_table() -> Dict[str, Dict[str, CivVILocationData]]:
location = CivVILocationData(
boost.Type, 0, 0, id_base, boost.EraType, CivVICheckType.BOOST
)
# If EraRequired is True, place the boost in its actual era
# Otherwise, place it in ERA_ANCIENT for early access
target_era = boost.EraType if boost.EraRequired else "ERA_ANCIENT"
era_locations[target_era][boost.Type] = location
era_locations["ERA_ANCIENT"][boost.Type] = location
id_base += 1
return era_locations

View File

@@ -210,8 +210,8 @@ boosts: List[CivVIBoostData] = [
CivVIBoostData(
"BOOST_TECH_SQUARE_RIGGING",
"ERA_RENAISSANCE",
["TECH_GUNPOWDER", "TECH_MILITARY_ENGINEERING", "TECH_MINING"],
3,
["TECH_GUNPOWDER"],
1,
"DEFAULT",
),
CivVIBoostData(
@@ -252,15 +252,15 @@ boosts: List[CivVIBoostData] = [
CivVIBoostData(
"BOOST_TECH_BALLISTICS",
"ERA_INDUSTRIAL",
["TECH_SIEGE_TACTICS", "TECH_MILITARY_ENGINEERING", "TECH_BRONZE_WORKING"],
3,
["TECH_SIEGE_TACTICS", "TECH_MILITARY_ENGINEERING"],
2,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_MILITARY_SCIENCE",
"ERA_INDUSTRIAL",
["TECH_BRONZE_WORKING", "TECH_STIRRUPS", "TECH_MINING"],
3,
["TECH_STIRRUPS"],
1,
"DEFAULT",
),
CivVIBoostData(
@@ -301,8 +301,8 @@ boosts: List[CivVIBoostData] = [
CivVIBoostData(
"BOOST_TECH_REPLACEABLE_PARTS",
"ERA_MODERN",
["TECH_MILITARY_SCIENCE", "TECH_MINING"],
2,
["TECH_MILITARY_SCIENCE"],
1,
"DEFAULT",
),
CivVIBoostData(
@@ -343,8 +343,8 @@ boosts: List[CivVIBoostData] = [
CivVIBoostData(
"BOOST_TECH_ADVANCED_FLIGHT",
"ERA_ATOMIC",
["TECH_FLIGHT", "TECH_REFINING", "TECH_MINING"],
3,
["TECH_FLIGHT"],
1,
"DEFAULT",
),
CivVIBoostData(
@@ -436,8 +436,8 @@ boosts: List[CivVIBoostData] = [
CivVIBoostData(
"BOOST_TECH_COMPOSITES",
"ERA_INFORMATION",
["TECH_COMBUSTION", "TECH_REFINING", "TECH_MINING"],
3,
["TECH_COMBUSTION"],
1,
"DEFAULT",
),
CivVIBoostData(
@@ -470,7 +470,7 @@ boosts: List[CivVIBoostData] = [
"TECH_ELECTRICITY",
"TECH_NUCLEAR_FISSION",
],
4,
1,
"DEFAULT",
),
CivVIBoostData(
@@ -651,11 +651,10 @@ boosts: List[CivVIBoostData] = [
),
CivVIBoostData(
"BOOST_CIVIC_FEUDALISM",
"ERA_CLASSICAL",
"ERA_MEDIEVAL",
[],
0,
"DEFAULT",
True,
),
CivVIBoostData(
"BOOST_CIVIC_CIVIL_SERVICE",
@@ -663,7 +662,6 @@ boosts: List[CivVIBoostData] = [
[],
0,
"DEFAULT",
True,
),
CivVIBoostData(
"BOOST_CIVIC_MERCENARIES",
@@ -792,7 +790,6 @@ boosts: List[CivVIBoostData] = [
[],
0,
"DEFAULT",
True
),
CivVIBoostData(
"BOOST_CIVIC_CONSERVATION",
@@ -888,7 +885,6 @@ boosts: List[CivVIBoostData] = [
["TECH_ROCKETRY"],
1,
"DEFAULT",
True
),
CivVIBoostData(
"BOOST_CIVIC_GLOBALIZATION",

View File

@@ -14,27 +14,25 @@ The following are required in order to play Civ VI in Archipelago:
- A copy of the game `Civilization VI` including the two expansions `Rise & Fall` and `Gathering Storm` (both the Steam and Epic version should work).
## Enabling the tuner
In the main menu, navigate to the "Game Options" page. On the "Game" menu, make sure that "Tuner (disables achievements)" is enabled.
## Mod Installation
1. Download and unzip the latest release of the mod from [GitHub](https://github.com/hesto2/civilization_archipelago_mod/releases/latest).
2. Copy the folder containing the mod files to your Civ VI mods folder. On Windows, this is usually located at `C:\Users\YOUR_USER\Documents\My Games\Sid Meier's Civilization VI\Mods`. If you use OneDrive, check if the folder is instead located in your OneDrive file structure, and use that path when relevant in future steps.
3. After the Archipelago host generates a game, you should be given a `.apcivvi` file. You can open it as a zip file, you can do this by either right clicking it and opening it with a program that handles zip files (if you associate that file with the program it will open it with that program in the future by double clicking it), or by right clicking and renaming the file extension from `apcivvi` to `zip` (only works if you are displaying file extensions). You can also associate the file with the Archipelago Launcher and double click it and it will create a folder with the mod files inside of it.
3. After the Archipelago host generates a game, you should be given a `.apcivvi` file. Associate the file with the Archipelago Launcher and double click it.
4. Copy the contents of the zip file or folder it generated (the name of the folder should be the same as the apcivvi file) into your Civilization VI Archipelago Mod folder (there should be five files placed there from the `.apcivvi` file, overwrite if asked).
4. Copy the contents of the new folder it generates (it will have the same name as the `.apcivvi` file) into your Civilization VI Archipelago Mod folder. If double clicking the `.apcivvi` file doesn't generate a folder, you can instead open it as a zip file. You can do this by either right clicking it and opening it with a program that handles zip files, or by right clicking and renaming the file extension from `apcivvi` to `zip`.
5. Your mod path should look something like `C:\Users\YOUR_USER\Documents\My Games\Sid Meier's Civilization VI\Mods\civilization_archipelago_mod`. If everything was done correctly you can now connect to the game.
5. Place the files generated from the `.apcivvi` in your archipelago mod folder (there should be five files placed there from the apcivvi file, overwrite if asked). Your mod path should look something like `C:\Users\YOUR_USER\Documents\My Games\Sid Meier's Civilization VI\Mods\civilization_archipelago_mod`.
## Connecting to a game
## Configuring your game
1. In the main menu, navigate to the "Game Options" page. On the "Game" menu, make sure that "Tuner (disables achievements)" is enabled.
2. In the main menu, navigate to the "Additional Content" page, then go to "Mods" and make sure the Archipelago mod is enabled.
3. When starting the game make sure you are on the Gathering Storm ruleset in a Single Player game. Additionally you must start in the ancient era, other settings and game modes can be customised to your own liking. An important thing to note is that settings preset saves the mod list from when you created it, so if you want to use a setting preset with this you must create it after installing the Archipelago mod.
4. To connect to the room open the Archipelago Launcher, from within the launcher open the Civ6 client and connect to the room. Once connected to the room enter your slot name and if everything went right you should now be connected.
Make sure you enable the mod in the main title under Additional Content > Mods. When configuring your game, make sure to start the game in the Ancient Era and leave all settings related to starting technologies and civics as the defaults. Other than that, configure difficulty, AI, etc. as you normally would.
## Troubleshooting
@@ -53,8 +51,3 @@ The following are required in order to play Civ VI in Archipelago:
- If you still have any errors make sure the two expansions Rise & Fall and Gathering Storm are active in the mod selector (all the official DLC works without issues but Rise & Fall and Gathering Storm are required for the mod).
- If boostsanity is enabled and those items are not being sent out but regular techs are, make sure you placed the files from your new room in the mod folder.
- If you are neither receiving or sending items, make sure you have the correct client open. The client should be the Civ6 and NOT the Text Client.
- This should be compatible with a lot of other mods, but if you are having issues try disabling all mods other than the Archipelago mod and see if the problem still persists.

View File

@@ -105,78 +105,3 @@ class TestBoostsanityExcluded(CivVITestBase):
if "BOOST" in location.name:
found_locations += 1
self.assertEqual(found_locations, 0)
class TestBoostsanityEraRequired(CivVITestBase):
options = {
"boostsanity": "true",
"progression_style": "none",
"shuffle_goody_hut_rewards": "false",
}
def test_era_required_boosts_not_accessible_early(self) -> None:
# BOOST_CIVIC_FEUDALISM has EraRequired=True and ERA_CLASSICAL
# It should NOT be accessible in Ancient era
self.assertFalse(self.can_reach_location("BOOST_CIVIC_FEUDALISM"))
# BOOST_CIVIC_URBANIZATION has EraRequired=True and ERA_INDUSTRIAL
# It should NOT be accessible in Ancient era
self.assertFalse(self.can_reach_location("BOOST_CIVIC_URBANIZATION"))
# BOOST_CIVIC_SPACE_RACE has EraRequired=True and ERA_ATOMIC
# It should NOT be accessible in Ancient era
self.assertFalse(self.can_reach_location("BOOST_CIVIC_SPACE_RACE"))
# Regular boosts without EraRequired should be accessible
self.assertTrue(self.can_reach_location("BOOST_TECH_SAILING"))
self.assertTrue(self.can_reach_location("BOOST_CIVIC_MILITARY_TRADITION"))
def test_era_required_boosts_accessible_in_correct_era(self) -> None:
# Collect items to reach Classical era
self.collect_by_name(["Mining", "Bronze Working", "Astrology", "Writing",
"Irrigation", "Sailing", "Animal Husbandry",
"State Workforce", "Foreign Trade"])
# BOOST_CIVIC_FEUDALISM should now be accessible in Classical era
self.assertTrue(self.can_reach_location("BOOST_CIVIC_FEUDALISM"))
# BOOST_CIVIC_URBANIZATION still not accessible (requires Industrial)
self.assertFalse(self.can_reach_location("BOOST_CIVIC_URBANIZATION"))
# Collect more items to reach Industrial era
self.collect_all_but(["TECH_ROCKETRY"])
# Now BOOST_CIVIC_URBANIZATION should be accessible
self.assertTrue(self.can_reach_location("BOOST_CIVIC_URBANIZATION"))
class TestBoostsanityEraRequiredWithProgression(CivVITestBase):
options = {
"boostsanity": "true",
"progression_style": "eras_and_districts",
"shuffle_goody_hut_rewards": "false",
}
def test_era_required_with_progressive_eras(self) -> None:
# Collect all items except Progressive Era
self.collect_all_but(["Progressive Era"])
# Even with all other items, era-required boosts should not be accessible
self.assertFalse(self.can_reach_location("BOOST_CIVIC_FEUDALISM"))
self.assertFalse(self.can_reach_location("BOOST_CIVIC_URBANIZATION"))
# Collect enough Progressive Era items to reach Classical (needs 2)
self.collect(self.get_item_by_name("Progressive Era"))
self.collect(self.get_item_by_name("Progressive Era"))
# BOOST_CIVIC_FEUDALISM should now be accessible
self.assertTrue(self.can_reach_location("BOOST_CIVIC_FEUDALISM"))
# But BOOST_CIVIC_URBANIZATION still requires Industrial era (needs 5 total)
self.assertFalse(self.can_reach_location("BOOST_CIVIC_URBANIZATION"))
# Collect 3 more Progressive Era items to reach Industrial
self.collect_by_name(["Progressive Era", "Progressive Era", "Progressive Era"])
# Now BOOST_CIVIC_URBANIZATION should be accessible
self.assertTrue(self.can_reach_location("BOOST_CIVIC_URBANIZATION"))

View File

@@ -1,27 +0,0 @@
Modified MIT License
Copyright (c) 2025 PoryGone
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, and/or distribute copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to
the following conditions:
No copy or substantial portion of the Software shall be sublicensed or relicensed
without the express written permission of the copyright holder(s)
No copy or substantial portion of the Software shall be sold without the express
written permission of the copyright holder(s)
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,6 +0,0 @@
{
"game": "Donkey Kong Country 3",
"authors": [ "PoryGone" ],
"minimum_ap_version": "0.6.3",
"world_version": "1.1.0"
}

View File

@@ -2,7 +2,6 @@ import csv
import enum
import math
from dataclasses import dataclass, field
from functools import reduce
from random import Random
from typing import Dict, List, Set
@@ -62,7 +61,7 @@ def load_item_csv():
item_reader = csv.DictReader(file)
for item in item_reader:
id = int(item["id"]) if item["id"] else None
classification = reduce((lambda a, b: a | b), {ItemClassification[str_classification] for str_classification in item["classification"].split(",")})
classification = ItemClassification[item["classification"]]
groups = {Group[group] for group in item["groups"].split(",") if group}
items.append(ItemData(id, item["name"], classification, groups))
return items

View File

@@ -22,7 +22,7 @@ id,name,classification,groups
20,Wall Jump Pack,progression,"DLC,Freemium"
21,Health Bar Pack,useful,"DLC,Freemium"
22,Parallax Pack,filler,"DLC,Freemium"
23,Harmless Plants Pack,"progression,trap","DLC,Freemium"
23,Harmless Plants Pack,progression,"DLC,Freemium"
24,Death of Comedy Pack,progression,"DLC,Freemium"
25,Canadian Dialog Pack,filler,"DLC,Freemium"
26,DLC NPC Pack,progression,"DLC,Freemium"
1 id name classification groups
22 20 Wall Jump Pack progression DLC,Freemium
23 21 Health Bar Pack useful DLC,Freemium
24 22 Parallax Pack filler DLC,Freemium
25 23 Harmless Plants Pack progression,trap progression DLC,Freemium
26 24 Death of Comedy Pack progression DLC,Freemium
27 25 Canadian Dialog Pack filler DLC,Freemium
28 26 DLC NPC Pack progression DLC,Freemium

View File

@@ -59,19 +59,6 @@ class FactorioCommandProcessor(ClientCommandProcessor):
def _cmd_toggle_chat(self):
"""Toggle sending of chat messages from players on the Factorio server to Archipelago."""
self.ctx.toggle_bridge_chat_out()
def _cmd_rcon_reconnect(self) -> bool:
"""Reconnect the RCON client if its disconnected."""
try:
result = self.ctx.rcon_client.send_command("/help")
if result:
self.output("RCON Client already connected.")
return True
except factorio_rcon.RCONNetworkError:
self.ctx.rcon_client = factorio_rcon.RCONClient("localhost", self.ctx.rcon_port, self.ctx.rcon_password, timeout=5)
self.output("RCON Client successfully reconnected.")
return True
return False
class FactorioContext(CommonContext):
@@ -255,13 +242,7 @@ async def game_watcher(ctx: FactorioContext):
if ctx.rcon_client and time.perf_counter() > next_bridge:
next_bridge = time.perf_counter() + 1
ctx.awaiting_bridge = False
try:
data = json.loads(ctx.rcon_client.send_command("/ap-sync"))
except factorio_rcon.RCONNotConnected:
continue
except factorio_rcon.RCONNetworkError:
bridge_logger.warning("RCON Client has unexpectedly lost connection. Please issue /rcon_reconnect.")
continue
data = json.loads(ctx.rcon_client.send_command("/ap-sync"))
if not ctx.auth:
pass # auth failed, wait for new attempt
elif data["slot_name"] != ctx.auth:
@@ -313,13 +294,9 @@ async def game_watcher(ctx: FactorioContext):
"cmd": "Set", "key": ctx.energylink_key, "operations":
[{"operation": "add", "value": value}]
}]))
try:
ctx.rcon_client.send_command(
f"/ap-energylink -{value}")
except factorio_rcon.RCONNetworkError:
bridge_logger.warning("RCON Client has unexpectedly lost connection. Please issue /rcon_reconnect.")
else:
logger.debug(f"EnergyLink: Sent {format_SI_prefix(value)}J")
ctx.rcon_client.send_command(
f"/ap-energylink -{value}")
logger.debug(f"EnergyLink: Sent {format_SI_prefix(value)}J")
await asyncio.sleep(0.1)

View File

@@ -16,7 +16,6 @@ logger = logging.getLogger("Client")
rom_name_location = 0x07FFE3
player_name_location = 0x07BCC0
locations_array_start = 0x200
locations_array_length = 0x100
items_obtained = 0x03
@@ -112,12 +111,6 @@ class FF1Client(BizHawkClient):
return True
async def set_auth(self, ctx: "BizHawkClientContext") -> None:
auth_raw = (await bizhawk.read(
ctx.bizhawk_ctx,
[(player_name_location, 0x40, self.rom)]))[0]
ctx.auth = str(auth_raw, "utf-8").replace("\x00", "").strip()
async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
if ctx.server is None:
return
@@ -211,7 +204,7 @@ class FF1Client(BizHawkClient):
write_list.append((location, [0], self.sram))
elif current_item_name in no_overworld_items:
if current_item_name == "Sigil":
location = 0x2B
location = 0x28
else:
location = 0x12
write_list.append((location, [1], self.sram))

View File

@@ -253,17 +253,5 @@
"CubeBot": 529,
"Sarda": 525,
"Fairy": 531,
"Lefein": 527,
"DeepDungeon32B_Chest144": 401,
"DeepDungeon30B_Chest145": 402,
"DeepDungeon29B_Chest146": 403,
"DeepDungeon29B_Chest147": 404,
"DeepDungeon40B_Chest186": 443,
"DeepDungeon38B_Chest188": 445,
"DeepDungeon36B_Chest189": 446,
"DeepDungeon33B_Chest190": 447,
"DeepDungeon40B_Chest191": 448,
"DeepDungeon41B_Chest192": 449,
"DeepDungeon34B_Chest193": 450,
"DeepDungeon39B_Chest194": 451
"Lefein": 527
}

View File

@@ -1,5 +1,4 @@
from typing import NamedTuple, Union
from typing_extensions import deprecated
import logging
from BaseClasses import Item, Tutorial, ItemClassification
@@ -50,8 +49,7 @@ class GenericWorld(World):
return Item(name, ItemClassification.filler, -1, self.player)
raise InvalidItemError(name)
@deprecated("worlds.generic.PlandoItem is deprecated and will be removed in the next version. "
"Use Options.PlandoItem(s) instead.")
class PlandoItem(NamedTuple):
item: str
location: str

View File

@@ -81,8 +81,7 @@ are `description`, `name`, `game`, `requires`, and the name of the games you wan
* `requires` details different requirements from the generator for the YAML to work as you expect it to. Generally this
is good for detailing the version of Archipelago this YAML was prepared for. If it is rolled on an older version,
options may be missing and as such it will not work as expected. If any plando is used in the file then requiring it
here to ensure it will be used is good practice. Specific versions of custom worlds can also be required, ensuring
that the generator is using a compatible version.
here to ensure it will be used is good practice.
## Game Options
@@ -166,9 +165,7 @@ game:
A Link to the Past: 10
Timespinner: 10
requires:
version: 0.6.4
game:
A Link to the Past: 0.6.4
version: 0.4.1
A Link to the Past:
accessibility: minimal
progression_balancing: 50
@@ -217,13 +214,12 @@ Timespinner:
progression_balancing: 50
item_links: # Share part of your item pool with other players.
- name: TSAll
item_pool:
item_pool:
- Everything
local_items:
- Twin Pyramid Key
- Timespinner Wheel
replacement_item: null
skip_if_solo: true
```
#### This is a fully functional yaml file that will do all the following things:
@@ -232,7 +228,7 @@ Timespinner:
* `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. This is
because each game has a weight of 10 and the total of all weights is 20.
* `requires` is set to require Archipelago release version 0.6.4 or higher, as well as A Link to the Past version 0.6.4.
* `requires` is set to required release version 0.3.2 or higher.
* `accessibility` for both games is set to `minimal` 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` for both games is set to 50, the default value, meaning we will likely receive important items
@@ -266,7 +262,7 @@ Timespinner:
* 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 to replace the
player items. This item link will only be created if there are at least two players in the group.
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. More information on triggers can be found in the

View File

@@ -136,27 +136,6 @@ are rolling locally, ensure this file is edited to your liking **before** rollin
when running the Archipelago Installation software. If you have changed settings in this file, and would like to retain
them, you may rename the file to `options.yaml`.
### Playing with custom worlds
If you are generating locally, you can play with worlds that are not included in the Archipelago installation.
These worlds are packaged as `.apworld` files. To add a world to your installation, click the "Install APWorld" button
in the launcher and select the `.apworld` file you wish to install. Alternatively, you can drag the `.apworld` file
onto the launcher or double-click the file itself (if on Windows). Once the world is installed, it will function like
the worlds that are already packaged with Archipelago. Also note that while generation with custom worlds must be done
locally, these games can then be uploaded to the website for hosting and played as normal.
We strongly recommend that you ensure the source of the `.apworld` is safe and trustworthy before playing with a
custom world. Installed APWorlds are able to run custom code on your computer whenever you open Archipelago.
#### Alternate versions of included worlds
If you want to play with an alternate version of a game that is already included in Archipelago, you should also
remove the original APWorld after completing the above installation. To do so, go to your Archipelago installation
folder and navigate to the `lib/worlds` directory. Then move the `.apworld` or the folder corresponding to the game you
want to play an alternate version of to somewhere else as a backup. If you want to play this original again, then
restore the original version to `lib/worlds` and remove the alternate version, which is in the `custom_worlds` folder.
Note: Currently, this cannot be done on the Linux AppImage release.
## Hosting an Archipelago Server

View File

@@ -5,8 +5,6 @@
* A legal copy of Hollow Knight.
* Steam, Gog, and Xbox Game Pass versions of the game are supported.
* Windows, Mac, and Linux (including Steam Deck) are supported.
**Do NOT** install BepInEx, it is not required and is incompatible with most mods. Archipelago, along with the majority of mods use custom tooling pre-dating BepInEx, and they are only available through Lumafly and similar installers rather than sites like Nexus Mods.
## Installing the Archipelago Mod using Lumafly
1. Launch Lumafly and ensure it locates your Hollow Knight installation directory.

View File

@@ -5,10 +5,6 @@
* Tener una copia legal de Hollow Knight.
* Las versiones de Steam, GOG y Xbox Game Pass son compatibles
* Las versiones de Windows, Mac y Linux (Incluyendo Steam Deck) son compatibles
**NO** instales BepInEx, **no** es necesario y es incompatible con varios mods. Archipelago (y la mayoría de los mods)
usan herramientas más antiguas que BepInEx, que solo están disponibles por medio de instaladores de mods como Lumafly y
similares, en vez de sitios web como Nexus Mods.
## Instalación del mod de Archipelago con Lumafly
1. Ejecuta Lumafly y asegurate de localizar la carpeta de instalación de Hollow Knight
@@ -65,4 +61,4 @@ de Archipelago para generar un YAML usando una interfaz gráfica.
## Consejos y otros comandos
Mientras juegas en un multiworld, puedes interactuar con el servidor usando varios comandos listados en la
[guía de comandos](/tutorial/Archipelago/commands/en). Puedes usar el Cliente de Texto Archipelago para hacer esto,
que está incluido en la última versión del [software de Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest).
que está incluido en la última versión del [software de Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest).

View File

@@ -5,10 +5,6 @@
* Uma cópia legal de Hollow Knight.
* Versões Steam, Gog, e Xbox Game Pass do jogo são suportadas.
* Windows, Mac, e Linux (incluindo Steam Deck) são suportados.
**NÃO** instale o BepInEx, ele **não** é necessário e é incompatível com vários mods. O Archipelago (e a maioria dos mods)
usam ferramentas mais antigas do que o BepInEx, disponíveis apenas a partir de gerenciadores como o Lumafly e semelhantes,
ao invés de sites como o Nexus Mods.
## Instalando o mod Archipelago Mod usando Lumafly
1. Abra o Lumafly e confirme que ele localizou sua pasta de instalação do Hollow Knight.

View File

@@ -1,6 +0,0 @@
{
"game": "Jak and Daxter: The Precursor Legacy",
"world_version": "1.0.0",
"minimum_ap_version": "0.6.2",
"authors": ["markustulliuscicero"]
}

View File

@@ -6,8 +6,7 @@ class TradesCostNothingTest(JakAndDaxterTestBase):
"enable_orbsanity": 2,
"global_orbsanity_bundle_size": 10,
"citizen_orb_trade_amount": 0,
"oracle_orb_trade_amount": 0,
"start_inventory": {"Power Cell": 100},
"oracle_orb_trade_amount": 0
}
def test_orb_items_are_filler(self):
@@ -25,8 +24,7 @@ class TradesCostEverythingTest(JakAndDaxterTestBase):
"enable_orbsanity": 2,
"global_orbsanity_bundle_size": 10,
"citizen_orb_trade_amount": 120,
"oracle_orb_trade_amount": 150,
"start_inventory": {"Power Cell": 100},
"oracle_orb_trade_amount": 150
}
def test_orb_items_are_progression(self):

View File

@@ -303,6 +303,9 @@ class KDL3World(World):
def generate_basic(self) -> None:
self.stage_shuffle_enabled = self.options.stage_shuffle > 0
goal = self.options.goal.value
goal_location = self.multiworld.get_location(location_name.goals[goal], self.player)
goal_location.place_locked_item(KDL3Item("Love-Love Rod", ItemClassification.progression, None, self.player))
for level in range(1, 6):
self.multiworld.get_location(f"Level {level} Boss - Defeated", self.player) \
.place_locked_item(
@@ -310,6 +313,7 @@ class KDL3World(World):
self.multiworld.get_location(f"Level {level} Boss - Purified", self.player) \
.place_locked_item(
KDL3Item(f"Level {level} Boss Purified", ItemClassification.progression, None, self.player))
self.multiworld.completion_condition[self.player] = lambda state: state.has("Love-Love Rod", self.player)
# this can technically be done at any point before generate_output
if self.options.allow_bb:
if self.options.allow_bb == self.options.allow_bb.option_enforced:

View File

@@ -1,8 +1,6 @@
from BaseClasses import ItemClassification
from worlds.generic.Rules import set_rule, add_rule
from .items import KDL3Item
from .locations import location_table
from .names import location_name, enemy_abilities, animal_friend_spawns
from .locations import location_table
from .options import GoalSpeed
import typing
@@ -113,11 +111,6 @@ def can_fix_angel_wings(state: "CollectionState", player: int, copy_abilities: t
def set_rules(world: "KDL3World") -> None:
goal = world.options.goal.value
goal_location = world.multiworld.get_location(location_name.goals[goal], world.player)
goal_location.place_locked_item(KDL3Item("Love-Love Rod", ItemClassification.progression, None, world.player))
world.multiworld.completion_condition[world.player] = lambda state: state.has("Love-Love Rod", world.player)
# Level 1
set_rule(world.multiworld.get_location(location_name.grass_land_muchi, world.player),
lambda state: can_reach_chuchu(state, world.player))

View File

@@ -13,6 +13,8 @@ import ModuleUpdate
ModuleUpdate.update()
import Utils
death_link = False
item_num = 1
logger = logging.getLogger("Client")
@@ -32,57 +34,62 @@ class KH1ClientCommandProcessor(ClientCommandProcessor):
def __init__(self, ctx):
super().__init__(ctx)
def _cmd_slot_data(self):
"""Prints slot data settings for the connected seed"""
for key in self.ctx.slot_data.keys():
if key not in ["remote_location_ids", "synthesis_item_name_byte_arrays"]:
self.output(str(key) + ": " + str(self.ctx.slot_data[key]))
def _cmd_deathlink(self):
"""If your Death Link setting is set to "Toggle", use this command to turn Death Link on and off."""
if "death_link" in self.ctx.slot_data.keys():
if self.ctx.slot_data["death_link"] == "toggle":
if self.ctx.death_link:
self.ctx.death_link = False
self.output(f"Death Link turned off")
else:
self.ctx.death_link = True
self.output(f"Death Link turned on")
else:
self.output(f"'death_link' is not set to 'toggle' for this seed.")
self.output(f"'death_link' = " + str(self.ctx.slot_data["death_link"]))
"""Toggles Deathlink"""
global death_link
if death_link:
death_link = False
self.output(f"Death Link turned off")
else:
self.output(f"No 'death_link' in slot_data keys. You probably aren't connected or are playing an older seed.")
death_link = True
self.output(f"Death Link turned on")
def _cmd_communication_path(self):
"""Opens a file browser to allow Linux users to manually set their %LOCALAPPDATA% path"""
directory = Utils.open_directory("Select %LOCALAPPDATA% dir", "~/.local/share/Steam/steamapps/compatdata/2552430/pfx/drive_c/users/steamuser/AppData/Local")
if directory:
directory += "/KH1FM"
if not os.path.exists(directory):
os.makedirs(directory)
self.ctx.game_communication_path = directory
def _cmd_goal(self):
"""Prints goal setting"""
if "goal" in self.ctx.slot_data.keys():
self.output(str(self.ctx.slot_data["goal"]))
else:
self.output(self.ctx.game_communication_path)
self.output("Unknown")
def _cmd_eotw_unlock(self):
"""Prints End of the World Unlock setting"""
if "required_reports_door" in self.ctx.slot_data.keys():
if self.ctx.slot_data["required_reports_door"] > 13:
self.output("Item")
else:
self.output(str(self.ctx.slot_data["required_reports_eotw"]) + " reports")
else:
self.output("Unknown")
def _cmd_door_unlock(self):
"""Prints Final Rest Door Unlock setting"""
if "door" in self.ctx.slot_data.keys():
if self.ctx.slot_data["door"] == "reports":
self.output(str(self.ctx.slot_data["required_reports_door"]) + " reports")
else:
self.output(str(self.ctx.slot_data["door"]))
else:
self.output("Unknown")
def _cmd_advanced_logic(self):
"""Prints advanced logic setting"""
if "advanced_logic" in self.ctx.slot_data.keys():
self.output(str(self.ctx.slot_data["advanced_logic"]))
else:
self.output("Unknown")
class KH1Context(CommonContext):
command_processor: int = KH1ClientCommandProcessor
game = "Kingdom Hearts"
items_handling = 0b011 # full remote except start inventory
items_handling = 0b111 # full remote
def __init__(self, server_address, password):
super(KH1Context, self).__init__(server_address, password)
self.send_index: int = 0
self.syncing = False
self.awaiting_bridge = False
self.hinted_location_ids: list[int] = []
self.slot_data: dict = {}
# Moved globals into instance attributes
self.death_link: bool = False
self.item_num: int = 1
self.remote_location_ids: list[int] = []
self.hinted_synth_location_ids = False
self.slot_data = {}
# 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%/KH1FM")
@@ -96,10 +103,6 @@ class KH1Context(CommonContext):
os.remove(root+"/"+file)
async def server_auth(self, password_requested: bool = False):
for root, dirs, files in os.walk(self.game_communication_path):
for file in files:
if file.find("obtain") <= -1:
os.remove(root+"/"+file)
if password_requested and not self.password:
await super(KH1Context, self).server_auth(password_requested)
await self.get_username()
@@ -111,7 +114,8 @@ class KH1Context(CommonContext):
for file in files:
if file.find("obtain") <= -1:
os.remove(root + "/" + file)
self.item_num = 1
global item_num
item_num = 1
@property
def endpoints(self):
@@ -126,7 +130,8 @@ class KH1Context(CommonContext):
for file in files:
if file.find("obtain") <= -1:
os.remove(root+"/"+file)
self.item_num = 1
global item_num
item_num = 1
def on_package(self, cmd: str, args: dict):
if cmd in {"Connected"}:
@@ -137,34 +142,38 @@ class KH1Context(CommonContext):
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
f.close()
# Handle Slot Data
#Handle Slot Data
self.slot_data = args['slot_data']
for key in list(args['slot_data'].keys()):
with open(os.path.join(self.game_communication_path, key + ".cfg"), 'w') as f:
f.write(str(args['slot_data'][key]))
f.close()
if key == "remote_location_ids":
self.remote_location_ids = args['slot_data'][key]
if key == "death_link":
if args['slot_data']["death_link"] != "off":
self.death_link = True
# End Handle Slot Data
###Support Legacy Games
if "Required Reports" in list(args['slot_data'].keys()) and "required_reports_eotw" not in list(args['slot_data'].keys()):
reports_required = args['slot_data']["Required Reports"]
with open(os.path.join(self.game_communication_path, "required_reports.cfg"), 'w') as f:
f.write(str(reports_required))
f.close()
###End Support Legacy Games
#End Handle Slot Data
if cmd in {"ReceivedItems"}:
start_index = args["index"]
if start_index != len(self.items_received):
global item_num
for item in args['items']:
found = False
item_filename = f"AP_{str(self.item_num)}.item"
item_filename = f"AP_{str(item_num)}.item"
for filename in os.listdir(self.game_communication_path):
if filename == item_filename:
found = True
if not found:
if (NetworkItem(*item).player == self.slot and (NetworkItem(*item).location in self.remote_location_ids) or (NetworkItem(*item).location < 0)) or NetworkItem(*item).player != self.slot:
with open(os.path.join(self.game_communication_path, item_filename), 'w') as f:
f.write(str(NetworkItem(*item).item) + "\n" + str(NetworkItem(*item).location) + "\n" + str(NetworkItem(*item).player))
f.close()
self.item_num += 1
with open(os.path.join(self.game_communication_path, item_filename), 'w') as f:
f.write(str(NetworkItem(*item).item) + "\n" + str(NetworkItem(*item).location) + "\n" + str(NetworkItem(*item).player))
f.close()
item_num = item_num + 1
if cmd in {"RoomUpdate"}:
if "checked_locations" in args:
@@ -177,39 +186,21 @@ class KH1Context(CommonContext):
if args["type"] == "ItemSend":
item = args["item"]
networkItem = NetworkItem(*item)
receiverID = args["receiving"]
recieverID = args["receiving"]
senderID = networkItem.player
locationID = networkItem.location
if receiverID == self.slot or senderID == self.slot:
itemName = self.item_names.lookup_in_slot(networkItem.item, receiverID)[:20]
if recieverID != self.slot and senderID == self.slot:
itemName = self.item_names.lookup_in_slot(networkItem.item, recieverID)
itemCategory = networkItem.flags
receiverName = self.player_names[receiverID][:20]
senderName = self.player_names[senderID][:20]
message = ""
if receiverID == self.slot and receiverID != senderID: # Item received from someone else
message = "From " + senderName + "\n" + itemName
elif senderID == self.slot and receiverID != senderID: # Item sent to someone else
message = itemName + "\nTo " + receiverName
elif locationID in self.remote_location_ids: # Found a remote item
message = itemName
filename = "msg"
if message != "":
if not os.path.exists(self.game_communication_path + "/" + filename):
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
f.write(message)
f.close()
if args["type"] == "ItemCheat":
item = args["item"]
networkItem = NetworkItem(*item)
receiverID = args["receiving"]
if receiverID == self.slot:
itemName = self.item_names.lookup_in_slot(networkItem.item, receiverID)[:20]
filename = "msg"
message = "Received " + itemName + "\nfrom server"
if not os.path.exists(self.game_communication_path + "/" + filename):
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
f.write(message)
f.close()
recieverName = self.player_names[recieverID]
filename = "sent"
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
f.write(
re.sub('[^A-Za-z0-9 ]+', '',str(itemName))[:15] + "\n"
+ re.sub('[^A-Za-z0-9 ]+', '',str(recieverName))[:6] + "\n"
+ str(itemCategory) + "\n"
+ str(locationID))
f.close()
def on_deathlink(self, data: dict[str, object]):
self.last_death_link = max(data["time"], self.last_death_link)
@@ -239,11 +230,12 @@ class KH1Context(CommonContext):
async def game_watcher(ctx: KH1Context):
from .Locations import lookup_id_to_name
while not ctx.exit_event.is_set():
if ctx.death_link and "DeathLink" not in ctx.tags:
await ctx.update_death_link(ctx.death_link)
if not ctx.death_link and "DeathLink" in ctx.tags:
await ctx.update_death_link(ctx.death_link)
if ctx.syncing is True:
global death_link
if death_link and "DeathLink" not in ctx.tags:
await ctx.update_death_link(death_link)
if not death_link and "DeathLink" in ctx.tags:
await ctx.update_death_link(death_link)
if ctx.syncing == True:
sync_msg = [{'cmd': 'Sync'}]
if ctx.locations_checked:
sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)})
@@ -264,17 +256,17 @@ async def game_watcher(ctx: KH1Context):
if st != "nil":
if timegm(time.strptime(st, '%Y%m%d%H%M%S')) > ctx.last_death_link and int(time.time()) % int(timegm(time.strptime(st, '%Y%m%d%H%M%S'))) < 10:
await ctx.send_death(death_text = "Sora was defeated!")
if file.find("hint") > -1:
hint_location_id = int(file.split("hint", -1)[1])
if hint_location_id not in ctx.hinted_location_ids:
if file.find("insynthshop") > -1:
if not ctx.hinted_synth_location_ids:
await ctx.send_msgs([{
"cmd": "LocationScouts",
"locations": [hint_location_id],
"locations": [2656401,2656402,2656403,2656404,2656405,2656406],
"create_as_hint": 2
}])
ctx.hinted_location_ids.append(hint_location_id)
ctx.hinted_synth_location_ids = True
ctx.locations_checked = sending
await ctx.check_locations(sending)
message = [{"cmd": 'LocationChecks', "locations": sending}]
await ctx.send_msgs(message)
if not ctx.finished_game and victory:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True

View File

@@ -1,202 +0,0 @@
VANILLA_KEYBLADE_STATS = [
{"STR": 3, "CRR": 20, "CRB": 0, "REC": 30, "MP": 0}, # Kingdom Key
{"STR": 1, "CRR": 20, "CRB": 0, "REC": 30, "MP": 0}, # Dream Sword
{"STR": 1, "CRR": 0, "CRB": 0, "REC": 60, "MP": 0}, # Dream Shield
{"STR": 1, "CRR": 10, "CRB": 0, "REC": 30, "MP": 0}, # Dream Rod
{"STR": 0, "CRR": 20, "CRB": 0, "REC": 30, "MP": 0}, # Wooden Sword
{"STR": 5, "CRR": 10, "CRB": 0, "REC": 1, "MP": 0}, # Jungle King
{"STR": 6, "CRR": 20, "CRB": 0, "REC": 60, "MP": 0}, # Three Wishes
{"STR": 8, "CRR": 10, "CRB": 2, "REC": 30, "MP": 1}, # Fairy Harp
{"STR": 7, "CRR": 40, "CRB": 0, "REC": 1, "MP": 0}, # Pumpkinhead
{"STR": 6, "CRR": 20, "CRB": 0, "REC": 30, "MP": 1}, # Crabclaw
{"STR": 13, "CRR": 40, "CRB": 0, "REC": 60, "MP": 0}, # Divine Rose
{"STR": 4, "CRR": 20, "CRB": 0, "REC": 30, "MP": 2}, # Spellbinder
{"STR": 10, "CRR": 20, "CRB": 2, "REC": 90, "MP": 0}, # Olympia
{"STR": 10, "CRR": 20, "CRB": 0, "REC": 30, "MP": 1}, # Lionheart
{"STR": 9, "CRR": 2, "CRB": 0, "REC": 90, "MP": -1}, # Metal Chocobo
{"STR": 9, "CRR": 40, "CRB": 0, "REC": 1, "MP": 1}, # Oathkeeper
{"STR": 11, "CRR": 20, "CRB": 2, "REC": 30, "MP": -1}, # Oblivion
{"STR": 7, "CRR": 20, "CRB": 0, "REC": 1, "MP": 2}, # Lady Luck
{"STR": 5, "CRR": 200, "CRB": 2, "REC": 1, "MP": 0}, # Wishing Star
{"STR": 14, "CRR": 40, "CRB": 2, "REC": 90, "MP": 2}, # Ultima Weapon
{"STR": 3, "CRR": 20, "CRB": 0, "REC": 1, "MP": 3}, # Diamond Dust
{"STR": 8, "CRR": 10, "CRB": 16, "REC": 90, "MP": -2}, # One-Winged Angel
]
VANILLA_PUPPY_LOCATIONS = [
"Traverse Town Mystical House Glide Chest",
"Traverse Town Alleyway Behind Crates Chest",
"Traverse Town Item Workshop Left Chest",
"Traverse Town Secret Waterway Near Stairs Chest",
"Wonderland Queen's Castle Hedge Right Blue Chest",
"Wonderland Lotus Forest Nut Chest",
"Wonderland Tea Party Garden Above Lotus Forest Entrance 1st Chest",
"Olympus Coliseum Coliseum Gates Right Blue Trinity Chest",
"Deep Jungle Hippo's Lagoon Center Chest",
"Deep Jungle Vines 2 Chest",
"Deep Jungle Waterfall Cavern Middle Chest",
"Deep Jungle Camp Blue Trinity Chest",
"Agrabah Cave of Wonders Treasure Room Across Platforms Chest",
"Halloween Town Oogie's Manor Hollow Chest",
"Neverland Pirate Ship Deck White Trinity Chest",
"Agrabah Cave of Wonders Hidden Room Left Chest",
"Agrabah Cave of Wonders Entrance Tall Tower Chest",
"Agrabah Palace Gates High Opposite Palace Chest",
"Monstro Chamber 3 Platform Above Chamber 2 Entrance Chest",
"Wonderland Lotus Forest Through the Painting Thunder Plant Chest",
"Hollow Bastion Grand Hall Left of Gate Chest",
"Halloween Town Cemetery By Cat Shape Chest",
"Halloween Town Moonlight Hill White Trinity Chest",
"Halloween Town Guillotine Square Pumpkin Structure Right Chest",
"Monstro Mouth High Platform Across from Boat Chest",
"Monstro Chamber 6 Low Chest",
"Monstro Chamber 5 Atop Barrel Chest",
"Neverland Hold Flight 1st Chest",
"Neverland Hold Yellow Trinity Green Chest",
"Neverland Captain's Cabin Chest",
"Hollow Bastion Rising Falls Floating Platform Near Save Chest",
"Hollow Bastion Castle Gates Gravity Chest",
"Hollow Bastion Lift Stop Outside Library Gravity Chest"
]
CHAR_TO_KH = {
" ": 0x01,
"0": 0x21,
"1": 0x22,
"2": 0x23,
"3": 0x24,
"4": 0x25,
"5": 0x26,
"6": 0x27,
"7": 0x28,
"8": 0x29,
"9": 0x2A,
"A": 0x2B,
"B": 0x2C,
"C": 0x2D,
"D": 0x2E,
"E": 0x2F,
"F": 0x30,
"G": 0x31,
"H": 0x32,
"I": 0x33,
"J": 0x34,
"K": 0x35,
"L": 0x36,
"M": 0x37,
"N": 0x38,
"O": 0x39,
"P": 0x3A,
"Q": 0x3B,
"R": 0x3C,
"S": 0x3D,
"T": 0x3E,
"U": 0x3F,
"V": 0x40,
"W": 0x41,
"X": 0x42,
"Y": 0x43,
"Z": 0x44,
"a": 0x45,
"b": 0x46,
"c": 0x47,
"d": 0x48,
"e": 0x49,
"f": 0x4A,
"g": 0x4B,
"h": 0x4C,
"i": 0x4D,
"j": 0x4E,
"k": 0x4F,
"l": 0x50,
"m": 0x51,
"n": 0x52,
"o": 0x53,
"p": 0x54,
"q": 0x55,
"r": 0x56,
"s": 0x57,
"t": 0x58,
"u": 0x59,
"v": 0x5A,
"w": 0x5B,
"x": 0x5C,
"y": 0x5D,
"z": 0x5E
}
VANILLA_ABILITY_AP_COSTS = [
{"Ability Name": "Treasure Magnet", "AP Cost": 2, "Randomize": True},
{"Ability Name": "Combo Plus", "AP Cost": 1, "Randomize": True},
{"Ability Name": "Air Combo Plus", "AP Cost": 1, "Randomize": True},
{"Ability Name": "Critical Plus", "AP Cost": 2, "Randomize": True},
{"Ability Name": "Second Wind", "AP Cost": 3, "Randomize": True},
{"Ability Name": "Scan", "AP Cost": 1, "Randomize": True},
{"Ability Name": "Sonic Blade", "AP Cost": 3, "Randomize": True},
{"Ability Name": "Ars Arcanum", "AP Cost": 4, "Randomize": True},
{"Ability Name": "Strike Raid", "AP Cost": 3, "Randomize": True},
{"Ability Name": "Ragnarok", "AP Cost": 4, "Randomize": True},
{"Ability Name": "Trinity Limit", "AP Cost": 1, "Randomize": True},
{"Ability Name": "Cheer", "AP Cost": 1, "Randomize": True},
{"Ability Name": "Vortex", "AP Cost": 1, "Randomize": True},
{"Ability Name": "Aerial Sweep", "AP Cost": 2, "Randomize": True},
{"Ability Name": "Counter Attack", "AP Cost": 2, "Randomize": True},
{"Ability Name": "Blitz", "AP Cost": 2, "Randomize": True},
{"Ability Name": "Guard", "AP Cost": 2, "Randomize": True},
{"Ability Name": "Dodge Roll", "AP Cost": 1, "Randomize": True},
{"Ability Name": "MP Haste", "AP Cost": 3, "Randomize": True},
{"Ability Name": "MP Rage", "AP Cost": 3, "Randomize": True},
{"Ability Name": "Second Chance", "AP Cost": 5, "Randomize": True},
{"Ability Name": "Berserk", "AP Cost": 1, "Randomize": True},
{"Ability Name": "Jackpot", "AP Cost": 2, "Randomize": True},
{"Ability Name": "Lucky Strike", "AP Cost": 2, "Randomize": True},
{"Ability Name": "Charge", "AP Cost": 1, "Randomize": True},
{"Ability Name": "Rocket", "AP Cost": 1, "Randomize": True},
{"Ability Name": "Tornado", "AP Cost": 2, "Randomize": True},
{"Ability Name": "MP Gift", "AP Cost": 3, "Randomize": True},
{"Ability Name": "Raging Boar", "AP Cost": 2, "Randomize": True},
{"Ability Name": "Asp's Bite", "AP Cost": 2, "Randomize": True},
{"Ability Name": "Healing Herb", "AP Cost": 3, "Randomize": True},
{"Ability Name": "Wind Armor", "AP Cost": 3, "Randomize": True},
{"Ability Name": "Crescent", "AP Cost": 2, "Randomize": True},
{"Ability Name": "Sandstorm", "AP Cost": 2, "Randomize": True},
{"Ability Name": "Applause!", "AP Cost": 1, "Randomize": True},
{"Ability Name": "Blazing Fury", "AP Cost": 2, "Randomize": True},
{"Ability Name": "Icy Terror", "AP Cost": 2, "Randomize": True},
{"Ability Name": "Bolts of Sorrow", "AP Cost": 3, "Randomize": True},
{"Ability Name": "Ghostly Scream", "AP Cost": 3, "Randomize": True},
{"Ability Name": "Hummingbird", "AP Cost": 2, "Randomize": True},
{"Ability Name": "Time-Out", "AP Cost": 4, "Randomize": True},
{"Ability Name": "Storm´s Eye", "AP Cost": 3, "Randomize": True},
{"Ability Name": "Ferocious Lunge", "AP Cost": 2, "Randomize": True},
{"Ability Name": "Furious Bellow", "AP Cost": 3, "Randomize": True},
{"Ability Name": "Spiral Wave", "AP Cost": 1, "Randomize": True},
{"Ability Name": "Thunder Potion", "AP Cost": 3, "Randomize": True},
{"Ability Name": "Cure Potion", "AP Cost": 3, "Randomize": True},
{"Ability Name": "Aero Potion", "AP Cost": 3, "Randomize": True},
{"Ability Name": "Slapshot", "AP Cost": 1, "Randomize": True},
{"Ability Name": "Sliding Dash", "AP Cost": 1, "Randomize": True},
{"Ability Name": "Hurricane Blast", "AP Cost": 2, "Randomize": True},
{"Ability Name": "Ripple Drive", "AP Cost": 3, "Randomize": True},
{"Ability Name": "Stun Impact", "AP Cost": 2, "Randomize": True},
{"Ability Name": "Gravity Break", "AP Cost": 2, "Randomize": True},
{"Ability Name": "Zantetsuken", "AP Cost": 2, "Randomize": True},
{"Ability Name": "Tech Boost", "AP Cost": 2, "Randomize": True},
{"Ability Name": "Encounter Plus", "AP Cost": 1, "Randomize": True},
{"Ability Name": "Leaf Bracer", "AP Cost": 5, "Randomize": True},
{"Ability Name": "Evolution", "AP Cost": 3, "Randomize": True},
{"Ability Name": "EXP Zero", "AP Cost": 0, "Randomize": True},
{"Ability Name": "Combo Master", "AP Cost": 3, "Randomize": True}
]
WORLD_KEY_ITEMS = {
"Footprints": "Wonderland",
"Entry Pass": "Olympus Coliseum",
"Slides": "Deep Jungle",
"Crystal Trident": "Atlantica",
"Forget-Me-Not": "Halloween Town",
"Jack-In-The-Box": "Halloween Town",
"Theon Vol. 6": "Hollow Bastion"
}
LOGIC_BEGINNER = 0
LOGIC_NORMAL = 5
LOGIC_PROUD = 10
LOGIC_MINIMAL = 15

View File

@@ -1,67 +0,0 @@
import logging
import yaml
import os
import io
from typing import TYPE_CHECKING, Dict, List, Optional, cast
import Utils
import zipfile
import json
from .Locations import KH1Location, location_table
from worlds.Files import APPlayerContainer
class KH1Container(APPlayerContainer):
game: str = 'Kingdom Hearts'
patch_file_ending = ".zip"
def __init__(self, patch_data: Dict[str, str] | io.BytesIO, base_path: str = "", output_directory: str = "",
player: Optional[int] = None, player_name: str = "", server: str = ""):
self.patch_data = patch_data
self.file_path = base_path
container_path = os.path.join(output_directory, base_path + ".zip")
super().__init__(container_path, player, player_name, server)
def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None:
for filename, text in self.patch_data.items():
opened_zipfile.writestr(filename, text)
super().write_contents(opened_zipfile)
def generate_json(world, output_directory):
mod_name = f"AP-{world.multiworld.seed_name}-P{world.player}-{world.multiworld.get_file_safe_player_name(world.player)}"
mod_dir = os.path.join(output_directory, mod_name + "_" + Utils.__version__)
item_location_map = get_item_location_map(world)
settings = get_settings(world)
files = {
"item_location_map.json": json.dumps(item_location_map),
"keyblade_stats.json": json.dumps(world.get_keyblade_stats()),
"settings.json": json.dumps(settings),
"ap_costs.json": json.dumps(world.get_ap_costs())
}
mod = KH1Container(files, mod_dir, output_directory, world.player,
world.multiworld.get_file_safe_player_name(world.player))
mod.write()
def get_item_location_map(world):
location_item_map = {}
for location in world.multiworld.get_filled_locations(world.player):
if location.name != "Final Ansem":
if world.player != location.item.player or (world.player == location.item.player and world.options.remote_items.current_key == "full" and (location_table[location.name].code < 2656800 or location_table[location.name].code > 2656814)):
item_id = 2641230
else:
item_id = location.item.code
location_data = location_table[location.name]
location_id = location_data.code
location_item_map[location_id] = item_id
return location_item_map
def get_settings(world):
settings = world.fill_slot_data()
return settings

View File

@@ -10,341 +10,518 @@ class KH1Item(Item):
class KH1ItemData(NamedTuple):
category: str
code: int
type: str
classification: ItemClassification = ItemClassification.filler
max_quantity: int = 1
weight: int = 1
def get_items_by_category(category: str) -> Dict[str, KH1ItemData]:
return {name: data for name, data in item_table.items() if data.category == category}
item_dict: Dict[str, KH1ItemData] = {}
for name, data in item_table.items():
if data.category == category:
item_dict.setdefault(name, data)
return item_dict
item_table: Dict[str, KH1ItemData] = {
"Potion": KH1ItemData("Item", code = 264_1001, classification = ItemClassification.filler, type = "Item", ),
"Hi-Potion": KH1ItemData("Item", code = 264_1002, classification = ItemClassification.filler, type = "Item", ),
"Ether": KH1ItemData("Item", code = 264_1003, classification = ItemClassification.filler, type = "Item", ),
"Elixir": KH1ItemData("Item", code = 264_1004, classification = ItemClassification.filler, type = "Item", ),
#"B05": KH1ItemData("Item", code = 264_1005, classification = ItemClassification.filler, type = "Item", ),
"Mega-Potion": KH1ItemData("Item", code = 264_1006, classification = ItemClassification.filler, type = "Item", ),
"Mega-Ether": KH1ItemData("Item", code = 264_1007, classification = ItemClassification.filler, type = "Item", ),
"Megalixir": KH1ItemData("Item", code = 264_1008, classification = ItemClassification.filler, type = "Item", ),
"Torn Page": KH1ItemData("Torn Pages", code = 264_1009, classification = ItemClassification.progression, type = "Item", max_quantity = 5 ),
"Final Door Key": KH1ItemData("Key", code = 264_1010, classification = ItemClassification.progression, type = "Item", ),
"Destiny Islands": KH1ItemData("Worlds", code = 264_1011, classification = ItemClassification.progression, type = "Item", ),
"Raft Materials": KH1ItemData("Key", code = 264_1012, classification = ItemClassification.progression, type = "Item", max_quantity = 2 ),
#"Frost Stone": KH1ItemData("Synthesis", code = 264_1013, classification = ItemClassification.filler, type = "Item", ),
#"Lightning Stone": KH1ItemData("Synthesis", code = 264_1014, classification = ItemClassification.filler, type = "Item", ),
#"Dazzling Stone": KH1ItemData("Synthesis", code = 264_1015, classification = ItemClassification.filler, type = "Item", ),
#"Stormy Stone": KH1ItemData("Synthesis", code = 264_1016, classification = ItemClassification.filler, type = "Item", ),
"Protect Chain": KH1ItemData("Accessory", code = 264_1017, classification = ItemClassification.useful, type = "Item", ),
"Protera Chain": KH1ItemData("Accessory", code = 264_1018, classification = ItemClassification.useful, type = "Item", ),
"Protega Chain": KH1ItemData("Accessory", code = 264_1019, classification = ItemClassification.useful, type = "Item", ),
"Fire Ring": KH1ItemData("Accessory", code = 264_1020, classification = ItemClassification.useful, type = "Item", ),
"Fira Ring": KH1ItemData("Accessory", code = 264_1021, classification = ItemClassification.useful, type = "Item", ),
"Firaga Ring": KH1ItemData("Accessory", code = 264_1022, classification = ItemClassification.useful, type = "Item", ),
"Blizzard Ring": KH1ItemData("Accessory", code = 264_1023, classification = ItemClassification.useful, type = "Item", ),
"Blizzara Ring": KH1ItemData("Accessory", code = 264_1024, classification = ItemClassification.useful, type = "Item", ),
"Blizzaga Ring": KH1ItemData("Accessory", code = 264_1025, classification = ItemClassification.useful, type = "Item", ),
"Thunder Ring": KH1ItemData("Accessory", code = 264_1026, classification = ItemClassification.useful, type = "Item", ),
"Thundara Ring": KH1ItemData("Accessory", code = 264_1027, classification = ItemClassification.useful, type = "Item", ),
"Thundaga Ring": KH1ItemData("Accessory", code = 264_1028, classification = ItemClassification.useful, type = "Item", ),
"Ability Stud": KH1ItemData("Accessory", code = 264_1029, classification = ItemClassification.useful, type = "Item", ),
"Guard Earring": KH1ItemData("Accessory", code = 264_1030, classification = ItemClassification.useful, type = "Item", ),
"Master Earring": KH1ItemData("Accessory", code = 264_1031, classification = ItemClassification.useful, type = "Item", ),
"Chaos Ring": KH1ItemData("Accessory", code = 264_1032, classification = ItemClassification.useful, type = "Item", ),
"Dark Ring": KH1ItemData("Accessory", code = 264_1033, classification = ItemClassification.useful, type = "Item", ),
"Element Ring": KH1ItemData("Accessory", code = 264_1034, classification = ItemClassification.useful, type = "Item", ),
"Three Stars": KH1ItemData("Accessory", code = 264_1035, classification = ItemClassification.useful, type = "Item", ),
"Power Chain": KH1ItemData("Accessory", code = 264_1036, classification = ItemClassification.useful, type = "Item", ),
"Golem Chain": KH1ItemData("Accessory", code = 264_1037, classification = ItemClassification.useful, type = "Item", ),
"Titan Chain": KH1ItemData("Accessory", code = 264_1038, classification = ItemClassification.useful, type = "Item", ),
"Energy Bangle": KH1ItemData("Accessory", code = 264_1039, classification = ItemClassification.useful, type = "Item", ),
"Angel Bangle": KH1ItemData("Accessory", code = 264_1040, classification = ItemClassification.useful, type = "Item", ),
"Gaia Bangle": KH1ItemData("Accessory", code = 264_1041, classification = ItemClassification.useful, type = "Item", ),
"Magic Armlet": KH1ItemData("Accessory", code = 264_1042, classification = ItemClassification.useful, type = "Item", ),
"Rune Armlet": KH1ItemData("Accessory", code = 264_1043, classification = ItemClassification.useful, type = "Item", ),
"Atlas Armlet": KH1ItemData("Accessory", code = 264_1044, classification = ItemClassification.useful, type = "Item", ),
"Heartguard": KH1ItemData("Accessory", code = 264_1045, classification = ItemClassification.useful, type = "Item", ),
"Ribbon": KH1ItemData("Accessory", code = 264_1046, classification = ItemClassification.useful, type = "Item", ),
"Crystal Crown": KH1ItemData("Accessory", code = 264_1047, classification = ItemClassification.useful, type = "Item", ),
"Brave Warrior": KH1ItemData("Accessory", code = 264_1048, classification = ItemClassification.useful, type = "Item", ),
"Ifrit's Horn": KH1ItemData("Accessory", code = 264_1049, classification = ItemClassification.useful, type = "Item", ),
"Inferno Band": KH1ItemData("Accessory", code = 264_1050, classification = ItemClassification.useful, type = "Item", ),
"White Fang": KH1ItemData("Accessory", code = 264_1051, classification = ItemClassification.useful, type = "Item", ),
"Ray of Light": KH1ItemData("Accessory", code = 264_1052, classification = ItemClassification.useful, type = "Item", ),
"Holy Circlet": KH1ItemData("Accessory", code = 264_1053, classification = ItemClassification.useful, type = "Item", ),
"Raven's Claw": KH1ItemData("Accessory", code = 264_1054, classification = ItemClassification.useful, type = "Item", ),
"Omega Arts": KH1ItemData("Accessory", code = 264_1055, classification = ItemClassification.useful, type = "Item", ),
"EXP Earring": KH1ItemData("Accessory", code = 264_1056, classification = ItemClassification.useful, type = "Item", ),
#"A41": KH1ItemData("Accessory", code = 264_1057, classification = ItemClassification.useful, type = "Item", ),
"EXP Ring": KH1ItemData("Accessory", code = 264_1058, classification = ItemClassification.useful, type = "Item", ),
"EXP Bracelet": KH1ItemData("Accessory", code = 264_1059, classification = ItemClassification.useful, type = "Item", ),
"EXP Necklace": KH1ItemData("Accessory", code = 264_1060, classification = ItemClassification.useful, type = "Item", ),
"Firagun Band": KH1ItemData("Accessory", code = 264_1061, classification = ItemClassification.useful, type = "Item", ),
"Blizzagun Band": KH1ItemData("Accessory", code = 264_1062, classification = ItemClassification.useful, type = "Item", ),
"Thundagun Band": KH1ItemData("Accessory", code = 264_1063, classification = ItemClassification.useful, type = "Item", ),
"Ifrit Belt": KH1ItemData("Accessory", code = 264_1064, classification = ItemClassification.useful, type = "Item", ),
"Shiva Belt": KH1ItemData("Accessory", code = 264_1065, classification = ItemClassification.useful, type = "Item", ),
"Ramuh Belt": KH1ItemData("Accessory", code = 264_1066, classification = ItemClassification.useful, type = "Item", ),
"Moogle Badge": KH1ItemData("Accessory", code = 264_1067, classification = ItemClassification.useful, type = "Item", ),
"Cosmic Arts": KH1ItemData("Accessory", code = 264_1068, classification = ItemClassification.useful, type = "Item", ),
"Royal Crown": KH1ItemData("Accessory", code = 264_1069, classification = ItemClassification.useful, type = "Item", ),
"Prime Cap": KH1ItemData("Accessory", code = 264_1070, classification = ItemClassification.useful, type = "Item", ),
"Obsidian Ring": KH1ItemData("Accessory", code = 264_1071, classification = ItemClassification.useful, type = "Item", ),
#"A56": KH1ItemData("Accessory", code = 264_1072, classification = ItemClassification.filler, type = "Item", ),
#"A57": KH1ItemData("Accessory", code = 264_1073, classification = ItemClassification.filler, type = "Item", ),
#"A58": KH1ItemData("Accessory", code = 264_1074, classification = ItemClassification.filler, type = "Item", ),
#"A59": KH1ItemData("Accessory", code = 264_1075, classification = ItemClassification.filler, type = "Item", ),
#"A60": KH1ItemData("Accessory", code = 264_1076, classification = ItemClassification.filler, type = "Item", ),
#"A61": KH1ItemData("Accessory", code = 264_1077, classification = ItemClassification.filler, type = "Item", ),
#"A62": KH1ItemData("Accessory", code = 264_1078, classification = ItemClassification.filler, type = "Item", ),
#"A63": KH1ItemData("Accessory", code = 264_1079, classification = ItemClassification.filler, type = "Item", ),
#"A64": KH1ItemData("Accessory", code = 264_1080, classification = ItemClassification.filler, type = "Item", ),
#"Kingdom Key": KH1ItemData("Keyblades", code = 264_1081, classification = ItemClassification.useful, type = "Item", ),
#"Dream Sword": KH1ItemData("Keyblades", code = 264_1082, classification = ItemClassification.useful, type = "Item", ),
#"Dream Shield": KH1ItemData("Keyblades", code = 264_1083, classification = ItemClassification.useful, type = "Item", ),
#"Dream Rod": KH1ItemData("Keyblades", code = 264_1084, classification = ItemClassification.useful, type = "Item", ),
#"Wooden Sword": KH1ItemData("Keyblades", code = 264_1085, classification = ItemClassification.useful, type = "Item", ),
"Jungle King": KH1ItemData("Keyblades", code = 264_1086, classification = ItemClassification.progression, type = "Item", ),
"Three Wishes": KH1ItemData("Keyblades", code = 264_1087, classification = ItemClassification.progression, type = "Item", ),
"Fairy Harp": KH1ItemData("Keyblades", code = 264_1088, classification = ItemClassification.progression, type = "Item", ),
"Pumpkinhead": KH1ItemData("Keyblades", code = 264_1089, classification = ItemClassification.progression, type = "Item", ),
"Crabclaw": KH1ItemData("Keyblades", code = 264_1090, classification = ItemClassification.progression, type = "Item", ),
"Divine Rose": KH1ItemData("Keyblades", code = 264_1091, classification = ItemClassification.progression, type = "Item", ),
"Spellbinder": KH1ItemData("Keyblades", code = 264_1092, classification = ItemClassification.progression, type = "Item", ),
"Olympia": KH1ItemData("Keyblades", code = 264_1093, classification = ItemClassification.progression, type = "Item", ),
"Lionheart": KH1ItemData("Keyblades", code = 264_1094, classification = ItemClassification.progression, type = "Item", ),
"Metal Chocobo": KH1ItemData("Keyblades", code = 264_1095, classification = ItemClassification.useful, type = "Item", ),
"Oathkeeper": KH1ItemData("Keyblades", code = 264_1096, classification = ItemClassification.progression, type = "Item", ),
"Oblivion": KH1ItemData("Keyblades", code = 264_1097, classification = ItemClassification.progression, type = "Item", ),
"Lady Luck": KH1ItemData("Keyblades", code = 264_1098, classification = ItemClassification.progression, type = "Item", ),
"Wishing Star": KH1ItemData("Keyblades", code = 264_1099, classification = ItemClassification.progression, type = "Item", ),
"Ultima Weapon": KH1ItemData("Keyblades", code = 264_1100, classification = ItemClassification.useful, type = "Item", ),
"Diamond Dust": KH1ItemData("Keyblades", code = 264_1101, classification = ItemClassification.useful, type = "Item", ),
"One-Winged Angel": KH1ItemData("Keyblades", code = 264_1102, classification = ItemClassification.useful, type = "Item", ),
#"Mage's Staff": KH1ItemData("Weapons", code = 264_1103, classification = ItemClassification.filler, type = "Item", ),
"Morning Star": KH1ItemData("Weapons", code = 264_1104, classification = ItemClassification.useful, type = "Item", ),
"Shooting Star": KH1ItemData("Weapons", code = 264_1105, classification = ItemClassification.useful, type = "Item", ),
"Magus Staff": KH1ItemData("Weapons", code = 264_1106, classification = ItemClassification.useful, type = "Item", ),
"Wisdom Staff": KH1ItemData("Weapons", code = 264_1107, classification = ItemClassification.useful, type = "Item", ),
"Warhammer": KH1ItemData("Weapons", code = 264_1108, classification = ItemClassification.useful, type = "Item", ),
"Silver Mallet": KH1ItemData("Weapons", code = 264_1109, classification = ItemClassification.useful, type = "Item", ),
"Grand Mallet": KH1ItemData("Weapons", code = 264_1110, classification = ItemClassification.useful, type = "Item", ),
"Lord Fortune": KH1ItemData("Weapons", code = 264_1111, classification = ItemClassification.useful, type = "Item", ),
"Violetta": KH1ItemData("Weapons", code = 264_1112, classification = ItemClassification.useful, type = "Item", ),
"Dream Rod (Donald)": KH1ItemData("Weapons", code = 264_1113, classification = ItemClassification.useful, type = "Item", ),
"Save the Queen": KH1ItemData("Weapons", code = 264_1114, classification = ItemClassification.useful, type = "Item", ),
"Wizard's Relic": KH1ItemData("Weapons", code = 264_1115, classification = ItemClassification.useful, type = "Item", ),
"Meteor Strike": KH1ItemData("Weapons", code = 264_1116, classification = ItemClassification.useful, type = "Item", ),
"Fantasista": KH1ItemData("Weapons", code = 264_1117, classification = ItemClassification.useful, type = "Item", ),
#"Unused (Donald)": KH1ItemData("Weapons", code = 264_1118, classification = ItemClassification.filler, type = "Item", ),
#"Knight's Shield": KH1ItemData("Weapons", code = 264_1119, classification = ItemClassification.filler, type = "Item", ),
"Mythril Shield": KH1ItemData("Weapons", code = 264_1120, classification = ItemClassification.useful, type = "Item", ),
"Onyx Shield": KH1ItemData("Weapons", code = 264_1121, classification = ItemClassification.useful, type = "Item", ),
"Stout Shield": KH1ItemData("Weapons", code = 264_1122, classification = ItemClassification.useful, type = "Item", ),
"Golem Shield": KH1ItemData("Weapons", code = 264_1123, classification = ItemClassification.useful, type = "Item", ),
"Adamant Shield": KH1ItemData("Weapons", code = 264_1124, classification = ItemClassification.useful, type = "Item", ),
"Smasher": KH1ItemData("Weapons", code = 264_1125, classification = ItemClassification.useful, type = "Item", ),
"Gigas Fist": KH1ItemData("Weapons", code = 264_1126, classification = ItemClassification.useful, type = "Item", ),
"Genji Shield": KH1ItemData("Weapons", code = 264_1127, classification = ItemClassification.useful, type = "Item", ),
"Herc's Shield": KH1ItemData("Weapons", code = 264_1128, classification = ItemClassification.useful, type = "Item", ),
"Dream Shield (Goofy)": KH1ItemData("Weapons", code = 264_1129, classification = ItemClassification.useful, type = "Item", ),
"Save the King": KH1ItemData("Weapons", code = 264_1130, classification = ItemClassification.useful, type = "Item", ),
"Defender": KH1ItemData("Weapons", code = 264_1131, classification = ItemClassification.useful, type = "Item", ),
"Mighty Shield": KH1ItemData("Weapons", code = 264_1132, classification = ItemClassification.useful, type = "Item", ),
"Seven Elements": KH1ItemData("Weapons", code = 264_1133, classification = ItemClassification.useful, type = "Item", ),
#"Unused (Goofy)": KH1ItemData("Weapons", code = 264_1134, classification = ItemClassification.filler, type = "Item", ),
#"Spear": KH1ItemData("Weapons", code = 264_1135, classification = ItemClassification.filler, type = "Item", ),
#"No Weapon": KH1ItemData("Weapons", code = 264_1136, classification = ItemClassification.filler, type = "Item", ),
#"Genie": KH1ItemData("Weapons", code = 264_1137, classification = ItemClassification.filler, type = "Item", ),
#"No Weapon": KH1ItemData("Weapons", code = 264_1138, classification = ItemClassification.filler, type = "Item", ),
#"No Weapon": KH1ItemData("Weapons", code = 264_1139, classification = ItemClassification.filler, type = "Item", ),
#"Dagger": KH1ItemData("Weapons", code = 264_1140, classification = ItemClassification.filler, type = "Item", ),
#"Claws": KH1ItemData("Weapons", code = 264_1141, classification = ItemClassification.filler, type = "Item", ),
"Tent": KH1ItemData("Camping", code = 264_1142, classification = ItemClassification.filler, type = "Item", ),
"Camping Set": KH1ItemData("Camping", code = 264_1143, classification = ItemClassification.filler, type = "Item", ),
"Cottage": KH1ItemData("Camping", code = 264_1144, classification = ItemClassification.filler, type = "Item", ),
#"C04": KH1ItemData("Camping", code = 264_1145, classification = ItemClassification.filler, type = "Item", ),
#"C05": KH1ItemData("Camping", code = 264_1146, classification = ItemClassification.filler, type = "Item", ),
#"C06": KH1ItemData("Camping", code = 264_1147, classification = ItemClassification.filler, type = "Item", ),
#"C07": KH1ItemData("Camping", code = 264_1148, classification = ItemClassification.filler, type = "Item", ),
"Wonderland": KH1ItemData("Worlds", code = 264_1149, classification = ItemClassification.progression, type = "Item", ),
"Olympus Coliseum": KH1ItemData("Worlds", code = 264_1150, classification = ItemClassification.progression, type = "Item", ),
"Deep Jungle": KH1ItemData("Worlds", code = 264_1151, classification = ItemClassification.progression, type = "Item", ),
"Power Up": KH1ItemData("Stat Ups", code = 264_1152, classification = ItemClassification.filler, type = "Item", ),
"Defense Up": KH1ItemData("Stat Ups", code = 264_1153, classification = ItemClassification.filler, type = "Item", ),
"AP Up": KH1ItemData("Stat Ups", code = 264_1154, classification = ItemClassification.filler, type = "Item", ),
"Agrabah": KH1ItemData("Worlds", code = 264_1155, classification = ItemClassification.progression, type = "Item", ),
"Monstro": KH1ItemData("Worlds", code = 264_1156, classification = ItemClassification.progression, type = "Item", ),
"Atlantica": KH1ItemData("Worlds", code = 264_1157, classification = ItemClassification.progression, type = "Item", ),
"Fire Arts": KH1ItemData("Key", code = 264_1158, classification = ItemClassification.progression, type = "Item", ),
"Blizzard Arts": KH1ItemData("Key", code = 264_1159, classification = ItemClassification.progression, type = "Item", ),
"Thunder Arts": KH1ItemData("Key", code = 264_1160, classification = ItemClassification.progression, type = "Item", ),
"Cure Arts": KH1ItemData("Key", code = 264_1161, classification = ItemClassification.progression, type = "Item", ),
"Gravity Arts": KH1ItemData("Key", code = 264_1162, classification = ItemClassification.progression, type = "Item", ),
"Stop Arts": KH1ItemData("Key", code = 264_1163, classification = ItemClassification.progression, type = "Item", ),
"Aero Arts": KH1ItemData("Key", code = 264_1164, classification = ItemClassification.progression, type = "Item", ),
"Neverland": KH1ItemData("Worlds", code = 264_1165, classification = ItemClassification.progression, type = "Item", ),
"Halloween Town": KH1ItemData("Worlds", code = 264_1166, classification = ItemClassification.progression, type = "Item", ),
"Puppy": KH1ItemData("Key", code = 264_1167, classification = ItemClassification.progression, type = "Item", ),
"Hollow Bastion": KH1ItemData("Worlds", code = 264_1168, classification = ItemClassification.progression, type = "Item", ),
"End of the World": KH1ItemData("Worlds", code = 264_1169, classification = ItemClassification.progression, type = "Item", ),
"Blue Trinity": KH1ItemData("Trinities", code = 264_1170, classification = ItemClassification.progression, type = "Item", ),
"Red Trinity": KH1ItemData("Trinities", code = 264_1171, classification = ItemClassification.progression, type = "Item", ),
"Green Trinity": KH1ItemData("Trinities", code = 264_1172, classification = ItemClassification.progression, type = "Item", ),
"Yellow Trinity": KH1ItemData("Trinities", code = 264_1173, classification = ItemClassification.progression, type = "Item", ),
"White Trinity": KH1ItemData("Trinities", code = 264_1174, classification = ItemClassification.progression, type = "Item", ),
"Progressive Fire": KH1ItemData("Magic", code = 264_1175, classification = ItemClassification.progression, type = "Item", max_quantity = 3 ),
"Progressive Blizzard": KH1ItemData("Magic", code = 264_1176, classification = ItemClassification.progression, type = "Item", max_quantity = 3 ),
"Progressive Thunder": KH1ItemData("Magic", code = 264_1177, classification = ItemClassification.progression, type = "Item", max_quantity = 3 ),
"Progressive Cure": KH1ItemData("Magic", code = 264_1178, classification = ItemClassification.progression, type = "Item", max_quantity = 3 ),
"Progressive Gravity": KH1ItemData("Magic", code = 264_1179, classification = ItemClassification.progression, type = "Item", max_quantity = 3 ),
"Progressive Stop": KH1ItemData("Magic", code = 264_1180, classification = ItemClassification.progression, type = "Item", max_quantity = 3 ),
"Progressive Aero": KH1ItemData("Magic", code = 264_1181, classification = ItemClassification.progression, type = "Item", max_quantity = 3 ),
"Phil Cup": KH1ItemData("Cups", code = 264_1182, classification = ItemClassification.progression, type = "Item", ),
"Theon Vol. 6": KH1ItemData("Key", code = 264_1183, classification = ItemClassification.progression, type = "Item", ),
"Pegasus Cup": KH1ItemData("Cups", code = 264_1184, classification = ItemClassification.progression, type = "Item", ),
"Hercules Cup": KH1ItemData("Cups", code = 264_1185, classification = ItemClassification.progression, type = "Item", ),
#"Empty Bottle": KH1ItemData("Key", code = 264_1186, classification = ItemClassification.progression, type = "Item", max_quantity = 6 ),
#"Old Book": KH1ItemData("Key", code = 264_1187, classification = ItemClassification.progression, type = "Item", ),
"Emblem Piece (Flame)": KH1ItemData("Key", code = 264_1188, classification = ItemClassification.progression, type = "Item", ),
"Emblem Piece (Chest)": KH1ItemData("Key", code = 264_1189, classification = ItemClassification.progression, type = "Item", ),
"Emblem Piece (Statue)": KH1ItemData("Key", code = 264_1190, classification = ItemClassification.progression, type = "Item", ),
"Emblem Piece (Fountain)": KH1ItemData("Key", code = 264_1191, classification = ItemClassification.progression, type = "Item", ),
#"Log": KH1ItemData("DI", code = 264_1192, classification = ItemClassification.progression, type = "Item", max_quantity = 2 ),
#"Cloth": KH1ItemData("DI", code = 264_1193, classification = ItemClassification.progression, type = "Item", ),
#"Rope": KH1ItemData("DI", code = 264_1194, classification = ItemClassification.progression, type = "Item", ),
#"Seagull Egg": KH1ItemData("DI", code = 264_1195, classification = ItemClassification.progression, type = "Item", ),
#"Fish": KH1ItemData("DI", code = 264_1196, classification = ItemClassification.progression, type = "Item", max_quantity = 3 ),
#"Mushroom": KH1ItemData("DI", code = 264_1197, classification = ItemClassification.progression, type = "Item", max_quantity = 3 ),
#"Coconut": KH1ItemData("DI", code = 264_1198, classification = ItemClassification.progression, type = "Item", max_quantity = 2 ),
#"Drinking Water": KH1ItemData("DI", code = 264_1199, classification = ItemClassification.progression, type = "Item", ),
#"Navi-G Piece 1": KH1ItemData("Key", code = 264_1200, classification = ItemClassification.progression, type = "Item", ),
#"Navi-G Piece 2": KH1ItemData("Key", code = 264_1201, classification = ItemClassification.progression, type = "Item", ),
#"Navi-Gummi Unused": KH1ItemData("Key", code = 264_1202, classification = ItemClassification.progression, type = "Item", ),
#"Navi-G Piece 3": KH1ItemData("Key", code = 264_1203, classification = ItemClassification.progression, type = "Item", ),
#"Navi-G Piece 4": KH1ItemData("Key", code = 264_1204, classification = ItemClassification.progression, type = "Item", ),
#"Navi-Gummi": KH1ItemData("Key", code = 264_1205, classification = ItemClassification.progression, type = "Item", ),
#"Watergleam": KH1ItemData("Key", code = 264_1206, classification = ItemClassification.progression, type = "Item", ),
#"Naturespark": KH1ItemData("Key", code = 264_1207, classification = ItemClassification.progression, type = "Item", ),
#"Fireglow": KH1ItemData("Key", code = 264_1208, classification = ItemClassification.progression, type = "Item", ),
#"Earthshine": KH1ItemData("Key", code = 264_1209, classification = ItemClassification.progression, type = "Item", ),
"Crystal Trident": KH1ItemData("Key", code = 264_1210, classification = ItemClassification.progression, type = "Item", ),
"Postcard": KH1ItemData("Key", code = 264_1211, classification = ItemClassification.progression, type = "Item", max_quantity = 10),
#"Torn Page 1": KH1ItemData("Torn Pages", code = 264_1212, classification = ItemClassification.progression, type = "Item", ),
#"Torn Page 2": KH1ItemData("Torn Pages", code = 264_1213, classification = ItemClassification.progression, type = "Item", ),
#"Torn Page 3": KH1ItemData("Torn Pages", code = 264_1214, classification = ItemClassification.progression, type = "Item", ),
#"Torn Page 4": KH1ItemData("Torn Pages", code = 264_1215, classification = ItemClassification.progression, type = "Item", ),
#"Torn Page 5": KH1ItemData("Torn Pages", code = 264_1216, classification = ItemClassification.progression, type = "Item", ),
"Slides": KH1ItemData("Key", code = 264_1217, classification = ItemClassification.progression, type = "Item", ),
#"Slide 2": KH1ItemData("Key", code = 264_1218, classification = ItemClassification.progression, type = "Item", ),
#"Slide 3": KH1ItemData("Key", code = 264_1219, classification = ItemClassification.progression, type = "Item", ),
#"Slide 4": KH1ItemData("Key", code = 264_1220, classification = ItemClassification.progression, type = "Item", ),
#"Slide 5": KH1ItemData("Key", code = 264_1221, classification = ItemClassification.progression, type = "Item", ),
#"Slide 6": KH1ItemData("Key", code = 264_1222, classification = ItemClassification.progression, type = "Item", ),
"Footprints": KH1ItemData("Key", code = 264_1223, classification = ItemClassification.progression, type = "Item", ),
#"Claw Marks": KH1ItemData("Key", code = 264_1224, classification = ItemClassification.progression, type = "Item", ),
#"Stench": KH1ItemData("Key", code = 264_1225, classification = ItemClassification.progression, type = "Item", ),
#"Antenna": KH1ItemData("Key", code = 264_1226, classification = ItemClassification.progression, type = "Item", ),
"Forget-Me-Not": KH1ItemData("Key", code = 264_1227, classification = ItemClassification.progression, type = "Item", ),
"Jack-In-The-Box": KH1ItemData("Key", code = 264_1228, classification = ItemClassification.progression, type = "Item", ),
"Entry Pass": KH1ItemData("Key", code = 264_1229, classification = ItemClassification.progression, type = "Item", ),
#"AP Item": KH1ItemData("Key", code = 264_1230, classification = ItemClassification.progression, type = "Item", ),
"Dumbo": KH1ItemData("Summons", code = 264_1231, classification = ItemClassification.progression, type = "Item", ),
#"N41": KH1ItemData("Synthesis", code = 264_1232, classification = ItemClassification.filler, type = "Item", ),
"Bambi": KH1ItemData("Summons", code = 264_1233, classification = ItemClassification.progression, type = "Item", ),
"Genie": KH1ItemData("Summons", code = 264_1234, classification = ItemClassification.progression, type = "Item", ),
"Tinker Bell": KH1ItemData("Summons", code = 264_1235, classification = ItemClassification.progression, type = "Item", ),
"Mushu": KH1ItemData("Summons", code = 264_1236, classification = ItemClassification.progression, type = "Item", ),
"Simba": KH1ItemData("Summons", code = 264_1237, classification = ItemClassification.progression, type = "Item", ),
"Lucky Emblem": KH1ItemData("Key", code = 264_1238, classification = ItemClassification.progression, type = "Item", ),
"Max HP Increase": KH1ItemData("Level Up", code = 264_1239, classification = ItemClassification.useful, type = "Item", max_quantity = 15),
"Max MP Increase": KH1ItemData("Level Up", code = 264_1240, classification = ItemClassification.useful, type = "Item", max_quantity = 15),
"Max AP Increase": KH1ItemData("Level Up", code = 264_1241, classification = ItemClassification.useful, type = "Item", max_quantity = 15),
"Strength Increase": KH1ItemData("Level Up", code = 264_1242, classification = ItemClassification.useful, type = "Item", max_quantity = 15),
"Defense Increase": KH1ItemData("Level Up", code = 264_1243, classification = ItemClassification.useful, type = "Item", max_quantity = 15),
"Item Slot Increase": KH1ItemData("Limited Level Up", code = 264_1244, classification = ItemClassification.useful, type = "Item", max_quantity = 15),
"Accessory Slot Increase": KH1ItemData("Limited Level Up", code = 264_1245, classification = ItemClassification.useful, type = "Item", max_quantity = 15),
#"Thunder Gem": KH1ItemData("Synthesis", code = 264_1246, classification = ItemClassification.filler, type = "Item", ),
#"Shiny Crystal": KH1ItemData("Synthesis", code = 264_1247, classification = ItemClassification.filler, type = "Item", ),
#"Bright Shard": KH1ItemData("Synthesis", code = 264_1248, classification = ItemClassification.filler, type = "Item", ),
#"Bright Gem": KH1ItemData("Synthesis", code = 264_1249, classification = ItemClassification.filler, type = "Item", ),
#"Bright Crystal": KH1ItemData("Synthesis", code = 264_1250, classification = ItemClassification.filler, type = "Item", ),
#"Mystery Goo": KH1ItemData("Synthesis", code = 264_1251, classification = ItemClassification.filler, type = "Item", ),
#"Gale": KH1ItemData("Synthesis", code = 264_1252, classification = ItemClassification.filler, type = "Item", ),
#"Mythril Shard": KH1ItemData("Synthesis", code = 264_1253, classification = ItemClassification.filler, type = "Item", ),
"Mythril": KH1ItemData("Key", code = 264_1254, classification = ItemClassification.progression, type = "Item", max_quantity = 16),
"Orichalcum": KH1ItemData("Key", code = 264_1255, classification = ItemClassification.progression, type = "Item", max_quantity = 17),
"High Jump": KH1ItemData("Shared Abilities", code = 264_2001, classification = ItemClassification.progression, type = "Shared Ability", ),
"Mermaid Kick": KH1ItemData("Shared Abilities", code = 264_2002, classification = ItemClassification.progression, type = "Shared Ability", ),
"Progressive Glide": KH1ItemData("Shared Abilities", code = 264_2003, classification = ItemClassification.progression, type = "Shared Ability", max_quantity = 2 ),
#"Superglide": KH1ItemData("Shared Abilities", code = 264_2004, classification = ItemClassification.progression, type = "Ability", ),
"Treasure Magnet": KH1ItemData("Abilities", code = 264_3005, classification = ItemClassification.useful, type = "Ability", max_quantity = 2 ),
"Combo Plus": KH1ItemData("Abilities", code = 264_3006, classification = ItemClassification.useful, type = "Ability", max_quantity = 4 ),
"Air Combo Plus": KH1ItemData("Abilities", code = 264_3007, classification = ItemClassification.progression, type = "Ability", max_quantity = 2 ),
"Critical Plus": KH1ItemData("Abilities", code = 264_3008, classification = ItemClassification.useful, type = "Ability", max_quantity = 3 ),
#"Second Wind": KH1ItemData("Abilities", code = 264_3009, classification = ItemClassification.useful, type = "Ability", ),
"Scan": KH1ItemData("Abilities", code = 264_3010, classification = ItemClassification.useful, type = "Ability", ),
"Sonic Blade": KH1ItemData("Abilities", code = 264_3011, classification = ItemClassification.progression, type = "Ability", ),
"Ars Arcanum": KH1ItemData("Abilities", code = 264_3012, classification = ItemClassification.useful, type = "Ability", ),
"Strike Raid": KH1ItemData("Abilities", code = 264_3013, classification = ItemClassification.progression, type = "Ability", ),
"Ragnarok": KH1ItemData("Abilities", code = 264_3014, classification = ItemClassification.useful, type = "Ability", ),
"Trinity Limit": KH1ItemData("Abilities", code = 264_3015, classification = ItemClassification.useful, type = "Ability", ),
"Cheer": KH1ItemData("Abilities", code = 264_3016, classification = ItemClassification.useful, type = "Ability", ),
"Vortex": KH1ItemData("Abilities", code = 264_3017, classification = ItemClassification.useful, type = "Ability", ),
"Aerial Sweep": KH1ItemData("Abilities", code = 264_3018, classification = ItemClassification.useful, type = "Ability", ),
"Counterattack": KH1ItemData("Abilities", code = 264_3019, classification = ItemClassification.progression, type = "Ability", ),
"Blitz": KH1ItemData("Abilities", code = 264_3020, classification = ItemClassification.useful, type = "Ability", ),
"Guard": KH1ItemData("Abilities", code = 264_3021, classification = ItemClassification.progression, type = "Ability", ),
"Dodge Roll": KH1ItemData("Abilities", code = 264_3022, classification = ItemClassification.progression, type = "Ability", ),
"MP Haste": KH1ItemData("Abilities", code = 264_3023, classification = ItemClassification.useful, type = "Ability", ),
"MP Rage": KH1ItemData("Abilities", code = 264_3024, classification = ItemClassification.progression, type = "Ability", ),
"Second Chance": KH1ItemData("Abilities", code = 264_3025, classification = ItemClassification.progression, type = "Ability", ),
"Berserk": KH1ItemData("Abilities", code = 264_3026, classification = ItemClassification.useful, type = "Ability", ),
"Jackpot": KH1ItemData("Abilities", code = 264_3027, classification = ItemClassification.useful, type = "Ability", ),
"Lucky Strike": KH1ItemData("Abilities", code = 264_3028, classification = ItemClassification.useful, type = "Ability", ),
#"Charge": KH1ItemData("Abilities", code = 264_3029, classification = ItemClassification.useful, type = "Ability", ),
#"Rocket": KH1ItemData("Abilities", code = 264_3030, classification = ItemClassification.useful, type = "Ability", ),
#"Tornado": KH1ItemData("Abilities", code = 264_3031, classification = ItemClassification.useful, type = "Ability", ),
#"MP Gift": KH1ItemData("Abilities", code = 264_3032, classification = ItemClassification.useful, type = "Ability", ),
#"Raging Boar": KH1ItemData("Abilities", code = 264_3033, classification = ItemClassification.useful, type = "Ability", ),
#"Asp's Bite": KH1ItemData("Abilities", code = 264_3034, classification = ItemClassification.useful, type = "Ability", ),
#"Healing Herb": KH1ItemData("Abilities", code = 264_3035, classification = ItemClassification.useful, type = "Ability", ),
#"Wind Armor": KH1ItemData("Abilities", code = 264_3036, classification = ItemClassification.useful, type = "Ability", ),
#"Crescent": KH1ItemData("Abilities", code = 264_3037, classification = ItemClassification.useful, type = "Ability", ),
#"Sandstorm": KH1ItemData("Abilities", code = 264_3038, classification = ItemClassification.useful, type = "Ability", ),
#"Applause!": KH1ItemData("Abilities", code = 264_3039, classification = ItemClassification.useful, type = "Ability", ),
#"Blazing Fury": KH1ItemData("Abilities", code = 264_3040, classification = ItemClassification.useful, type = "Ability", ),
#"Icy Terror": KH1ItemData("Abilities", code = 264_3041, classification = ItemClassification.useful, type = "Ability", ),
#"Bolts of Sorrow": KH1ItemData("Abilities", code = 264_3042, classification = ItemClassification.useful, type = "Ability", ),
#"Ghostly Scream": KH1ItemData("Abilities", code = 264_3043, classification = ItemClassification.useful, type = "Ability", ),
#"Humming Bird": KH1ItemData("Abilities", code = 264_3044, classification = ItemClassification.useful, type = "Ability", ),
#"Time-Out": KH1ItemData("Abilities", code = 264_3045, classification = ItemClassification.useful, type = "Ability", ),
#"Storm's Eye": KH1ItemData("Abilities", code = 264_3046, classification = ItemClassification.useful, type = "Ability", ),
#"Ferocious Lunge": KH1ItemData("Abilities", code = 264_3047, classification = ItemClassification.useful, type = "Ability", ),
#"Furious Bellow": KH1ItemData("Abilities", code = 264_3048, classification = ItemClassification.useful, type = "Ability", ),
#"Spiral Wave": KH1ItemData("Abilities", code = 264_3049, classification = ItemClassification.useful, type = "Ability", ),
#"Thunder Potion": KH1ItemData("Abilities", code = 264_3050, classification = ItemClassification.useful, type = "Ability", ),
#"Cure Potion": KH1ItemData("Abilities", code = 264_3051, classification = ItemClassification.useful, type = "Ability", ),
#"Aero Potion": KH1ItemData("Abilities", code = 264_3052, classification = ItemClassification.useful, type = "Ability", ),
"Slapshot": KH1ItemData("Abilities", code = 264_3053, classification = ItemClassification.useful, type = "Ability", ),
"Sliding Dash": KH1ItemData("Abilities", code = 264_3054, classification = ItemClassification.useful, type = "Ability", ),
"Hurricane Blast": KH1ItemData("Abilities", code = 264_3055, classification = ItemClassification.useful, type = "Ability", ),
"Ripple Drive": KH1ItemData("Abilities", code = 264_3056, classification = ItemClassification.useful, type = "Ability", ),
"Stun Impact": KH1ItemData("Abilities", code = 264_3057, classification = ItemClassification.useful, type = "Ability", ),
"Gravity Break": KH1ItemData("Abilities", code = 264_3058, classification = ItemClassification.useful, type = "Ability", ),
"Zantetsuken": KH1ItemData("Abilities", code = 264_3059, classification = ItemClassification.useful, type = "Ability", ),
"Tech Boost": KH1ItemData("Abilities", code = 264_3060, classification = ItemClassification.useful, type = "Ability", max_quantity = 4 ),
"Encounter Plus": KH1ItemData("Abilities", code = 264_3061, classification = ItemClassification.useful, type = "Ability", ),
"Leaf Bracer": KH1ItemData("Abilities", code = 264_3062, classification = ItemClassification.progression, type = "Ability", ),
#"Evolution": KH1ItemData("Abilities", code = 264_3063, classification = ItemClassification.useful, type = "Ability", ),
"EXP Zero": KH1ItemData("Abilities", code = 264_3064, classification = ItemClassification.useful, type = "Ability", ),
"Combo Master": KH1ItemData("Abilities", code = 264_3065, classification = ItemClassification.progression, type = "Ability", )
"Victory": KH1ItemData("VIC", code = 264_0000, classification = ItemClassification.progression, ),
"Potion": KH1ItemData("Item", code = 264_1001, classification = ItemClassification.filler, ),
"Hi-Potion": KH1ItemData("Item", code = 264_1002, classification = ItemClassification.filler, ),
"Ether": KH1ItemData("Item", code = 264_1003, classification = ItemClassification.filler, ),
"Elixir": KH1ItemData("Item", code = 264_1004, classification = ItemClassification.filler, ),
#"B05": KH1ItemData("Item", code = 264_1005, classification = ItemClassification.filler, ),
"Mega-Potion": KH1ItemData("Item", code = 264_1006, classification = ItemClassification.filler, ),
"Mega-Ether": KH1ItemData("Item", code = 264_1007, classification = ItemClassification.filler, ),
"Megalixir": KH1ItemData("Item", code = 264_1008, classification = ItemClassification.filler, ),
#"Fury Stone": KH1ItemData("Synthesis", code = 264_1009, classification = ItemClassification.filler, ),
#"Power Stone": KH1ItemData("Synthesis", code = 264_1010, classification = ItemClassification.filler, ),
#"Energy Stone": KH1ItemData("Synthesis", code = 264_1011, classification = ItemClassification.filler, ),
#"Blazing Stone": KH1ItemData("Synthesis", code = 264_1012, classification = ItemClassification.filler, ),
#"Frost Stone": KH1ItemData("Synthesis", code = 264_1013, classification = ItemClassification.filler, ),
#"Lightning Stone": KH1ItemData("Synthesis", code = 264_1014, classification = ItemClassification.filler, ),
#"Dazzling Stone": KH1ItemData("Synthesis", code = 264_1015, classification = ItemClassification.filler, ),
#"Stormy Stone": KH1ItemData("Synthesis", code = 264_1016, classification = ItemClassification.filler, ),
"Protect Chain": KH1ItemData("Accessory", code = 264_1017, classification = ItemClassification.useful, ),
"Protera Chain": KH1ItemData("Accessory", code = 264_1018, classification = ItemClassification.useful, ),
"Protega Chain": KH1ItemData("Accessory", code = 264_1019, classification = ItemClassification.useful, ),
"Fire Ring": KH1ItemData("Accessory", code = 264_1020, classification = ItemClassification.useful, ),
"Fira Ring": KH1ItemData("Accessory", code = 264_1021, classification = ItemClassification.useful, ),
"Firaga Ring": KH1ItemData("Accessory", code = 264_1022, classification = ItemClassification.useful, ),
"Blizzard Ring": KH1ItemData("Accessory", code = 264_1023, classification = ItemClassification.useful, ),
"Blizzara Ring": KH1ItemData("Accessory", code = 264_1024, classification = ItemClassification.useful, ),
"Blizzaga Ring": KH1ItemData("Accessory", code = 264_1025, classification = ItemClassification.useful, ),
"Thunder Ring": KH1ItemData("Accessory", code = 264_1026, classification = ItemClassification.useful, ),
"Thundara Ring": KH1ItemData("Accessory", code = 264_1027, classification = ItemClassification.useful, ),
"Thundaga Ring": KH1ItemData("Accessory", code = 264_1028, classification = ItemClassification.useful, ),
"Ability Stud": KH1ItemData("Accessory", code = 264_1029, classification = ItemClassification.useful, ),
"Guard Earring": KH1ItemData("Accessory", code = 264_1030, classification = ItemClassification.useful, ),
"Master Earring": KH1ItemData("Accessory", code = 264_1031, classification = ItemClassification.useful, ),
"Chaos Ring": KH1ItemData("Accessory", code = 264_1032, classification = ItemClassification.useful, ),
"Dark Ring": KH1ItemData("Accessory", code = 264_1033, classification = ItemClassification.useful, ),
"Element Ring": KH1ItemData("Accessory", code = 264_1034, classification = ItemClassification.useful, ),
"Three Stars": KH1ItemData("Accessory", code = 264_1035, classification = ItemClassification.useful, ),
"Power Chain": KH1ItemData("Accessory", code = 264_1036, classification = ItemClassification.useful, ),
"Golem Chain": KH1ItemData("Accessory", code = 264_1037, classification = ItemClassification.useful, ),
"Titan Chain": KH1ItemData("Accessory", code = 264_1038, classification = ItemClassification.useful, ),
"Energy Bangle": KH1ItemData("Accessory", code = 264_1039, classification = ItemClassification.useful, ),
"Angel Bangle": KH1ItemData("Accessory", code = 264_1040, classification = ItemClassification.useful, ),
"Gaia Bangle": KH1ItemData("Accessory", code = 264_1041, classification = ItemClassification.useful, ),
"Magic Armlet": KH1ItemData("Accessory", code = 264_1042, classification = ItemClassification.useful, ),
"Rune Armlet": KH1ItemData("Accessory", code = 264_1043, classification = ItemClassification.useful, ),
"Atlas Armlet": KH1ItemData("Accessory", code = 264_1044, classification = ItemClassification.useful, ),
"Heartguard": KH1ItemData("Accessory", code = 264_1045, classification = ItemClassification.useful, ),
"Ribbon": KH1ItemData("Accessory", code = 264_1046, classification = ItemClassification.useful, ),
"Crystal Crown": KH1ItemData("Accessory", code = 264_1047, classification = ItemClassification.useful, ),
"Brave Warrior": KH1ItemData("Accessory", code = 264_1048, classification = ItemClassification.useful, ),
"Ifrit's Horn": KH1ItemData("Accessory", code = 264_1049, classification = ItemClassification.useful, ),
"Inferno Band": KH1ItemData("Accessory", code = 264_1050, classification = ItemClassification.useful, ),
"White Fang": KH1ItemData("Accessory", code = 264_1051, classification = ItemClassification.useful, ),
"Ray of Light": KH1ItemData("Accessory", code = 264_1052, classification = ItemClassification.useful, ),
"Holy Circlet": KH1ItemData("Accessory", code = 264_1053, classification = ItemClassification.useful, ),
"Raven's Claw": KH1ItemData("Accessory", code = 264_1054, classification = ItemClassification.useful, ),
"Omega Arts": KH1ItemData("Accessory", code = 264_1055, classification = ItemClassification.useful, ),
"EXP Earring": KH1ItemData("Accessory", code = 264_1056, classification = ItemClassification.useful, ),
#"A41": KH1ItemData("Accessory", code = 264_1057, classification = ItemClassification.useful, ),
"EXP Ring": KH1ItemData("Accessory", code = 264_1058, classification = ItemClassification.useful, ),
"EXP Bracelet": KH1ItemData("Accessory", code = 264_1059, classification = ItemClassification.useful, ),
"EXP Necklace": KH1ItemData("Accessory", code = 264_1060, classification = ItemClassification.useful, ),
"Firagun Band": KH1ItemData("Accessory", code = 264_1061, classification = ItemClassification.useful, ),
"Blizzagun Band": KH1ItemData("Accessory", code = 264_1062, classification = ItemClassification.useful, ),
"Thundagun Band": KH1ItemData("Accessory", code = 264_1063, classification = ItemClassification.useful, ),
"Ifrit Belt": KH1ItemData("Accessory", code = 264_1064, classification = ItemClassification.useful, ),
"Shiva Belt": KH1ItemData("Accessory", code = 264_1065, classification = ItemClassification.useful, ),
"Ramuh Belt": KH1ItemData("Accessory", code = 264_1066, classification = ItemClassification.useful, ),
"Moogle Badge": KH1ItemData("Accessory", code = 264_1067, classification = ItemClassification.useful, ),
"Cosmic Arts": KH1ItemData("Accessory", code = 264_1068, classification = ItemClassification.useful, ),
"Royal Crown": KH1ItemData("Accessory", code = 264_1069, classification = ItemClassification.useful, ),
"Prime Cap": KH1ItemData("Accessory", code = 264_1070, classification = ItemClassification.useful, ),
"Obsidian Ring": KH1ItemData("Accessory", code = 264_1071, classification = ItemClassification.useful, ),
#"A56": KH1ItemData("Accessory", code = 264_1072, classification = ItemClassification.filler, ),
#"A57": KH1ItemData("Accessory", code = 264_1073, classification = ItemClassification.filler, ),
#"A58": KH1ItemData("Accessory", code = 264_1074, classification = ItemClassification.filler, ),
#"A59": KH1ItemData("Accessory", code = 264_1075, classification = ItemClassification.filler, ),
#"A60": KH1ItemData("Accessory", code = 264_1076, classification = ItemClassification.filler, ),
#"A61": KH1ItemData("Accessory", code = 264_1077, classification = ItemClassification.filler, ),
#"A62": KH1ItemData("Accessory", code = 264_1078, classification = ItemClassification.filler, ),
#"A63": KH1ItemData("Accessory", code = 264_1079, classification = ItemClassification.filler, ),
#"A64": KH1ItemData("Accessory", code = 264_1080, classification = ItemClassification.filler, ),
#"Kingdom Key": KH1ItemData("Keyblades", code = 264_1081, classification = ItemClassification.useful, ),
#"Dream Sword": KH1ItemData("Keyblades", code = 264_1082, classification = ItemClassification.useful, ),
#"Dream Shield": KH1ItemData("Keyblades", code = 264_1083, classification = ItemClassification.useful, ),
#"Dream Rod": KH1ItemData("Keyblades", code = 264_1084, classification = ItemClassification.useful, ),
"Wooden Sword": KH1ItemData("Keyblades", code = 264_1085, classification = ItemClassification.useful, ),
"Jungle King": KH1ItemData("Keyblades", code = 264_1086, classification = ItemClassification.progression, ),
"Three Wishes": KH1ItemData("Keyblades", code = 264_1087, classification = ItemClassification.progression, ),
"Fairy Harp": KH1ItemData("Keyblades", code = 264_1088, classification = ItemClassification.progression, ),
"Pumpkinhead": KH1ItemData("Keyblades", code = 264_1089, classification = ItemClassification.progression, ),
"Crabclaw": KH1ItemData("Keyblades", code = 264_1090, classification = ItemClassification.useful, ),
"Divine Rose": KH1ItemData("Keyblades", code = 264_1091, classification = ItemClassification.progression, ),
"Spellbinder": KH1ItemData("Keyblades", code = 264_1092, classification = ItemClassification.useful, ),
"Olympia": KH1ItemData("Keyblades", code = 264_1093, classification = ItemClassification.progression, ),
"Lionheart": KH1ItemData("Keyblades", code = 264_1094, classification = ItemClassification.progression, ),
"Metal Chocobo": KH1ItemData("Keyblades", code = 264_1095, classification = ItemClassification.useful, ),
"Oathkeeper": KH1ItemData("Keyblades", code = 264_1096, classification = ItemClassification.progression, ),
"Oblivion": KH1ItemData("Keyblades", code = 264_1097, classification = ItemClassification.progression, ),
"Lady Luck": KH1ItemData("Keyblades", code = 264_1098, classification = ItemClassification.progression, ),
"Wishing Star": KH1ItemData("Keyblades", code = 264_1099, classification = ItemClassification.progression, ),
"Ultima Weapon": KH1ItemData("Keyblades", code = 264_1100, classification = ItemClassification.useful, ),
"Diamond Dust": KH1ItemData("Keyblades", code = 264_1101, classification = ItemClassification.useful, ),
"One-Winged Angel": KH1ItemData("Keyblades", code = 264_1102, classification = ItemClassification.useful, ),
#"Mage's Staff": KH1ItemData("Weapons", code = 264_1103, classification = ItemClassification.filler, ),
"Morning Star": KH1ItemData("Weapons", code = 264_1104, classification = ItemClassification.useful, ),
"Shooting Star": KH1ItemData("Weapons", code = 264_1105, classification = ItemClassification.useful, ),
"Magus Staff": KH1ItemData("Weapons", code = 264_1106, classification = ItemClassification.useful, ),
"Wisdom Staff": KH1ItemData("Weapons", code = 264_1107, classification = ItemClassification.useful, ),
"Warhammer": KH1ItemData("Weapons", code = 264_1108, classification = ItemClassification.useful, ),
"Silver Mallet": KH1ItemData("Weapons", code = 264_1109, classification = ItemClassification.useful, ),
"Grand Mallet": KH1ItemData("Weapons", code = 264_1110, classification = ItemClassification.useful, ),
"Lord Fortune": KH1ItemData("Weapons", code = 264_1111, classification = ItemClassification.useful, ),
"Violetta": KH1ItemData("Weapons", code = 264_1112, classification = ItemClassification.useful, ),
"Dream Rod (Donald)": KH1ItemData("Weapons", code = 264_1113, classification = ItemClassification.useful, ),
"Save the Queen": KH1ItemData("Weapons", code = 264_1114, classification = ItemClassification.useful, ),
"Wizard's Relic": KH1ItemData("Weapons", code = 264_1115, classification = ItemClassification.useful, ),
"Meteor Strike": KH1ItemData("Weapons", code = 264_1116, classification = ItemClassification.useful, ),
"Fantasista": KH1ItemData("Weapons", code = 264_1117, classification = ItemClassification.useful, ),
#"Unused (Donald)": KH1ItemData("Weapons", code = 264_1118, classification = ItemClassification.filler, ),
#"Knight's Shield": KH1ItemData("Weapons", code = 264_1119, classification = ItemClassification.filler, ),
"Mythril Shield": KH1ItemData("Weapons", code = 264_1120, classification = ItemClassification.useful, ),
"Onyx Shield": KH1ItemData("Weapons", code = 264_1121, classification = ItemClassification.useful, ),
"Stout Shield": KH1ItemData("Weapons", code = 264_1122, classification = ItemClassification.useful, ),
"Golem Shield": KH1ItemData("Weapons", code = 264_1123, classification = ItemClassification.useful, ),
"Adamant Shield": KH1ItemData("Weapons", code = 264_1124, classification = ItemClassification.useful, ),
"Smasher": KH1ItemData("Weapons", code = 264_1125, classification = ItemClassification.useful, ),
"Gigas Fist": KH1ItemData("Weapons", code = 264_1126, classification = ItemClassification.useful, ),
"Genji Shield": KH1ItemData("Weapons", code = 264_1127, classification = ItemClassification.useful, ),
"Herc's Shield": KH1ItemData("Weapons", code = 264_1128, classification = ItemClassification.useful, ),
"Dream Shield (Goofy)": KH1ItemData("Weapons", code = 264_1129, classification = ItemClassification.useful, ),
"Save the King": KH1ItemData("Weapons", code = 264_1130, classification = ItemClassification.useful, ),
"Defender": KH1ItemData("Weapons", code = 264_1131, classification = ItemClassification.useful, ),
"Mighty Shield": KH1ItemData("Weapons", code = 264_1132, classification = ItemClassification.useful, ),
"Seven Elements": KH1ItemData("Weapons", code = 264_1133, classification = ItemClassification.useful, ),
#"Unused (Goofy)": KH1ItemData("Weapons", code = 264_1134, classification = ItemClassification.filler, ),
#"Spear": KH1ItemData("Weapons", code = 264_1135, classification = ItemClassification.filler, ),
#"No Weapon": KH1ItemData("Weapons", code = 264_1136, classification = ItemClassification.filler, ),
#"Genie": KH1ItemData("Weapons", code = 264_1137, classification = ItemClassification.filler, ),
#"No Weapon": KH1ItemData("Weapons", code = 264_1138, classification = ItemClassification.filler, ),
#"No Weapon": KH1ItemData("Weapons", code = 264_1139, classification = ItemClassification.filler, ),
#"Tinker Bell": KH1ItemData("Weapons", code = 264_1140, classification = ItemClassification.filler, ),
#"Claws": KH1ItemData("Weapons", code = 264_1141, classification = ItemClassification.filler, ),
"Tent": KH1ItemData("Camping", code = 264_1142, classification = ItemClassification.filler, ),
"Camping Set": KH1ItemData("Camping", code = 264_1143, classification = ItemClassification.filler, ),
"Cottage": KH1ItemData("Camping", code = 264_1144, classification = ItemClassification.filler, ),
#"C04": KH1ItemData("Camping", code = 264_1145, classification = ItemClassification.filler, ),
#"C05": KH1ItemData("Camping", code = 264_1146, classification = ItemClassification.filler, ),
#"C06": KH1ItemData("Camping", code = 264_1147, classification = ItemClassification.filler, ),
#"C07": KH1ItemData("Camping", code = 264_1148, classification = ItemClassification.filler, ),
"Ansem's Report 11": KH1ItemData("Reports", code = 264_1149, classification = ItemClassification.progression, ),
"Ansem's Report 12": KH1ItemData("Reports", code = 264_1150, classification = ItemClassification.progression, ),
"Ansem's Report 13": KH1ItemData("Reports", code = 264_1151, classification = ItemClassification.progression, ),
"Power Up": KH1ItemData("Stat Ups", code = 264_1152, classification = ItemClassification.filler, ),
"Defense Up": KH1ItemData("Stat Ups", code = 264_1153, classification = ItemClassification.filler, ),
"AP Up": KH1ItemData("Stat Ups", code = 264_1154, classification = ItemClassification.filler, ),
#"Serenity Power": KH1ItemData("Synthesis", code = 264_1155, classification = ItemClassification.filler, ),
#"Dark Matter": KH1ItemData("Synthesis", code = 264_1156, classification = ItemClassification.filler, ),
#"Mythril Stone": KH1ItemData("Synthesis", code = 264_1157, classification = ItemClassification.filler, ),
"Fire Arts": KH1ItemData("Key", code = 264_1158, classification = ItemClassification.progression, ),
"Blizzard Arts": KH1ItemData("Key", code = 264_1159, classification = ItemClassification.progression, ),
"Thunder Arts": KH1ItemData("Key", code = 264_1160, classification = ItemClassification.progression, ),
"Cure Arts": KH1ItemData("Key", code = 264_1161, classification = ItemClassification.progression, ),
"Gravity Arts": KH1ItemData("Key", code = 264_1162, classification = ItemClassification.progression, ),
"Stop Arts": KH1ItemData("Key", code = 264_1163, classification = ItemClassification.progression, ),
"Aero Arts": KH1ItemData("Key", code = 264_1164, classification = ItemClassification.progression, ),
#"Shiitank Rank": KH1ItemData("Synthesis", code = 264_1165, classification = ItemClassification.filler, ),
#"Matsutake Rank": KH1ItemData("Synthesis", code = 264_1166, classification = ItemClassification.filler, ),
#"Mystery Mold": KH1ItemData("Synthesis", code = 264_1167, classification = ItemClassification.filler, ),
"Ansem's Report 1": KH1ItemData("Reports", code = 264_1168, classification = ItemClassification.progression, ),
"Ansem's Report 2": KH1ItemData("Reports", code = 264_1169, classification = ItemClassification.progression, ),
"Ansem's Report 3": KH1ItemData("Reports", code = 264_1170, classification = ItemClassification.progression, ),
"Ansem's Report 4": KH1ItemData("Reports", code = 264_1171, classification = ItemClassification.progression, ),
"Ansem's Report 5": KH1ItemData("Reports", code = 264_1172, classification = ItemClassification.progression, ),
"Ansem's Report 6": KH1ItemData("Reports", code = 264_1173, classification = ItemClassification.progression, ),
"Ansem's Report 7": KH1ItemData("Reports", code = 264_1174, classification = ItemClassification.progression, ),
"Ansem's Report 8": KH1ItemData("Reports", code = 264_1175, classification = ItemClassification.progression, ),
"Ansem's Report 9": KH1ItemData("Reports", code = 264_1176, classification = ItemClassification.progression, ),
"Ansem's Report 10": KH1ItemData("Reports", code = 264_1177, classification = ItemClassification.progression, ),
#"Khama Vol. 8": KH1ItemData("Key", code = 264_1178, classification = ItemClassification.progression, ),
#"Salegg Vol. 6": KH1ItemData("Key", code = 264_1179, classification = ItemClassification.progression, ),
#"Azal Vol. 3": KH1ItemData("Key", code = 264_1180, classification = ItemClassification.progression, ),
#"Mava Vol. 3": KH1ItemData("Key", code = 264_1181, classification = ItemClassification.progression, ),
#"Mava Vol. 6": KH1ItemData("Key", code = 264_1182, classification = ItemClassification.progression, ),
"Theon Vol. 6": KH1ItemData("Key", code = 264_1183, classification = ItemClassification.progression, ),
#"Nahara Vol. 5": KH1ItemData("Key", code = 264_1184, classification = ItemClassification.progression, ),
#"Hafet Vol. 4": KH1ItemData("Key", code = 264_1185, classification = ItemClassification.progression, ),
"Empty Bottle": KH1ItemData("Key", code = 264_1186, classification = ItemClassification.progression, max_quantity = 6 ),
#"Old Book": KH1ItemData("Key", code = 264_1187, classification = ItemClassification.progression, ),
"Emblem Piece (Flame)": KH1ItemData("Key", code = 264_1188, classification = ItemClassification.progression, ),
"Emblem Piece (Chest)": KH1ItemData("Key", code = 264_1189, classification = ItemClassification.progression, ),
"Emblem Piece (Statue)": KH1ItemData("Key", code = 264_1190, classification = ItemClassification.progression, ),
"Emblem Piece (Fountain)": KH1ItemData("Key", code = 264_1191, classification = ItemClassification.progression, ),
#"Log": KH1ItemData("Key", code = 264_1192, classification = ItemClassification.progression, ),
#"Cloth": KH1ItemData("Key", code = 264_1193, classification = ItemClassification.progression, ),
#"Rope": KH1ItemData("Key", code = 264_1194, classification = ItemClassification.progression, ),
#"Seagull Egg": KH1ItemData("Key", code = 264_1195, classification = ItemClassification.progression, ),
#"Fish": KH1ItemData("Key", code = 264_1196, classification = ItemClassification.progression, ),
#"Mushroom": KH1ItemData("Key", code = 264_1197, classification = ItemClassification.progression, ),
#"Coconut": KH1ItemData("Key", code = 264_1198, classification = ItemClassification.progression, ),
#"Drinking Water": KH1ItemData("Key", code = 264_1199, classification = ItemClassification.progression, ),
#"Navi-G Piece 1": KH1ItemData("Key", code = 264_1200, classification = ItemClassification.progression, ),
#"Navi-G Piece 2": KH1ItemData("Key", code = 264_1201, classification = ItemClassification.progression, ),
#"Navi-Gummi Unused": KH1ItemData("Key", code = 264_1202, classification = ItemClassification.progression, ),
#"Navi-G Piece 3": KH1ItemData("Key", code = 264_1203, classification = ItemClassification.progression, ),
#"Navi-G Piece 4": KH1ItemData("Key", code = 264_1204, classification = ItemClassification.progression, ),
#"Navi-Gummi": KH1ItemData("Key", code = 264_1205, classification = ItemClassification.progression, ),
#"Watergleam": KH1ItemData("Key", code = 264_1206, classification = ItemClassification.progression, ),
#"Naturespark": KH1ItemData("Key", code = 264_1207, classification = ItemClassification.progression, ),
#"Fireglow": KH1ItemData("Key", code = 264_1208, classification = ItemClassification.progression, ),
#"Earthshine": KH1ItemData("Key", code = 264_1209, classification = ItemClassification.progression, ),
"Crystal Trident": KH1ItemData("Key", code = 264_1210, classification = ItemClassification.progression, ),
"Postcard": KH1ItemData("Key", code = 264_1211, classification = ItemClassification.progression, max_quantity = 10),
"Torn Page 1": KH1ItemData("Torn Pages", code = 264_1212, classification = ItemClassification.progression, ),
"Torn Page 2": KH1ItemData("Torn Pages", code = 264_1213, classification = ItemClassification.progression, ),
"Torn Page 3": KH1ItemData("Torn Pages", code = 264_1214, classification = ItemClassification.progression, ),
"Torn Page 4": KH1ItemData("Torn Pages", code = 264_1215, classification = ItemClassification.progression, ),
"Torn Page 5": KH1ItemData("Torn Pages", code = 264_1216, classification = ItemClassification.progression, ),
"Slides": KH1ItemData("Key", code = 264_1217, classification = ItemClassification.progression, ),
#"Slide 2": KH1ItemData("Key", code = 264_1218, classification = ItemClassification.progression, ),
#"Slide 3": KH1ItemData("Key", code = 264_1219, classification = ItemClassification.progression, ),
#"Slide 4": KH1ItemData("Key", code = 264_1220, classification = ItemClassification.progression, ),
#"Slide 5": KH1ItemData("Key", code = 264_1221, classification = ItemClassification.progression, ),
#"Slide 6": KH1ItemData("Key", code = 264_1222, classification = ItemClassification.progression, ),
"Footprints": KH1ItemData("Key", code = 264_1223, classification = ItemClassification.progression, ),
#"Claw Marks": KH1ItemData("Key", code = 264_1224, classification = ItemClassification.progression, ),
#"Stench": KH1ItemData("Key", code = 264_1225, classification = ItemClassification.progression, ),
#"Antenna": KH1ItemData("Key", code = 264_1226, classification = ItemClassification.progression, ),
"Forget-Me-Not": KH1ItemData("Key", code = 264_1227, classification = ItemClassification.progression, ),
"Jack-In-The-Box": KH1ItemData("Key", code = 264_1228, classification = ItemClassification.progression, ),
"Entry Pass": KH1ItemData("Key", code = 264_1229, classification = ItemClassification.progression, ),
#"Hero License": KH1ItemData("Key", code = 264_1230, classification = ItemClassification.progression, ),
#"Pretty Stone": KH1ItemData("Synthesis", code = 264_1231, classification = ItemClassification.filler, ),
#"N41": KH1ItemData("Synthesis", code = 264_1232, classification = ItemClassification.filler, ),
#"Lucid Shard": KH1ItemData("Synthesis", code = 264_1233, classification = ItemClassification.filler, ),
#"Lucid Gem": KH1ItemData("Synthesis", code = 264_1234, classification = ItemClassification.filler, ),
#"Lucid Crystal": KH1ItemData("Synthesis", code = 264_1235, classification = ItemClassification.filler, ),
#"Spirit Shard": KH1ItemData("Synthesis", code = 264_1236, classification = ItemClassification.filler, ),
#"Spirit Gem": KH1ItemData("Synthesis", code = 264_1237, classification = ItemClassification.filler, ),
#"Power Shard": KH1ItemData("Synthesis", code = 264_1238, classification = ItemClassification.filler, ),
#"Power Gem": KH1ItemData("Synthesis", code = 264_1239, classification = ItemClassification.filler, ),
#"Power Crystal": KH1ItemData("Synthesis", code = 264_1240, classification = ItemClassification.filler, ),
#"Blaze Shard": KH1ItemData("Synthesis", code = 264_1241, classification = ItemClassification.filler, ),
#"Blaze Gem": KH1ItemData("Synthesis", code = 264_1242, classification = ItemClassification.filler, ),
#"Frost Shard": KH1ItemData("Synthesis", code = 264_1243, classification = ItemClassification.filler, ),
#"Frost Gem": KH1ItemData("Synthesis", code = 264_1244, classification = ItemClassification.filler, ),
#"Thunder Shard": KH1ItemData("Synthesis", code = 264_1245, classification = ItemClassification.filler, ),
#"Thunder Gem": KH1ItemData("Synthesis", code = 264_1246, classification = ItemClassification.filler, ),
#"Shiny Crystal": KH1ItemData("Synthesis", code = 264_1247, classification = ItemClassification.filler, ),
#"Bright Shard": KH1ItemData("Synthesis", code = 264_1248, classification = ItemClassification.filler, ),
#"Bright Gem": KH1ItemData("Synthesis", code = 264_1249, classification = ItemClassification.filler, ),
#"Bright Crystal": KH1ItemData("Synthesis", code = 264_1250, classification = ItemClassification.filler, ),
#"Mystery Goo": KH1ItemData("Synthesis", code = 264_1251, classification = ItemClassification.filler, ),
#"Gale": KH1ItemData("Synthesis", code = 264_1252, classification = ItemClassification.filler, ),
#"Mythril Shard": KH1ItemData("Synthesis", code = 264_1253, classification = ItemClassification.filler, ),
#"Mythril": KH1ItemData("Synthesis", code = 264_1254, classification = ItemClassification.filler, ),
#"Orichalcum": KH1ItemData("Synthesis", code = 264_1255, classification = ItemClassification.filler, ),
"High Jump": KH1ItemData("Shared Abilities", code = 264_2001, classification = ItemClassification.progression, ),
"Mermaid Kick": KH1ItemData("Shared Abilities", code = 264_2002, classification = ItemClassification.progression, ),
"Progressive Glide": KH1ItemData("Shared Abilities", code = 264_2003, classification = ItemClassification.progression, max_quantity = 2 ),
#"Superglide": KH1ItemData("Shared Abilities", code = 264_2004, classification = ItemClassification.progression, ),
"Puppy 01": KH1ItemData("Puppies", code = 264_2101, classification = ItemClassification.progression, ),
"Puppy 02": KH1ItemData("Puppies", code = 264_2102, classification = ItemClassification.progression, ),
"Puppy 03": KH1ItemData("Puppies", code = 264_2103, classification = ItemClassification.progression, ),
"Puppy 04": KH1ItemData("Puppies", code = 264_2104, classification = ItemClassification.progression, ),
"Puppy 05": KH1ItemData("Puppies", code = 264_2105, classification = ItemClassification.progression, ),
"Puppy 06": KH1ItemData("Puppies", code = 264_2106, classification = ItemClassification.progression, ),
"Puppy 07": KH1ItemData("Puppies", code = 264_2107, classification = ItemClassification.progression, ),
"Puppy 08": KH1ItemData("Puppies", code = 264_2108, classification = ItemClassification.progression, ),
"Puppy 09": KH1ItemData("Puppies", code = 264_2109, classification = ItemClassification.progression, ),
"Puppy 10": KH1ItemData("Puppies", code = 264_2110, classification = ItemClassification.progression, ),
"Puppy 11": KH1ItemData("Puppies", code = 264_2111, classification = ItemClassification.progression, ),
"Puppy 12": KH1ItemData("Puppies", code = 264_2112, classification = ItemClassification.progression, ),
"Puppy 13": KH1ItemData("Puppies", code = 264_2113, classification = ItemClassification.progression, ),
"Puppy 14": KH1ItemData("Puppies", code = 264_2114, classification = ItemClassification.progression, ),
"Puppy 15": KH1ItemData("Puppies", code = 264_2115, classification = ItemClassification.progression, ),
"Puppy 16": KH1ItemData("Puppies", code = 264_2116, classification = ItemClassification.progression, ),
"Puppy 17": KH1ItemData("Puppies", code = 264_2117, classification = ItemClassification.progression, ),
"Puppy 18": KH1ItemData("Puppies", code = 264_2118, classification = ItemClassification.progression, ),
"Puppy 19": KH1ItemData("Puppies", code = 264_2119, classification = ItemClassification.progression, ),
"Puppy 20": KH1ItemData("Puppies", code = 264_2120, classification = ItemClassification.progression, ),
"Puppy 21": KH1ItemData("Puppies", code = 264_2121, classification = ItemClassification.progression, ),
"Puppy 22": KH1ItemData("Puppies", code = 264_2122, classification = ItemClassification.progression, ),
"Puppy 23": KH1ItemData("Puppies", code = 264_2123, classification = ItemClassification.progression, ),
"Puppy 24": KH1ItemData("Puppies", code = 264_2124, classification = ItemClassification.progression, ),
"Puppy 25": KH1ItemData("Puppies", code = 264_2125, classification = ItemClassification.progression, ),
"Puppy 26": KH1ItemData("Puppies", code = 264_2126, classification = ItemClassification.progression, ),
"Puppy 27": KH1ItemData("Puppies", code = 264_2127, classification = ItemClassification.progression, ),
"Puppy 28": KH1ItemData("Puppies", code = 264_2128, classification = ItemClassification.progression, ),
"Puppy 29": KH1ItemData("Puppies", code = 264_2129, classification = ItemClassification.progression, ),
"Puppy 30": KH1ItemData("Puppies", code = 264_2130, classification = ItemClassification.progression, ),
"Puppy 31": KH1ItemData("Puppies", code = 264_2131, classification = ItemClassification.progression, ),
"Puppy 32": KH1ItemData("Puppies", code = 264_2132, classification = ItemClassification.progression, ),
"Puppy 33": KH1ItemData("Puppies", code = 264_2133, classification = ItemClassification.progression, ),
"Puppy 34": KH1ItemData("Puppies", code = 264_2134, classification = ItemClassification.progression, ),
"Puppy 35": KH1ItemData("Puppies", code = 264_2135, classification = ItemClassification.progression, ),
"Puppy 36": KH1ItemData("Puppies", code = 264_2136, classification = ItemClassification.progression, ),
"Puppy 37": KH1ItemData("Puppies", code = 264_2137, classification = ItemClassification.progression, ),
"Puppy 38": KH1ItemData("Puppies", code = 264_2138, classification = ItemClassification.progression, ),
"Puppy 39": KH1ItemData("Puppies", code = 264_2139, classification = ItemClassification.progression, ),
"Puppy 40": KH1ItemData("Puppies", code = 264_2140, classification = ItemClassification.progression, ),
"Puppy 41": KH1ItemData("Puppies", code = 264_2141, classification = ItemClassification.progression, ),
"Puppy 42": KH1ItemData("Puppies", code = 264_2142, classification = ItemClassification.progression, ),
"Puppy 43": KH1ItemData("Puppies", code = 264_2143, classification = ItemClassification.progression, ),
"Puppy 44": KH1ItemData("Puppies", code = 264_2144, classification = ItemClassification.progression, ),
"Puppy 45": KH1ItemData("Puppies", code = 264_2145, classification = ItemClassification.progression, ),
"Puppy 46": KH1ItemData("Puppies", code = 264_2146, classification = ItemClassification.progression, ),
"Puppy 47": KH1ItemData("Puppies", code = 264_2147, classification = ItemClassification.progression, ),
"Puppy 48": KH1ItemData("Puppies", code = 264_2148, classification = ItemClassification.progression, ),
"Puppy 49": KH1ItemData("Puppies", code = 264_2149, classification = ItemClassification.progression, ),
"Puppy 50": KH1ItemData("Puppies", code = 264_2150, classification = ItemClassification.progression, ),
"Puppy 51": KH1ItemData("Puppies", code = 264_2151, classification = ItemClassification.progression, ),
"Puppy 52": KH1ItemData("Puppies", code = 264_2152, classification = ItemClassification.progression, ),
"Puppy 53": KH1ItemData("Puppies", code = 264_2153, classification = ItemClassification.progression, ),
"Puppy 54": KH1ItemData("Puppies", code = 264_2154, classification = ItemClassification.progression, ),
"Puppy 55": KH1ItemData("Puppies", code = 264_2155, classification = ItemClassification.progression, ),
"Puppy 56": KH1ItemData("Puppies", code = 264_2156, classification = ItemClassification.progression, ),
"Puppy 57": KH1ItemData("Puppies", code = 264_2157, classification = ItemClassification.progression, ),
"Puppy 58": KH1ItemData("Puppies", code = 264_2158, classification = ItemClassification.progression, ),
"Puppy 59": KH1ItemData("Puppies", code = 264_2159, classification = ItemClassification.progression, ),
"Puppy 60": KH1ItemData("Puppies", code = 264_2160, classification = ItemClassification.progression, ),
"Puppy 61": KH1ItemData("Puppies", code = 264_2161, classification = ItemClassification.progression, ),
"Puppy 62": KH1ItemData("Puppies", code = 264_2162, classification = ItemClassification.progression, ),
"Puppy 63": KH1ItemData("Puppies", code = 264_2163, classification = ItemClassification.progression, ),
"Puppy 64": KH1ItemData("Puppies", code = 264_2164, classification = ItemClassification.progression, ),
"Puppy 65": KH1ItemData("Puppies", code = 264_2165, classification = ItemClassification.progression, ),
"Puppy 66": KH1ItemData("Puppies", code = 264_2166, classification = ItemClassification.progression, ),
"Puppy 67": KH1ItemData("Puppies", code = 264_2167, classification = ItemClassification.progression, ),
"Puppy 68": KH1ItemData("Puppies", code = 264_2168, classification = ItemClassification.progression, ),
"Puppy 69": KH1ItemData("Puppies", code = 264_2169, classification = ItemClassification.progression, ),
"Puppy 70": KH1ItemData("Puppies", code = 264_2170, classification = ItemClassification.progression, ),
"Puppy 71": KH1ItemData("Puppies", code = 264_2171, classification = ItemClassification.progression, ),
"Puppy 72": KH1ItemData("Puppies", code = 264_2172, classification = ItemClassification.progression, ),
"Puppy 73": KH1ItemData("Puppies", code = 264_2173, classification = ItemClassification.progression, ),
"Puppy 74": KH1ItemData("Puppies", code = 264_2174, classification = ItemClassification.progression, ),
"Puppy 75": KH1ItemData("Puppies", code = 264_2175, classification = ItemClassification.progression, ),
"Puppy 76": KH1ItemData("Puppies", code = 264_2176, classification = ItemClassification.progression, ),
"Puppy 77": KH1ItemData("Puppies", code = 264_2177, classification = ItemClassification.progression, ),
"Puppy 78": KH1ItemData("Puppies", code = 264_2178, classification = ItemClassification.progression, ),
"Puppy 79": KH1ItemData("Puppies", code = 264_2179, classification = ItemClassification.progression, ),
"Puppy 80": KH1ItemData("Puppies", code = 264_2180, classification = ItemClassification.progression, ),
"Puppy 81": KH1ItemData("Puppies", code = 264_2181, classification = ItemClassification.progression, ),
"Puppy 82": KH1ItemData("Puppies", code = 264_2182, classification = ItemClassification.progression, ),
"Puppy 83": KH1ItemData("Puppies", code = 264_2183, classification = ItemClassification.progression, ),
"Puppy 84": KH1ItemData("Puppies", code = 264_2184, classification = ItemClassification.progression, ),
"Puppy 85": KH1ItemData("Puppies", code = 264_2185, classification = ItemClassification.progression, ),
"Puppy 86": KH1ItemData("Puppies", code = 264_2186, classification = ItemClassification.progression, ),
"Puppy 87": KH1ItemData("Puppies", code = 264_2187, classification = ItemClassification.progression, ),
"Puppy 88": KH1ItemData("Puppies", code = 264_2188, classification = ItemClassification.progression, ),
"Puppy 89": KH1ItemData("Puppies", code = 264_2189, classification = ItemClassification.progression, ),
"Puppy 90": KH1ItemData("Puppies", code = 264_2190, classification = ItemClassification.progression, ),
"Puppy 91": KH1ItemData("Puppies", code = 264_2191, classification = ItemClassification.progression, ),
"Puppy 92": KH1ItemData("Puppies", code = 264_2192, classification = ItemClassification.progression, ),
"Puppy 93": KH1ItemData("Puppies", code = 264_2193, classification = ItemClassification.progression, ),
"Puppy 94": KH1ItemData("Puppies", code = 264_2194, classification = ItemClassification.progression, ),
"Puppy 95": KH1ItemData("Puppies", code = 264_2195, classification = ItemClassification.progression, ),
"Puppy 96": KH1ItemData("Puppies", code = 264_2196, classification = ItemClassification.progression, ),
"Puppy 97": KH1ItemData("Puppies", code = 264_2197, classification = ItemClassification.progression, ),
"Puppy 98": KH1ItemData("Puppies", code = 264_2198, classification = ItemClassification.progression, ),
"Puppy 99": KH1ItemData("Puppies", code = 264_2199, classification = ItemClassification.progression, ),
"Puppies 01-03": KH1ItemData("Puppies", code = 264_2201, classification = ItemClassification.progression, ),
"Puppies 04-06": KH1ItemData("Puppies", code = 264_2202, classification = ItemClassification.progression, ),
"Puppies 07-09": KH1ItemData("Puppies", code = 264_2203, classification = ItemClassification.progression, ),
"Puppies 10-12": KH1ItemData("Puppies", code = 264_2204, classification = ItemClassification.progression, ),
"Puppies 13-15": KH1ItemData("Puppies", code = 264_2205, classification = ItemClassification.progression, ),
"Puppies 16-18": KH1ItemData("Puppies", code = 264_2206, classification = ItemClassification.progression, ),
"Puppies 19-21": KH1ItemData("Puppies", code = 264_2207, classification = ItemClassification.progression, ),
"Puppies 22-24": KH1ItemData("Puppies", code = 264_2208, classification = ItemClassification.progression, ),
"Puppies 25-27": KH1ItemData("Puppies", code = 264_2209, classification = ItemClassification.progression, ),
"Puppies 28-30": KH1ItemData("Puppies", code = 264_2210, classification = ItemClassification.progression, ),
"Puppies 31-33": KH1ItemData("Puppies", code = 264_2211, classification = ItemClassification.progression, ),
"Puppies 34-36": KH1ItemData("Puppies", code = 264_2212, classification = ItemClassification.progression, ),
"Puppies 37-39": KH1ItemData("Puppies", code = 264_2213, classification = ItemClassification.progression, ),
"Puppies 40-42": KH1ItemData("Puppies", code = 264_2214, classification = ItemClassification.progression, ),
"Puppies 43-45": KH1ItemData("Puppies", code = 264_2215, classification = ItemClassification.progression, ),
"Puppies 46-48": KH1ItemData("Puppies", code = 264_2216, classification = ItemClassification.progression, ),
"Puppies 49-51": KH1ItemData("Puppies", code = 264_2217, classification = ItemClassification.progression, ),
"Puppies 52-54": KH1ItemData("Puppies", code = 264_2218, classification = ItemClassification.progression, ),
"Puppies 55-57": KH1ItemData("Puppies", code = 264_2219, classification = ItemClassification.progression, ),
"Puppies 58-60": KH1ItemData("Puppies", code = 264_2220, classification = ItemClassification.progression, ),
"Puppies 61-63": KH1ItemData("Puppies", code = 264_2221, classification = ItemClassification.progression, ),
"Puppies 64-66": KH1ItemData("Puppies", code = 264_2222, classification = ItemClassification.progression, ),
"Puppies 67-69": KH1ItemData("Puppies", code = 264_2223, classification = ItemClassification.progression, ),
"Puppies 70-72": KH1ItemData("Puppies", code = 264_2224, classification = ItemClassification.progression, ),
"Puppies 73-75": KH1ItemData("Puppies", code = 264_2225, classification = ItemClassification.progression, ),
"Puppies 76-78": KH1ItemData("Puppies", code = 264_2226, classification = ItemClassification.progression, ),
"Puppies 79-81": KH1ItemData("Puppies", code = 264_2227, classification = ItemClassification.progression, ),
"Puppies 82-84": KH1ItemData("Puppies", code = 264_2228, classification = ItemClassification.progression, ),
"Puppies 85-87": KH1ItemData("Puppies", code = 264_2229, classification = ItemClassification.progression, ),
"Puppies 88-90": KH1ItemData("Puppies", code = 264_2230, classification = ItemClassification.progression, ),
"Puppies 91-93": KH1ItemData("Puppies", code = 264_2231, classification = ItemClassification.progression, ),
"Puppies 94-96": KH1ItemData("Puppies", code = 264_2232, classification = ItemClassification.progression, ),
"Puppies 97-99": KH1ItemData("Puppies", code = 264_2233, classification = ItemClassification.progression, ),
"All Puppies": KH1ItemData("Puppies", code = 264_2240, classification = ItemClassification.progression, ),
"Treasure Magnet": KH1ItemData("Abilities", code = 264_3005, classification = ItemClassification.useful, max_quantity = 2 ),
"Combo Plus": KH1ItemData("Abilities", code = 264_3006, classification = ItemClassification.useful, max_quantity = 4 ),
"Air Combo Plus": KH1ItemData("Abilities", code = 264_3007, classification = ItemClassification.useful, max_quantity = 2 ),
"Critical Plus": KH1ItemData("Abilities", code = 264_3008, classification = ItemClassification.useful, max_quantity = 3 ),
#"Second Wind": KH1ItemData("Abilities", code = 264_3009, classification = ItemClassification.useful, ),
"Scan": KH1ItemData("Abilities", code = 264_3010, classification = ItemClassification.useful, ),
"Sonic Blade": KH1ItemData("Abilities", code = 264_3011, classification = ItemClassification.useful, ),
"Ars Arcanum": KH1ItemData("Abilities", code = 264_3012, classification = ItemClassification.useful, ),
"Strike Raid": KH1ItemData("Abilities", code = 264_3013, classification = ItemClassification.useful, ),
"Ragnarok": KH1ItemData("Abilities", code = 264_3014, classification = ItemClassification.useful, ),
"Trinity Limit": KH1ItemData("Abilities", code = 264_3015, classification = ItemClassification.useful, ),
"Cheer": KH1ItemData("Abilities", code = 264_3016, classification = ItemClassification.useful, ),
"Vortex": KH1ItemData("Abilities", code = 264_3017, classification = ItemClassification.useful, ),
"Aerial Sweep": KH1ItemData("Abilities", code = 264_3018, classification = ItemClassification.useful, ),
"Counterattack": KH1ItemData("Abilities", code = 264_3019, classification = ItemClassification.useful, ),
"Blitz": KH1ItemData("Abilities", code = 264_3020, classification = ItemClassification.useful, ),
"Guard": KH1ItemData("Abilities", code = 264_3021, classification = ItemClassification.progression, ),
"Dodge Roll": KH1ItemData("Abilities", code = 264_3022, classification = ItemClassification.progression, ),
"MP Haste": KH1ItemData("Abilities", code = 264_3023, classification = ItemClassification.useful, ),
"MP Rage": KH1ItemData("Abilities", code = 264_3024, classification = ItemClassification.progression, ),
"Second Chance": KH1ItemData("Abilities", code = 264_3025, classification = ItemClassification.progression, ),
"Berserk": KH1ItemData("Abilities", code = 264_3026, classification = ItemClassification.useful, ),
"Jackpot": KH1ItemData("Abilities", code = 264_3027, classification = ItemClassification.useful, ),
"Lucky Strike": KH1ItemData("Abilities", code = 264_3028, classification = ItemClassification.useful, ),
#"Charge": KH1ItemData("Abilities", code = 264_3029, classification = ItemClassification.useful, ),
#"Rocket": KH1ItemData("Abilities", code = 264_3030, classification = ItemClassification.useful, ),
#"Tornado": KH1ItemData("Abilities", code = 264_3031, classification = ItemClassification.useful, ),
#"MP Gift": KH1ItemData("Abilities", code = 264_3032, classification = ItemClassification.useful, ),
#"Raging Boar": KH1ItemData("Abilities", code = 264_3033, classification = ItemClassification.useful, ),
#"Asp's Bite": KH1ItemData("Abilities", code = 264_3034, classification = ItemClassification.useful, ),
#"Healing Herb": KH1ItemData("Abilities", code = 264_3035, classification = ItemClassification.useful, ),
#"Wind Armor": KH1ItemData("Abilities", code = 264_3036, classification = ItemClassification.useful, ),
#"Crescent": KH1ItemData("Abilities", code = 264_3037, classification = ItemClassification.useful, ),
#"Sandstorm": KH1ItemData("Abilities", code = 264_3038, classification = ItemClassification.useful, ),
#"Applause!": KH1ItemData("Abilities", code = 264_3039, classification = ItemClassification.useful, ),
#"Blazing Fury": KH1ItemData("Abilities", code = 264_3040, classification = ItemClassification.useful, ),
#"Icy Terror": KH1ItemData("Abilities", code = 264_3041, classification = ItemClassification.useful, ),
#"Bolts of Sorrow": KH1ItemData("Abilities", code = 264_3042, classification = ItemClassification.useful, ),
#"Ghostly Scream": KH1ItemData("Abilities", code = 264_3043, classification = ItemClassification.useful, ),
#"Humming Bird": KH1ItemData("Abilities", code = 264_3044, classification = ItemClassification.useful, ),
#"Time-Out": KH1ItemData("Abilities", code = 264_3045, classification = ItemClassification.useful, ),
#"Storm's Eye": KH1ItemData("Abilities", code = 264_3046, classification = ItemClassification.useful, ),
#"Ferocious Lunge": KH1ItemData("Abilities", code = 264_3047, classification = ItemClassification.useful, ),
#"Furious Bellow": KH1ItemData("Abilities", code = 264_3048, classification = ItemClassification.useful, ),
#"Spiral Wave": KH1ItemData("Abilities", code = 264_3049, classification = ItemClassification.useful, ),
#"Thunder Potion": KH1ItemData("Abilities", code = 264_3050, classification = ItemClassification.useful, ),
#"Cure Potion": KH1ItemData("Abilities", code = 264_3051, classification = ItemClassification.useful, ),
#"Aero Potion": KH1ItemData("Abilities", code = 264_3052, classification = ItemClassification.useful, ),
"Slapshot": KH1ItemData("Abilities", code = 264_3053, classification = ItemClassification.useful, ),
"Sliding Dash": KH1ItemData("Abilities", code = 264_3054, classification = ItemClassification.useful, ),
"Hurricane Blast": KH1ItemData("Abilities", code = 264_3055, classification = ItemClassification.useful, ),
"Ripple Drive": KH1ItemData("Abilities", code = 264_3056, classification = ItemClassification.useful, ),
"Stun Impact": KH1ItemData("Abilities", code = 264_3057, classification = ItemClassification.useful, ),
"Gravity Break": KH1ItemData("Abilities", code = 264_3058, classification = ItemClassification.useful, ),
"Zantetsuken": KH1ItemData("Abilities", code = 264_3059, classification = ItemClassification.useful, ),
"Tech Boost": KH1ItemData("Abilities", code = 264_3060, classification = ItemClassification.useful, max_quantity = 4 ),
"Encounter Plus": KH1ItemData("Abilities", code = 264_3061, classification = ItemClassification.useful, ),
"Leaf Bracer": KH1ItemData("Abilities", code = 264_3062, classification = ItemClassification.progression, ),
#"Evolution": KH1ItemData("Abilities", code = 264_3063, classification = ItemClassification.useful, ),
"EXP Zero": KH1ItemData("Abilities", code = 264_3064, classification = ItemClassification.useful, ),
"Combo Master": KH1ItemData("Abilities", code = 264_3065, classification = ItemClassification.progression, ),
"Max HP Increase": KH1ItemData("Level Up", code = 264_4001, classification = ItemClassification.useful, max_quantity = 15),
"Max MP Increase": KH1ItemData("Level Up", code = 264_4002, classification = ItemClassification.useful, max_quantity = 15),
"Max AP Increase": KH1ItemData("Level Up", code = 264_4003, classification = ItemClassification.useful, max_quantity = 15),
"Strength Increase": KH1ItemData("Level Up", code = 264_4004, classification = ItemClassification.useful, max_quantity = 15),
"Defense Increase": KH1ItemData("Level Up", code = 264_4005, classification = ItemClassification.useful, max_quantity = 15),
"Accessory Slot Increase": KH1ItemData("Limited Level Up", code = 264_4006, classification = ItemClassification.useful, max_quantity = 15),
"Item Slot Increase": KH1ItemData("Limited Level Up", code = 264_4007, classification = ItemClassification.useful, max_quantity = 15),
"Dumbo": KH1ItemData("Summons", code = 264_5000, classification = ItemClassification.progression, ),
"Bambi": KH1ItemData("Summons", code = 264_5001, classification = ItemClassification.progression, ),
"Genie": KH1ItemData("Summons", code = 264_5002, classification = ItemClassification.progression, ),
"Tinker Bell": KH1ItemData("Summons", code = 264_5003, classification = ItemClassification.progression, ),
"Mushu": KH1ItemData("Summons", code = 264_5004, classification = ItemClassification.progression, ),
"Simba": KH1ItemData("Summons", code = 264_5005, classification = ItemClassification.progression, ),
"Progressive Fire": KH1ItemData("Magic", code = 264_6001, classification = ItemClassification.progression, max_quantity = 3 ),
"Progressive Blizzard": KH1ItemData("Magic", code = 264_6002, classification = ItemClassification.progression, max_quantity = 3 ),
"Progressive Thunder": KH1ItemData("Magic", code = 264_6003, classification = ItemClassification.progression, max_quantity = 3 ),
"Progressive Cure": KH1ItemData("Magic", code = 264_6004, classification = ItemClassification.progression, max_quantity = 3 ),
"Progressive Gravity": KH1ItemData("Magic", code = 264_6005, classification = ItemClassification.progression, max_quantity = 3 ),
"Progressive Stop": KH1ItemData("Magic", code = 264_6006, classification = ItemClassification.progression, max_quantity = 3 ),
"Progressive Aero": KH1ItemData("Magic", code = 264_6007, classification = ItemClassification.progression, max_quantity = 3 ),
#"Traverse Town": KH1ItemData("Worlds", code = 264_7001, classification = ItemClassification.progression, ),
"Wonderland": KH1ItemData("Worlds", code = 264_7002, classification = ItemClassification.progression, ),
"Olympus Coliseum": KH1ItemData("Worlds", code = 264_7003, classification = ItemClassification.progression, ),
"Deep Jungle": KH1ItemData("Worlds", code = 264_7004, classification = ItemClassification.progression, ),
"Agrabah": KH1ItemData("Worlds", code = 264_7005, classification = ItemClassification.progression, ),
"Halloween Town": KH1ItemData("Worlds", code = 264_7006, classification = ItemClassification.progression, ),
"Atlantica": KH1ItemData("Worlds", code = 264_7007, classification = ItemClassification.progression, ),
"Neverland": KH1ItemData("Worlds", code = 264_7008, classification = ItemClassification.progression, ),
"Hollow Bastion": KH1ItemData("Worlds", code = 264_7009, classification = ItemClassification.progression, ),
"End of the World": KH1ItemData("Worlds", code = 264_7010, classification = ItemClassification.progression, ),
"Monstro": KH1ItemData("Worlds", code = 264_7011, classification = ItemClassification.progression, ),
"Blue Trinity": KH1ItemData("Trinities", code = 264_8001, classification = ItemClassification.progression, ),
"Red Trinity": KH1ItemData("Trinities", code = 264_8002, classification = ItemClassification.progression, ),
"Green Trinity": KH1ItemData("Trinities", code = 264_8003, classification = ItemClassification.progression, ),
"Yellow Trinity": KH1ItemData("Trinities", code = 264_8004, classification = ItemClassification.progression, ),
"White Trinity": KH1ItemData("Trinities", code = 264_8005, classification = ItemClassification.progression, ),
"Phil Cup": KH1ItemData("Cups", code = 264_9001, classification = ItemClassification.progression, ),
"Pegasus Cup": KH1ItemData("Cups", code = 264_9002, classification = ItemClassification.progression, ),
"Hercules Cup": KH1ItemData("Cups", code = 264_9003, classification = ItemClassification.progression, ),
#"Hades Cup": KH1ItemData("Cups", code = 264_9004, classification = ItemClassification.progression, ),
}
event_item_table: Dict[str, KH1ItemData] = {
"Victory": KH1ItemData("Event", code = None, classification = ItemClassification.progression, type = "Event")
}
event_item_table: Dict[str, KH1ItemData] = {}
#Make item categories
item_name_groups: Dict[str, Set[str]] = {}

File diff suppressed because it is too large Load Diff

View File

@@ -6,9 +6,8 @@ class StrengthIncrease(Range):
"""
Determines the number of Strength Increases to add to the multiworld.
The randomizer will add all stat ups defined here into a pool and choose up to 99 to add to the multiworld.
Accessory Slot Increases and Item Slot Increases are prioritized first, then the remaining items (up to 99 total) are chosen at random.
The randomizer will add all stat ups defined here into a pool and choose up to 100 to add to the multiworld.
Accessory Slot Increases and Item Slot Increases are prioritized first, then the remaining items (up to 100 total) are chosen at random.
"""
display_name = "STR Increases"
range_start = 0
@@ -19,9 +18,8 @@ class DefenseIncrease(Range):
"""
Determines the number of Defense Increases to add to the multiworld.
The randomizer will add all stat ups defined here into a pool and choose up to 99 to add to the multiworld.
Accessory Slot Increases and Item Slot Increases are prioritized first, then the remaining items (up to 99 total) are chosen at random.
The randomizer will add all stat ups defined here into a pool and choose up to 100 to add to the multiworld.
Accessory Slot Increases and Item Slot Increases are prioritized first, then the remaining items (up to 100 total) are chosen at random.
"""
display_name = "DEF Increases"
range_start = 0
@@ -32,9 +30,8 @@ class HPIncrease(Range):
"""
Determines the number of HP Increases to add to the multiworld.
The randomizer will add all stat ups defined here into a pool and choose up to 99 to add to the multiworld.
Accessory Slot Increases and Item Slot Increases are prioritized first, then the remaining items (up to 99 total) are chosen at random.
The randomizer will add all stat ups defined here into a pool and choose up to 100 to add to the multiworld.
Accessory Slot Increases and Item Slot Increases are prioritized first, then the remaining items (up to 100 total) are chosen at random.
"""
display_name = "HP Increases"
range_start = 0
@@ -45,9 +42,8 @@ class APIncrease(Range):
"""
Determines the number of AP Increases to add to the multiworld.
The randomizer will add all stat ups defined here into a pool and choose up to 99 to add to the multiworld.
Accessory Slot Increases and Item Slot Increases are prioritized first, then the remaining items (up to 99 total) are chosen at random.
The randomizer will add all stat ups defined here into a pool and choose up to 100 to add to the multiworld.
Accessory Slot Increases and Item Slot Increases are prioritized first, then the remaining items (up to 100 total) are chosen at random.
"""
display_name = "AP Increases"
range_start = 0
@@ -58,9 +54,8 @@ class MPIncrease(Range):
"""
Determines the number of MP Increases to add to the multiworld.
The randomizer will add all stat ups defined here into a pool and choose up to 99 to add to the multiworld.
Accessory Slot Increases and Item Slot Increases are prioritized first, then the remaining items (up to 99 total) are chosen at random.
The randomizer will add all stat ups defined here into a pool and choose up to 100 to add to the multiworld.
Accessory Slot Increases and Item Slot Increases are prioritized first, then the remaining items (up to 100 total) are chosen at random.
"""
display_name = "MP Increases"
range_start = 0
@@ -71,9 +66,8 @@ class AccessorySlotIncrease(Range):
"""
Determines the number of Accessory Slot Increases to add to the multiworld.
The randomizer will add all stat ups defined here into a pool and choose up to 99 to add to the multiworld.
Accessory Slot Increases and Item Slot Increases are prioritized first, then the remaining items (up to 99 total) are chosen at random.
The randomizer will add all stat ups defined here into a pool and choose up to 100 to add to the multiworld.
Accessory Slot Increases and Item Slot Increases are prioritized first, then the remaining items (up to 100 total) are chosen at random.
"""
display_name = "Accessory Slot Increases"
range_start = 0
@@ -84,9 +78,8 @@ class ItemSlotIncrease(Range):
"""
Determines the number of Item Slot Increases to add to the multiworld.
The randomizer will add all stat ups defined here into a pool and choose up to 99 to add to the multiworld.
Accessory Slot Increases and Item Slot Increases are prioritized first, then the remaining items (up to 99 total) are chosen at random.
The randomizer will add all stat ups defined here into a pool and choose up to 100 to add to the multiworld.
Accessory Slot Increases and Item Slot Increases are prioritized first, then the remaining items (up to 100 total) are chosen at random.
"""
display_name = "Item Slot Increases"
range_start = 0
@@ -111,45 +104,29 @@ class SuperBosses(Toggle):
"""
display_name = "Super Bosses"
class Cups(Choice):
class Cups(Toggle):
"""
Determines which cups have their locations added to the multiworld.
Please note that the cup items will still appear in the multiworld even if set to off, as they are required to challenge Sephiroth.
Off: All cup locations are removed
Cups: Phil, Pegasus, and Hercules cups are included
Hades Cup: Hades Cup is included in addition to Phil, Pegasus, and Hercules cups. If Super Bosses are enabled, then Ice Titan is included
Toggle whether to include checks behind completing Phil, Pegasus, Hercules, or Hades cups.
Please note that the cup items will still appear in the multiworld even if toggled off, as they are required to challenge Sephiroth.
"""
display_name = "Cups"
option_off = 0
option_cups = 1
option_hades_cup = 2
default = 0
class FinalRestDoorKey(Choice):
class Goal(Choice):
"""
Determines what grants the player the Final Rest Door Key.
Determines when victory is achieved in your playthrough.
Sephiroth: Defeat Sephiroth
Unknown: Defeat Unknown
Postcards: Turn in an amount of postcards in Traverse Town
Postcards: Turn in all 10 postcards in Traverse Town
Final Ansem: Enter End of the World and defeat Ansem as normal
Puppies: Rescue and return an amount of puppies in Traverse Town
Puppies: Rescue and return all 99 puppies in Traverse Town
Final Rest: Open the chest in End of the World Final Rest
"""
display_name = "Final Rest Door Key"
display_name = "Goal"
option_sephiroth = 0
option_unknown = 1
option_postcards = 2
option_lucky_emblems = 3
option_final_ansem = 3
option_puppies = 4
option_final_rest = 5
default = 3
@@ -158,115 +135,89 @@ class EndoftheWorldUnlock(Choice):
"""Determines how End of the World is unlocked.
Item: You can receive an item called "End of the World" which unlocks the world
Lucky Emblems: A certain amount of lucky emblems are required to unlock End of the World, which is defined in your options"""
Reports: A certain amount of reports are required to unlock End of the World, which is defined in your options"""
display_name = "End of the World Unlock"
option_item = 0
option_lucky_emblems = 1
option_reports = 1
default = 1
class RequiredPostcards(Range):
"""
If "Final Rest Door Key" is set to "Postcards", defines how many postcards are required.
"""
display_name = "Required Postcards"
default = 8
range_start = 1
range_end = 10
class RequiredPuppies(Choice):
"""
If "Final Rest Door Key" is set to "Puppies", defines how many puppies are required.
"""
display_name = "Required Puppies"
default = 80
option_10 = 10
option_20 = 20
option_30 = 30
option_40 = 40
option_50 = 50
option_60 = 60
option_70 = 70
option_80 = 80
option_90 = 90
option_99 = 99
class PuppyValue(Range):
"""
Determines how many dalmatian puppies are given when a puppy item is found.
"""
display_name = "Puppy Value"
default = 3
range_start = 1
range_end = 99
class RandomizePuppies(DefaultOnToggle):
"""
If OFF, the "Puppy" item is worth 3 puppies and puppies are placed in vanilla locations.
class FinalRestDoor(Choice):
"""Determines what conditions need to be met to manifest the door in Final Rest, allowing the player to challenge Ansem.
If ON, the "Puppy" item is worth an amount of puppies defined by "Puppy Value", and are shuffled randomly.
Reports: A certain number of Ansem's Reports are required, determined by the "Reports to Open Final Rest Door" option
Puppies: Having all 99 puppies is required
Postcards: Turning in all 10 postcards is required
Superbosses: Defeating Sephiroth, Unknown, Kurt Zisa, and Phantom are required
"""
display_name = "Randomize Puppies"
display_name = "Final Rest Door"
option_reports = 0
option_puppies = 1
option_postcards = 2
option_superbosses = 3
class Puppies(Choice):
"""
Determines how dalmatian puppies are shuffled into the pool.
Full: All puppies are in one location
Triplets: Puppies are found in triplets just as they are in the base game
Individual: One puppy can be found per location
"""
display_name = "Puppies"
option_full = 0
option_triplets = 1
option_individual = 2
default = 1
class EXPMultiplier(NamedRange):
"""
Determines the multiplier to apply to EXP gained.
"""
display_name = "EXP Multiplier"
default = 16 * 4
range_start = 16 // 4
default = 16
range_start = default // 4
range_end = 128
special_range_names = {
"0.25x": int(16 // 4),
"0.5x": int(16 // 2),
"1x": 16,
"2x": 16 * 2,
"3x": 16 * 3,
"4x": 16 * 4,
"8x": 16 * 8,
"0.25x": int(default // 4),
"0.5x": int(default // 2),
"1x": default,
"2x": default * 2,
"3x": default * 3,
"4x": default * 4,
"8x": default * 8,
}
class RequiredLuckyEmblemsEotW(Range):
class RequiredReportsEotW(Range):
"""
If End of the World Unlock is set to "Lucky Emblems", determines the number of Lucky Emblems required.
If End of the World Unlock is set to "Reports", determines the number of Ansem's Reports required to open End of the World.
"""
display_name = "Lucky Emblems to Open End of the World"
default = 7
display_name = "Reports to Open End of the World"
default = 4
range_start = 0
range_end = 20
range_end = 13
class RequiredLuckyEmblemsDoor(Range):
class RequiredReportsDoor(Range):
"""
If Final Rest Door Key is set to "Lucky Emblems", determines the number of Lucky Emblems required.
If Final Rest Door is set to "Reports", determines the number of Ansem's Reports required to manifest the door in Final Rest to challenge Ansem.
"""
display_name = "Lucky Emblems to Open Final Rest Door"
default = 10
display_name = "Reports to Open Final Rest Door"
default = 4
range_start = 0
range_end = 20
range_end = 13
class LuckyEmblemsInPool(Range):
class ReportsInPool(Range):
"""
Determines the number of Lucky Emblems in the item pool.
Determines the number of Ansem's Reports in the item pool.
"""
display_name = "Lucky Emblems in Pool"
default = 13
display_name = "Reports in Pool"
default = 4
range_start = 0
range_end = 20
range_end = 13
class KeybladeStats(Choice):
class RandomizeKeybladeStats(DefaultOnToggle):
"""
Determines whether Keyblade stats should be randomized.
Randomize: Randomly generates stats for each keyblade between the defined minimums and maximums.
Shuffle: Shuffles the stats of the vanilla keyblades amongst each other.
Vanilla: Keyblade stats are unchanged.
"""
display_name = "Keyblade Stats"
option_randomize = 0
option_shuffle = 1
option_vanilla = 2
display_name = "Randomize Keyblade Stats"
class KeybladeMinStrength(Range):
"""
@@ -286,60 +237,6 @@ class KeybladeMaxStrength(Range):
range_start = 0
range_end = 20
class KeybladeMinCritRateBonus(Range):
"""
Determines the minimum Crit Rate bonus a keyblade can have.
"""
display_name = "Keyblade Minimum Crit Rate Bonus"
default = 0
range_start = 0
range_end = 200
class KeybladeMaxCritRateBonus(Range):
"""
Determines the maximum Crit Rate bonus a keyblade can have.
"""
display_name = "Keyblade Maximum Crit Rate Bonus"
default = 200
range_start = 0
range_end = 200
class KeybladeMinCritSTRBonus(Range):
"""
Determines the minimum Crit STR bonus a keyblade can have.
"""
display_name = "Keyblade Minimum Crit Rate Bonus"
default = 0
range_start = 0
range_end = 16
class KeybladeMaxCritSTRBonus(Range):
"""
Determines the maximum Crit STR bonus a keyblade can have.
"""
display_name = "Keyblade Maximum Crit Rate Bonus"
default = 16
range_start = 0
range_end = 16
class KeybladeMinRecoil(Range):
"""
Determines the minimum recoil a keyblade can have.
"""
display_name = "Keyblade Minimum Recoil"
default = 1
range_start = 1
range_end = 90
class KeybladeMaxRecoil(Range):
"""
Determines the maximum recoil a keyblade can have.
"""
display_name = "Keyblade Maximum Recoil"
default = 90
range_start = 1
range_end = 90
class KeybladeMinMP(Range):
"""
Determines the minimum MP bonus a keyblade can have.
@@ -363,43 +260,31 @@ class LevelChecks(Range):
Determines the maximum level for which checks can be obtained.
"""
display_name = "Level Checks"
default = 99
default = 100
range_start = 0
range_end = 99
range_end = 100
class ForceStatsOnLevels(NamedRange):
"""
If this value is less than the value for Level Checks, this determines the minimum level from which only stat ups are obtained at level up locations.
For example, if you want to be able to find any multiworld item from levels 2-50, then just stat ups for levels 51-100, set this value to 51.
For example, if you want to be able to find any multiworld item from levels 1-50, then just stat ups for levels 51-100, set this value to 51.
"""
display_name = "Force Stats on Level Starting From"
default = 2
range_start = 2
default = 1
range_start = 1
range_end = 101
special_range_names = {
"none": 101,
"multiworld-to-level-50": 51,
"all": 2
"all": 1
}
class BadStartingWeapons(Toggle):
"""
Forces Kingdom Key, Dream Sword, Dream Shield, and Dream Staff to have vanilla stats.
Forces Kingdom Key, Dream Sword, Dream Shield, and Dream Staff to have bad stats.
"""
display_name = "Bad Starting Weapons"
class DeathLink(Choice):
"""
If Sora is KO'ed, the other players with "Death Link" on will also be KO'ed.
The opposite is also true.
"""
display_name = "Death Link"
option_off = 0
option_toggle = 1
option_on = 2
default = 0
class DonaldDeathLink(Toggle):
"""
If Donald is KO'ed, so is Sora. If Death Link is toggled on in your client, this will send a death to everyone who enabled death link.
@@ -415,61 +300,35 @@ class GoofyDeathLink(Toggle):
class KeybladesUnlockChests(Toggle):
"""
If toggled on, the player is required to have a certain keyblade to open chests in certain worlds.
TT - Lionheart
WL - Lady Luck
OC - Olympia
DJ - Jungle King
AG - Three Wishes
MS - Wishing Star
HT - Pumpkinhead
NL - Fairy Harp
HB - Divine Rose
EotW - Oblivion
HAW - Spellbinder
DI - Oathkeeper
HAW - Oathkeeper
Note: Does not apply to Atlantica, the emblem and carousel chests in Hollow Bastion, or the Aero chest in Neverland currently.
"""
display_name = "Keyblades Unlock Chests"
class InteractInBattle(DefaultOnToggle):
class InteractInBattle(Toggle):
"""
Allow Sora to talk to people, examine objects, and open chests in battle.
"""
display_name = "Interact in Battle"
class LogicDifficulty(Choice):
class AdvancedLogic(Toggle):
"""
Determines what the randomizer logic may expect you to do to reach certain locations.
Beginner: Logic only expects what would be the natural solution in vanilla gameplay or similar, as well as a guarantee of tools for boss fights.
Normal: Logic expects some clever use of abilities, exploration of options, and competent combat ability; generally does not require advanced knowledge.
Proud: Logic expects advanced knowledge of tricks and obscure interactions, such as using Combo Master, Dumbo, and other unusual methods to reach locations.
Minimal: Logic expects the bare minimum to get to locations; may require extensive grinding, beating fights with no tools, and performing very difficult or tedious tricks.
If on, logic may expect you to do advanced skips like using Combo Master, Dumbo, and other unusual methods to reach locations.
"""
display_name = "Logic Difficulty"
option_beginner = 0
option_normal = 5
option_proud = 10
option_minimal = 15
default = 5
display_name = "Advanced Logic"
class ExtraSharedAbilities(DefaultOnToggle):
class ExtraSharedAbilities(Toggle):
"""
If on, adds extra shared abilities to the pool. These can stack, so multiple high jumps make you jump higher and multiple glides make you superglide faster.
"""
@@ -481,361 +340,51 @@ class EXPZeroInPool(Toggle):
"""
display_name = "EXP Zero in Pool"
class RandomizeEmblemPieces(Toggle):
class VanillaEmblemPieces(DefaultOnToggle):
"""
If off, the Hollow Bastion emblem pieces are in their vanilla locations.
If on, the Hollow Bastion emblem pieces are in their vanilla locations.
"""
display_name = "Randomize Emblem Pieces"
class RandomizePostcards(Choice):
"""
Determines how Postcards are randomized
All: All Postcards are randomized
Chests: Only the 3 Postcards in chests are randomized
Vanilla: Postcards are in their original location
"""
display_name = "Randomize Postcards"
option_all = 0
option_chests = 1
option_vanilla = 2
class JungleSlider(Toggle):
"""
Determines whether checks are behind the Jungle Slider minigame.
"""
display_name = "Jungle Slider"
display_name = "Vanilla Emblem Pieces"
class StartingWorlds(Range):
"""
Number of random worlds to start with in addition to Traverse Town, which is always available.
Will only consider Atlantica if toggled, and will only consider End of the World if its unlock is set to "Item".
These are given by the server, and are received after connection.
Number of random worlds to start with in addition to Traverse Town, which is always available. Will only consider Atlantica if toggled, and will only consider End of the World if its unlock is set to "Item".
"""
display_name = "Starting Worlds"
default = 4
default = 0
range_start = 0
range_end = 10
class StartingTools(DefaultOnToggle):
"""
Determines whether you start with Scan and Dodge Roll.
These are given by the server, and are received after connection.
"""
display_name = "Starting Tools"
class RemoteItems(Choice):
"""
Determines if items can be placed on locations in your own world in such a way that will force them to be remote items.
Off: When your items are placed in your world, they can only be placed in locations that they can be acquired without server connection (stats on levels, items in chests, etc).
Allow: When your items are placed in your world, items that normally can't be placed in a location in-game are simply made remote (abilities on static events, etc).
Full: All items are remote. Use this when doing something like a co-op seed.
"""
display_name = "Remote Items"
option_off = 0
option_allow = 1
option_full = 2
default = 0
class Slot2LevelChecks(Range):
"""
Determines how many levels have an additional item.
If Remote Items is OFF, these checks will only contain abilities or items for other players.
"""
display_name = "Slot 2 Level Checks"
default = 0
range_start = 0
range_end = 33
class ShortenGoMode(DefaultOnToggle):
"""
If on, the player warps to the final cutscene after defeating Ansem 1 > Darkside > Ansem 2, skipping World of Chaos.
"""
display_name = "Shorten Go Mode"
class DestinyIslands(Toggle):
"""
If on, Adds a Destiny Islands item and a number of Raft Materials items to the pool.
When "Destiny Islands" is found, Traverse Town will have an additional place to land - Seashore.
"Raft Materials" allow progress into Day 2 and to Homecoming. The amount is defined in Day 2 Materials and Homecoming Materials.
"""
display_name = "Destiny Islands"
class MythrilInPool(Range):
"""
Determines how much Mythril, one of the two synthesis items, is in the item pool.
You need 16 to synth every recipe that requires it.
"""
display_name = "Mythril In Pool"
default = 20
range_start = 16
range_end = 30
class OrichalcumInPool(Range):
"""
Determines how much Orichalcum, one of the two synthesis items, is in the item pool.
You need 17 to synth every recipe that requires it.
"""
display_name = "Mythril In Pool"
default = 20
range_start = 17
range_end = 30
class MythrilPrice(Range):
"""
Determines the cost of Mythril in each shop.
"""
display_name = "Mythril Price"
default = 500
range_start = 100
range_end = 5000
class OrichalcumPrice(Range):
"""
Determines the cost of Orichalcum in each shop.
"""
display_name = "Orichalcum Price"
default = 500
range_start = 100
range_end = 5000
class OneHP(Toggle):
"""
If on, forces Sora's max HP to 1 and removes the low health warning sound.
"""
display_name = "One HP"
class FourByThree(Toggle):
"""
If on, changes the aspect ratio to 4 by 3.
"""
display_name = "4 by 3"
class AutoAttack(Toggle):
"""
If on, you can combo by holding confirm.
"""
display_name = "Auto Attack"
class BeepHack(Toggle):
"""
If on, removes low health warning sound. Works up to max health of 41.
"""
display_name = "Beep Hack"
class ConsistentFinishers(Toggle):
"""
If on, 30% chance finishers are now 100% chance.
"""
display_name = "Consistent Finishers"
class EarlySkip(DefaultOnToggle):
"""
If on, allows skipping cutscenes immediately that normally take time to be able to skip.
"""
display_name = "Early Skip"
class FastCamera(Toggle):
"""
If on, speeds up camera movement and camera centering.
"""
display_name = "Fast Camera"
class FasterAnimations(DefaultOnToggle):
"""
If on, speeds up animations during which you can't play.
"""
display_name = "Faster Animations"
class Unlock0Volume(Toggle):
"""
If on, volume 1 mutes the audio channel.
"""
display_name = "Unlock 0 Volume"
class Unskippable(DefaultOnToggle):
"""
If on, makes unskippable cutscenes skippable.
"""
display_name = "Unskippable"
class AutoSave(DefaultOnToggle):
"""
If on, enables auto saving.
Press L1+L2+R1+R2+D-Pad Left to instantly load continue state.
Press L1+L2+R1+R2+D-Pad Right to instantly load autosave.
"""
display_name = "AutoSave"
class WarpAnywhere(Toggle):
"""
If on, enables the player to warp at any time, even when not at a save point.
Press L1+L2+R2+Select to open the Save/Warp menu at any time.
"""
display_name = "WarpAnywhere"
class RandomizePartyMemberStartingAccessories(DefaultOnToggle):
"""
If on, the 10 accessories that some party members (Aladdin, Ariel, Jack, Peter Pan, Beast) start with are randomized.
10 random accessories will be distributed amongst any party member aside from Sora in their starting equipment.
"""
display_name = "Randomize Party Member Starting Accessories"
class MaxLevelForSlot2LevelChecks(Range):
"""
Determines the max level for slot 2 level checks.
"""
display_name = "Max Level for Slot 2 Level Checks"
default = 50
range_start = 2
range_end = 100
class RandomizeAPCosts(Choice):
"""
Off: No randomization
Shuffle: Ability AP Costs will be shuffled amongst themselves.
Randomize: Ability AP Costs will be randomized to the specified max and min.
Distribute: Ability AP Costs will totalled and re-distributed randomly between the specified max and min.
"""
display_name = "Randomize AP Costs"
option_off = 0
option_shuffle = 1
option_randomize = 2
option_distribute = 3
default = 0
class MaxAPCost(Range):
"""
If Randomize AP Costs is set to Randomize or Distribute, this defined the max AP cost an ability can have.
"""
display_name = "Max AP Cost"
default = 5
range_start = 4
range_end = 9
class MinAPCost(Range):
"""
If Randomize AP Costs is set to Randomize or Distribute, this defined the min AP cost an ability can have.
"""
display_name = "Min AP Cost"
default = 0
range_start = 0
range_end = 2
class Day2Materials(Range):
"""
The amount of Raft Materials required to access Day 2.
"""
display_name = "Day 2 Materials"
default = 4
range_start = 0
range_end = 20
class HomecomingMaterials(Range):
"""
The amount of Raft Materials required to access Homecoming.
"""
display_name = "Homecoming Materials"
default = 10
range_start = 0
range_end = 20
class MaterialsInPool(Range):
"""
The amount of Raft Materials required to access Homecoming.
"""
display_name = "Materials in Pool"
default = 16
range_start = 0
range_end = 20
class StackingWorldItems(DefaultOnToggle):
"""
Multiple world items give you the world's associated key item.
WL - Footprints
OC - Entry Pass
DJ - Slides
HT - Forget-Me-Not and Jack-In-The-Box
HB - Theon Vol. 6
Adds an extra world to the pool for each that has a key item (WL, OC, DJ, HT, HB).
Forces Halloween Town Key Item Bundle ON.
"""
display_name = "Stacking World Items"
class HalloweenTownKeyItemBundle(DefaultOnToggle):
"""
Obtaining the Forget-Me-Not automatically gives Jack-in-the-Box as well.
Removes Jack-in-the-Box from the pool.
"""
display_name = "Halloween Town Key Item Bundle"
@dataclass
class KH1Options(PerGameCommonOptions):
final_rest_door_key: FinalRestDoorKey
goal: Goal
end_of_the_world_unlock: EndoftheWorldUnlock
required_lucky_emblems_eotw: RequiredLuckyEmblemsEotW
required_lucky_emblems_door: RequiredLuckyEmblemsDoor
lucky_emblems_in_pool: LuckyEmblemsInPool
required_postcards: RequiredPostcards
required_puppies: RequiredPuppies
final_rest_door: FinalRestDoor
required_reports_eotw: RequiredReportsEotW
required_reports_door: RequiredReportsDoor
reports_in_pool: ReportsInPool
super_bosses: SuperBosses
atlantica: Atlantica
hundred_acre_wood: HundredAcreWood
cups: Cups
randomize_puppies: RandomizePuppies
puppy_value: PuppyValue
puppies: Puppies
starting_worlds: StartingWorlds
keyblades_unlock_chests: KeybladesUnlockChests
interact_in_battle: InteractInBattle
exp_multiplier: EXPMultiplier
logic_difficulty: LogicDifficulty
advanced_logic: AdvancedLogic
extra_shared_abilities: ExtraSharedAbilities
exp_zero_in_pool: EXPZeroInPool
randomize_emblem_pieces: RandomizeEmblemPieces
randomize_postcards: RandomizePostcards
vanilla_emblem_pieces: VanillaEmblemPieces
donald_death_link: DonaldDeathLink
goofy_death_link: GoofyDeathLink
keyblade_stats: KeybladeStats
randomize_keyblade_stats: RandomizeKeybladeStats
bad_starting_weapons: BadStartingWeapons
keyblade_min_str: KeybladeMinStrength
keyblade_max_str: KeybladeMaxStrength
keyblade_min_crit_rate: KeybladeMinCritRateBonus
keyblade_max_crit_rate: KeybladeMaxCritRateBonus
keyblade_min_crit_str: KeybladeMinCritSTRBonus
keyblade_max_crit_str: KeybladeMaxCritSTRBonus
keyblade_min_recoil: KeybladeMinRecoil
keyblade_max_recoil: KeybladeMaxRecoil
keyblade_min_mp: KeybladeMinMP
keyblade_max_mp: KeybladeMaxMP
level_checks: LevelChecks
slot_2_level_checks: Slot2LevelChecks
force_stats_on_levels: ForceStatsOnLevels
strength_increase: StrengthIncrease
defense_increase: DefenseIncrease
@@ -845,68 +394,26 @@ class KH1Options(PerGameCommonOptions):
accessory_slot_increase: AccessorySlotIncrease
item_slot_increase: ItemSlotIncrease
start_inventory_from_pool: StartInventoryPool
jungle_slider: JungleSlider
starting_tools: StartingTools
remote_items: RemoteItems
shorten_go_mode: ShortenGoMode
death_link: DeathLink
destiny_islands: DestinyIslands
orichalcum_in_pool: OrichalcumInPool
orichalcum_price: OrichalcumPrice
mythril_in_pool: MythrilInPool
mythril_price: MythrilPrice
one_hp: OneHP
four_by_three: FourByThree
auto_attack: AutoAttack
beep_hack: BeepHack
consistent_finishers: ConsistentFinishers
early_skip: EarlySkip
fast_camera: FastCamera
faster_animations: FasterAnimations
unlock_0_volume: Unlock0Volume
unskippable: Unskippable
auto_save: AutoSave
warp_anywhere: WarpAnywhere
randomize_party_member_starting_accessories: RandomizePartyMemberStartingAccessories
max_level_for_slot_2_level_checks: MaxLevelForSlot2LevelChecks
randomize_ap_costs: RandomizeAPCosts
max_ap_cost: MaxAPCost
min_ap_cost: MinAPCost
day_2_materials: Day2Materials
homecoming_materials: HomecomingMaterials
materials_in_pool: MaterialsInPool
stacking_world_items: StackingWorldItems
halloween_town_key_item_bundle: HalloweenTownKeyItemBundle
kh1_option_groups = [
OptionGroup("Goal", [
FinalRestDoorKey,
Goal,
EndoftheWorldUnlock,
RequiredLuckyEmblemsDoor,
RequiredLuckyEmblemsEotW,
LuckyEmblemsInPool,
RequiredPostcards,
RequiredPuppies,
DestinyIslands,
Day2Materials,
HomecomingMaterials,
MaterialsInPool,
FinalRestDoor,
RequiredReportsDoor,
RequiredReportsEotW,
ReportsInPool,
]),
OptionGroup("Locations", [
SuperBosses,
Atlantica,
Cups,
HundredAcreWood,
JungleSlider,
RandomizeEmblemPieces,
RandomizePostcards,
VanillaEmblemPieces,
]),
OptionGroup("Levels", [
EXPMultiplier,
LevelChecks,
Slot2LevelChecks,
MaxLevelForSlot2LevelChecks,
ForceStatsOnLevels,
StrengthIncrease,
DefenseIncrease,
@@ -918,58 +425,21 @@ kh1_option_groups = [
]),
OptionGroup("Keyblades", [
KeybladesUnlockChests,
KeybladeStats,
RandomizeKeybladeStats,
BadStartingWeapons,
KeybladeMinStrength,
KeybladeMaxStrength,
KeybladeMinCritRateBonus,
KeybladeMaxCritRateBonus,
KeybladeMinCritSTRBonus,
KeybladeMaxCritSTRBonus,
KeybladeMinRecoil,
KeybladeMaxRecoil,
KeybladeMinMP,
KeybladeMinStrength,
KeybladeMaxMP,
]),
OptionGroup("Synth", [
OrichalcumInPool,
OrichalcumPrice,
MythrilInPool,
MythrilPrice,
]),
OptionGroup("AP Costs", [
RandomizeAPCosts,
MaxAPCost,
MinAPCost
KeybladeMinMP,
]),
OptionGroup("Misc", [
StartingWorlds,
StartingTools,
RandomizePuppies,
PuppyValue,
Puppies,
InteractInBattle,
LogicDifficulty,
AdvancedLogic,
ExtraSharedAbilities,
StackingWorldItems,
HalloweenTownKeyItemBundle,
EXPZeroInPool,
RandomizePartyMemberStartingAccessories,
DeathLink,
DonaldDeathLink,
GoofyDeathLink,
RemoteItems,
ShortenGoMode,
OneHP,
FourByThree,
AutoAttack,
BeepHack,
ConsistentFinishers,
EarlySkip,
FastCamera,
FasterAnimations,
Unlock0Volume,
Unskippable,
AutoSave,
WarpAnywhere
])
]

View File

@@ -3,33 +3,24 @@ from typing import Any, Dict
from .Options import *
kh1_option_presets: Dict[str, Dict[str, Any]] = {
# Standard playthrough where your goal is to defeat Ansem, reaching him by acquiring enough lucky emblems.
# Standard playthrough where your goal is to defeat Ansem, reaching him by acquiring enough reports.
"Final Ansem": {
"final_rest_door_key": FinalRestDoorKey.option_lucky_emblems,
"end_of_the_world_unlock": EndoftheWorldUnlock.option_lucky_emblems,
"required_lucky_emblems_eotw": 7,
"required_lucky_emblems_door": 10,
"lucky_emblems_in_pool": 13,
"required_postcards": 10,
"required_puppies": 99,
"destiny_islands": True,
"day_2_materials": 4,
"homecoming_materials": 10,
"materials_in_pool": 13,
"goal": Goal.option_final_ansem,
"end_of_the_world_unlock": EndoftheWorldUnlock.option_reports,
"final_rest_door": FinalRestDoor.option_reports,
"required_reports_eotw": 7,
"required_reports_door": 10,
"reports_in_pool": 13,
"super_bosses": False,
"atlantica": False,
"hundred_acre_wood": False,
"cups": Cups.option_off,
"jungle_slider": False,
"randomize_emblem_pieces": False,
"randomize_postcards": RandomizePostcards.option_all,
"cups": False,
"vanilla_emblem_pieces": True,
"exp_multiplier": 64,
"level_checks": 99,
"slot_2_level_checks": 33,
"max_level_for_slot_2_level_checks": 50,
"force_stats_on_levels": 2,
"exp_multiplier": 48,
"level_checks": 100,
"force_stats_on_levels": 1,
"strength_increase": 24,
"defense_increase": 24,
"hp_increase": 23,
@@ -39,83 +30,40 @@ kh1_option_presets: Dict[str, Dict[str, Any]] = {
"item_slot_increase": 3,
"keyblades_unlock_chests": False,
"keyblade_stats": KeybladeStats.option_shuffle,
"randomize_keyblade_stats": True,
"bad_starting_weapons": False,
"keyblade_max_str": 14,
"keyblade_min_str": 3,
"keyblade_max_crit_rate": 200,
"keyblade_min_crit_rate": 0,
"keyblade_max_crit_str": 16,
"keyblade_min_crit_str": 0,
"keyblade_max_recoil": 90,
"keyblade_min_recoil": 1,
"keyblade_max_mp": 3,
"keyblade_min_mp": -2,
"orichalcum_in_pool": 20,
"orichalcum_price": 500,
"mythril_in_pool": 20,
"mythril_price": 500,
"randomize_ap_costs": RandomizeAPCosts.option_off,
"max_ap_cost": 5,
"min_ap_cost": 0,
"randomize_puppies": True,
"puppy_value": 3,
"starting_worlds": 4,
"starting_tools": True,
"interact_in_battle": True,
"logic_difficulty": LogicDifficulty.option_normal,
"extra_shared_abilities": True,
"stacking_world_items": True,
"halloween_town_key_item_bundle": True,
"puppies": Puppies.option_triplets,
"starting_worlds": 0,
"interact_in_battle": False,
"advanced_logic": False,
"extra_shared_abilities": False,
"exp_zero_in_pool": False,
"randomize_party_member_starting_accessories": True,
"death_link": False,
"donald_death_link": False,
"goofy_death_link": False,
"remote_items": RemoteItems.option_off,
"shorten_go_mode": True,
"one_hp": False,
"four_by_three": False,
"beep_hack": False,
"consistent_finishers": False,
"early_skip": True,
"fast_camera": False,
"faster_animations": True,
"unlock_0_volume": False,
"unskippable": True,
"auto_save": True,
"warp_anywhere": False
"goofy_death_link": False
},
# Puppies are found individually, and the goal is to return them all.
"Puppy Hunt": {
"final_rest_door_key": FinalRestDoorKey.option_puppies,
"goal": Goal.option_puppies,
"end_of_the_world_unlock": EndoftheWorldUnlock.option_item,
"required_lucky_emblems_eotw": 13,
"required_lucky_emblems_door": 13,
"lucky_emblems_in_pool": 13,
"required_postcards": 10,
"required_puppies": 99,
"destiny_islands": False,
"day_2_materials": 4,
"homecoming_materials": 10,
"materials_in_pool": 13,
"final_rest_door": FinalRestDoor.option_puppies,
"required_reports_eotw": 13,
"required_reports_door": 13,
"reports_in_pool": 13,
"super_bosses": False,
"atlantica": False,
"hundred_acre_wood": False,
"cups": Cups.option_off,
"jungle_slider": False,
"randomize_emblem_pieces": False,
"randomize_postcards": RandomizePostcards.option_all,
"cups": False,
"vanilla_emblem_pieces": True,
"exp_multiplier": 64,
"level_checks": 99,
"slot_2_level_checks": 33,
"max_level_for_slot_2_level_checks": 50,
"force_stats_on_levels": 2,
"exp_multiplier": 48,
"level_checks": 100,
"force_stats_on_levels": 1,
"strength_increase": 24,
"defense_increase": 24,
"hp_increase": 23,
@@ -125,83 +73,40 @@ kh1_option_presets: Dict[str, Dict[str, Any]] = {
"item_slot_increase": 3,
"keyblades_unlock_chests": False,
"keyblade_stats": KeybladeStats.option_shuffle,
"randomize_keyblade_stats": True,
"bad_starting_weapons": False,
"keyblade_max_str": 14,
"keyblade_min_str": 3,
"keyblade_max_crit_rate": 200,
"keyblade_min_crit_rate": 0,
"keyblade_max_crit_str": 16,
"keyblade_min_crit_str": 0,
"keyblade_max_recoil": 90,
"keyblade_min_recoil": 1,
"keyblade_max_mp": 3,
"keyblade_min_mp": -2,
"orichalcum_in_pool": 20,
"orichalcum_price": 500,
"mythril_in_pool": 20,
"mythril_price": 500,
"randomize_ap_costs": RandomizeAPCosts.option_off,
"max_ap_cost": 5,
"min_ap_cost": 0,
"randomize_puppies": True,
"puppy_value": 1,
"puppies": Puppies.option_individual,
"starting_worlds": 0,
"starting_tools": True,
"interact_in_battle": True,
"logic_difficulty": LogicDifficulty.option_normal,
"extra_shared_abilities": True,
"stacking_world_items": True,
"halloween_town_key_item_bundle": True,
"interact_in_battle": False,
"advanced_logic": False,
"extra_shared_abilities": False,
"exp_zero_in_pool": False,
"randomize_party_member_starting_accessories": True,
"death_link": False,
"donald_death_link": False,
"goofy_death_link": False,
"remote_items": RemoteItems.option_off,
"shorten_go_mode": True,
"one_hp": False,
"four_by_three": False,
"beep_hack": False,
"consistent_finishers": False,
"early_skip": True,
"fast_camera": False,
"faster_animations": True,
"unlock_0_volume": False,
"unskippable": True,
"auto_save": True,
"warp_anywhere": False
"goofy_death_link": False
},
# Advanced playthrough with most settings on.
"Advanced": {
"final_rest_door_key": FinalRestDoorKey.option_lucky_emblems,
"end_of_the_world_unlock": EndoftheWorldUnlock.option_lucky_emblems,
"required_lucky_emblems_eotw": 7,
"required_lucky_emblems_door": 10,
"lucky_emblems_in_pool": 13,
"required_postcards": 10,
"required_puppies": 99,
"destiny_islands": True,
"day_2_materials": 4,
"homecoming_materials": 10,
"materials_in_pool": 13,
"goal": Goal.option_final_ansem,
"end_of_the_world_unlock": EndoftheWorldUnlock.option_reports,
"final_rest_door": FinalRestDoor.option_reports,
"required_reports_eotw": 7,
"required_reports_door": 10,
"reports_in_pool": 13,
"super_bosses": True,
"atlantica": True,
"hundred_acre_wood": True,
"cups": Cups.option_off,
"jungle_slider": True,
"randomize_emblem_pieces": True,
"randomize_postcards": RandomizePostcards.option_all,
"cups": True,
"vanilla_emblem_pieces": False,
"exp_multiplier": 64,
"level_checks": 99,
"slot_2_level_checks": 33,
"max_level_for_slot_2_level_checks": 50,
"force_stats_on_levels": 2,
"exp_multiplier": 48,
"level_checks": 100,
"force_stats_on_levels": 1,
"strength_increase": 24,
"defense_increase": 24,
"hp_increase": 23,
@@ -211,83 +116,40 @@ kh1_option_presets: Dict[str, Dict[str, Any]] = {
"item_slot_increase": 3,
"keyblades_unlock_chests": True,
"keyblade_stats": KeybladeStats.option_shuffle,
"randomize_keyblade_stats": True,
"bad_starting_weapons": True,
"keyblade_max_str": 14,
"keyblade_min_str": 3,
"keyblade_max_crit_rate": 200,
"keyblade_min_crit_rate": 0,
"keyblade_max_crit_str": 16,
"keyblade_min_crit_str": 0,
"keyblade_max_recoil": 90,
"keyblade_min_recoil": 1,
"keyblade_max_mp": 3,
"keyblade_min_mp": -2,
"orichalcum_in_pool": 20,
"orichalcum_price": 500,
"mythril_in_pool": 20,
"mythril_price": 500,
"randomize_ap_costs": RandomizeAPCosts.option_off,
"max_ap_cost": 5,
"min_ap_cost": 0,
"randomize_puppies": True,
"puppy_value": 3,
"puppies": Puppies.option_triplets,
"starting_worlds": 0,
"starting_tools": True,
"interact_in_battle": True,
"logic_difficulty": LogicDifficulty.option_proud,
"advanced_logic": True,
"extra_shared_abilities": True,
"stacking_world_items": True,
"halloween_town_key_item_bundle": True,
"exp_zero_in_pool": True,
"randomize_party_member_starting_accessories": True,
"death_link": False,
"donald_death_link": False,
"goofy_death_link": False,
"remote_items": RemoteItems.option_off,
"shorten_go_mode": True,
"one_hp": False,
"four_by_three": False,
"beep_hack": False,
"consistent_finishers": False,
"early_skip": True,
"fast_camera": False,
"faster_animations": True,
"unlock_0_volume": False,
"unskippable": True,
"auto_save": True,
"warp_anywhere": False
"goofy_death_link": False
},
# Playthrough meant to enhance the level 1 experience.
"Level 1": {
"final_rest_door_key": FinalRestDoorKey.option_lucky_emblems,
"end_of_the_world_unlock": EndoftheWorldUnlock.option_lucky_emblems,
"required_lucky_emblems_eotw": 7,
"required_lucky_emblems_door": 10,
"lucky_emblems_in_pool": 13,
"required_postcards": 10,
"required_puppies": 99,
"destiny_islands": True,
"day_2_materials": 4,
"homecoming_materials": 10,
"materials_in_pool": 13,
"goal": Goal.option_final_ansem,
"end_of_the_world_unlock": EndoftheWorldUnlock.option_reports,
"final_rest_door": FinalRestDoor.option_reports,
"required_reports_eotw": 7,
"required_reports_door": 10,
"reports_in_pool": 13,
"super_bosses": False,
"atlantica": False,
"hundred_acre_wood": False,
"cups": Cups.option_off,
"jungle_slider": False,
"randomize_emblem_pieces": False,
"randomize_postcards": RandomizePostcards.option_all,
"cups": False,
"vanilla_emblem_pieces": True,
"exp_multiplier": 16,
"level_checks": 0,
"slot_2_level_checks": 0,
"max_level_for_slot_2_level_checks": 50,
"force_stats_on_levels": 2,
"force_stats_on_levels": 101,
"strength_increase": 0,
"defense_increase": 0,
"hp_increase": 0,
@@ -296,54 +158,20 @@ kh1_option_presets: Dict[str, Dict[str, Any]] = {
"item_slot_increase": 5,
"keyblades_unlock_chests": False,
"keyblade_stats": KeybladeStats.option_shuffle,
"randomize_keyblade_stats": True,
"bad_starting_weapons": False,
"keyblade_max_str": 14,
"keyblade_min_str": 3,
"keyblade_max_crit_rate": 200,
"keyblade_min_crit_rate": 0,
"keyblade_max_crit_str": 16,
"keyblade_min_crit_str": 0,
"keyblade_max_recoil": 90,
"keyblade_min_recoil": 1,
"keyblade_max_mp": 3,
"keyblade_min_mp": -2,
"orichalcum_in_pool": 20,
"orichalcum_price": 500,
"mythril_in_pool": 20,
"mythril_price": 500,
"randomize_ap_costs": RandomizeAPCosts.option_off,
"max_ap_cost": 5,
"min_ap_cost": 0,
"randomize_puppies": True,
"puppy_value": 3,
"puppies": Puppies.option_triplets,
"starting_worlds": 0,
"starting_tools": True,
"interact_in_battle": True,
"logic_difficulty": LogicDifficulty.option_normal,
"extra_shared_abilities": True,
"stacking_world_items": True,
"halloween_town_key_item_bundle": True,
"interact_in_battle": False,
"advanced_logic": False,
"extra_shared_abilities": False,
"exp_zero_in_pool": False,
"randomize_party_member_starting_accessories": True,
"death_link": False,
"donald_death_link": False,
"goofy_death_link": False,
"remote_items": RemoteItems.option_off,
"shorten_go_mode": True,
"one_hp": False,
"four_by_three": False,
"beep_hack": False,
"consistent_finishers": True,
"early_skip": True,
"fast_camera": False,
"faster_animations": True,
"unlock_0_volume": False,
"unskippable": True,
"auto_save": True,
"warp_anywhere": False
"goofy_death_link": False
}
}

View File

@@ -9,16 +9,12 @@ class KH1RegionData(NamedTuple):
region_exits: Optional[List[str]]
def create_regions(kh1world):
multiworld = kh1world.multiworld
player = kh1world.player
options = kh1world.options
def create_regions(multiworld: MultiWorld, player: int, options):
regions: Dict[str, KH1RegionData] = {
"Menu": KH1RegionData([], ["Awakening", "Levels", "World Map"]),
"Awakening": KH1RegionData([], []),
"Destiny Islands": KH1RegionData([], []),
"Traverse Town": KH1RegionData([], []),
"Menu": KH1RegionData([], ["Awakening", "Levels"]),
"Awakening": KH1RegionData([], ["Destiny Islands"]),
"Destiny Islands": KH1RegionData([], ["Traverse Town"]),
"Traverse Town": KH1RegionData([], ["World Map"]),
"Wonderland": KH1RegionData([], []),
"Olympus Coliseum": KH1RegionData([], []),
"Deep Jungle": KH1RegionData([], []),
@@ -31,27 +27,17 @@ def create_regions(kh1world):
"End of the World": KH1RegionData([], []),
"100 Acre Wood": KH1RegionData([], []),
"Levels": KH1RegionData([], []),
"Homecoming": KH1RegionData([], []),
"World Map": KH1RegionData([], ["Destiny Islands", "Traverse Town",
"Wonderland", "Olympus Coliseum", "Deep Jungle",
"World Map": KH1RegionData([], ["Wonderland", "Olympus Coliseum", "Deep Jungle",
"Agrabah", "Monstro", "Atlantica",
"Halloween Town", "Neverland", "Hollow Bastion",
"End of the World", "100 Acre Wood", "Homecoming"])
"End of the World", "100 Acre Wood"])
}
if not options.atlantica:
del regions["Atlantica"]
regions["World Map"].region_exits.remove("Atlantica")
if not options.destiny_islands:
del regions["Destiny Islands"]
regions["World Map"].region_exits.remove("Destiny Islands")
# Set up locations
regions["Agrabah"].locations.append("Agrabah Aladdin's House Main Street Entrance Chest")
regions["Agrabah"].locations.append("Agrabah Aladdin's House Plaza Entrance Chest")
regions["Agrabah"].locations.append("Agrabah Alley Chest")
regions["Agrabah"].locations.append("Agrabah Bazaar Across Windows Chest")
regions["Agrabah"].locations.append("Agrabah Bazaar Blue Trinity")
regions["Agrabah"].locations.append("Agrabah Bazaar High Corner Chest")
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Bottomless Hall Across Chasm Chest")
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Bottomless Hall Pillar Chest")
@@ -73,7 +59,6 @@ def create_regions(kh1world):
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Treasure Room Above Fire Chest")
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Treasure Room Across Platforms Chest")
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Treasure Room Large Treasure Pile Chest")
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Treasure Room Red Trinity")
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Treasure Room Small Treasure Pile Chest")
regions["Agrabah"].locations.append("Agrabah Defeat Jafar Blizzard Event")
regions["Agrabah"].locations.append("Agrabah Defeat Jafar Genie Ansem's Report 1")
@@ -111,11 +96,15 @@ def create_regions(kh1world):
regions["Deep Jungle"].locations.append("Deep Jungle Hippo's Lagoon Center Chest")
regions["Deep Jungle"].locations.append("Deep Jungle Hippo's Lagoon Left Chest")
regions["Deep Jungle"].locations.append("Deep Jungle Hippo's Lagoon Right Chest")
regions["Deep Jungle"].locations.append("Deep Jungle Jungle Slider 10 Fruits")
regions["Deep Jungle"].locations.append("Deep Jungle Jungle Slider 20 Fruits")
regions["Deep Jungle"].locations.append("Deep Jungle Jungle Slider 30 Fruits")
regions["Deep Jungle"].locations.append("Deep Jungle Jungle Slider 40 Fruits")
regions["Deep Jungle"].locations.append("Deep Jungle Jungle Slider 50 Fruits")
regions["Deep Jungle"].locations.append("Deep Jungle Seal Keyhole Jungle King Event")
regions["Deep Jungle"].locations.append("Deep Jungle Seal Keyhole Red Trinity Event")
regions["Deep Jungle"].locations.append("Deep Jungle Tent Chest")
regions["Deep Jungle"].locations.append("Deep Jungle Tent Protect-G Event")
regions["Deep Jungle"].locations.append("Deep Jungle Treetop Green Trinity")
regions["Deep Jungle"].locations.append("Deep Jungle Tree House Beneath Tree House Chest")
regions["Deep Jungle"].locations.append("Deep Jungle Tree House Rooftop Chest")
regions["Deep Jungle"].locations.append("Deep Jungle Tree House Save Gorillas")
@@ -149,7 +138,7 @@ def create_regions(kh1world):
regions["End of the World"].locations.append("End of the World World Terminus Atlantica Chest")
regions["End of the World"].locations.append("End of the World World Terminus Deep Jungle Chest")
regions["End of the World"].locations.append("End of the World World Terminus Halloween Town Chest")
regions["End of the World"].locations.append("End of the World World Terminus Hollow Bastion Chest")
#regions["End of the World"].locations.append("End of the World World Terminus Hollow Bastion Chest")
regions["End of the World"].locations.append("End of the World World Terminus Neverland Chest")
regions["End of the World"].locations.append("End of the World World Terminus Olympus Coliseum Chest")
regions["End of the World"].locations.append("End of the World World Terminus Traverse Town Chest")
@@ -192,7 +181,6 @@ def create_regions(kh1world):
regions["Hollow Bastion"].locations.append("Hollow Bastion Defeat Maleficent Donald Cheer Event")
regions["Hollow Bastion"].locations.append("Hollow Bastion Defeat Riku I White Trinity Event")
regions["Hollow Bastion"].locations.append("Hollow Bastion Defeat Riku II Ragnarok Event")
regions["Hollow Bastion"].locations.append("Hollow Bastion Dungeon Blue Trinity")
regions["Hollow Bastion"].locations.append("Hollow Bastion Dungeon By Candles Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Dungeon Corner Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Entrance Hall Emblem Piece (Chest)")
@@ -204,7 +192,6 @@ def create_regions(kh1world):
regions["Hollow Bastion"].locations.append("Hollow Bastion Grand Hall Oblivion Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Grand Hall Steps Right Side Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Great Crest After Battle Platform Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Great Crest Blue Trinity")
regions["Hollow Bastion"].locations.append("Hollow Bastion Great Crest Lower Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion High Tower 1st Gravity Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion High Tower 2nd Gravity Chest")
@@ -216,7 +203,6 @@ def create_regions(kh1world):
regions["Hollow Bastion"].locations.append("Hollow Bastion Library Speak to Belle Divine Rose")
regions["Hollow Bastion"].locations.append("Hollow Bastion Library Top of Bookshelf Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Library Top of Bookshelf Turn the Carousel Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Lift Stop from Waterway Examine Node")
regions["Hollow Bastion"].locations.append("Hollow Bastion Lift Stop Heartless Sigil Door Gravity Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Lift Stop Library Node After High Tower Switch Gravity Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Lift Stop Library Node Gravity Chest")
@@ -244,7 +230,6 @@ def create_regions(kh1world):
regions["Monstro"].locations.append("Monstro Chamber 3 Platform Above Chamber 2 Entrance Chest")
regions["Monstro"].locations.append("Monstro Chamber 3 Platform Near Chamber 6 Entrance Chest")
regions["Monstro"].locations.append("Monstro Chamber 5 Atop Barrel Chest")
regions["Monstro"].locations.append("Monstro Chamber 5 Blue Trinity")
regions["Monstro"].locations.append("Monstro Chamber 5 Low 1st Chest")
regions["Monstro"].locations.append("Monstro Chamber 5 Low 2nd Chest")
regions["Monstro"].locations.append("Monstro Chamber 5 Platform Chest")
@@ -255,28 +240,26 @@ def create_regions(kh1world):
regions["Monstro"].locations.append("Monstro Chamber 6 White Trinity Chest")
regions["Monstro"].locations.append("Monstro Defeat Parasite Cage I Goofy Cheer Event")
regions["Monstro"].locations.append("Monstro Defeat Parasite Cage II Stop Event")
regions["Monstro"].locations.append("Monstro Mouth Blue Trinity")
regions["Monstro"].locations.append("Monstro Mouth Boat Deck Chest")
regions["Monstro"].locations.append("Monstro Mouth Green Trinity Top of Boat Chest")
regions["Monstro"].locations.append("Monstro Mouth High Platform Across from Boat Chest")
regions["Monstro"].locations.append("Monstro Mouth High Platform Boat Side Chest")
regions["Monstro"].locations.append("Monstro Mouth High Platform Near Teeth Chest")
regions["Monstro"].locations.append("Monstro Mouth Near Ship Chest")
regions["Monstro"].locations.append("Monstro Throat Blue Trinity")
regions["Neverland"].locations.append("Neverland Cabin Chest")
regions["Neverland"].locations.append("Neverland Captain's Cabin Chest")
regions["Neverland"].locations.append("Neverland Clock Tower 01:00 Door")
regions["Neverland"].locations.append("Neverland Clock Tower 02:00 Door")
regions["Neverland"].locations.append("Neverland Clock Tower 03:00 Door")
regions["Neverland"].locations.append("Neverland Clock Tower 04:00 Door")
regions["Neverland"].locations.append("Neverland Clock Tower 05:00 Door")
regions["Neverland"].locations.append("Neverland Clock Tower 06:00 Door")
regions["Neverland"].locations.append("Neverland Clock Tower 07:00 Door")
regions["Neverland"].locations.append("Neverland Clock Tower 08:00 Door")
regions["Neverland"].locations.append("Neverland Clock Tower 09:00 Door")
regions["Neverland"].locations.append("Neverland Clock Tower 10:00 Door")
regions["Neverland"].locations.append("Neverland Clock Tower 11:00 Door")
regions["Neverland"].locations.append("Neverland Clock Tower 12:00 Door")
#regions["Neverland"].locations.append("Neverland Clock Tower 01:00 Door")
#regions["Neverland"].locations.append("Neverland Clock Tower 02:00 Door")
#regions["Neverland"].locations.append("Neverland Clock Tower 03:00 Door")
#regions["Neverland"].locations.append("Neverland Clock Tower 04:00 Door")
#regions["Neverland"].locations.append("Neverland Clock Tower 05:00 Door")
#regions["Neverland"].locations.append("Neverland Clock Tower 06:00 Door")
#regions["Neverland"].locations.append("Neverland Clock Tower 07:00 Door")
#regions["Neverland"].locations.append("Neverland Clock Tower 08:00 Door")
#regions["Neverland"].locations.append("Neverland Clock Tower 09:00 Door")
#regions["Neverland"].locations.append("Neverland Clock Tower 10:00 Door")
#regions["Neverland"].locations.append("Neverland Clock Tower 11:00 Door")
#regions["Neverland"].locations.append("Neverland Clock Tower 12:00 Door")
regions["Neverland"].locations.append("Neverland Clock Tower Chest")
regions["Neverland"].locations.append("Neverland Defeat Anti Sora Raven's Claw Event")
regions["Neverland"].locations.append("Neverland Defeat Captain Hook Ars Arcanum Event")
@@ -293,7 +276,6 @@ def create_regions(kh1world):
regions["Neverland"].locations.append("Neverland Pirate Ship Deck White Trinity Chest")
regions["Neverland"].locations.append("Neverland Seal Keyhole Fairy Harp Event")
regions["Neverland"].locations.append("Neverland Seal Keyhole Glide Event")
regions["Neverland"].locations.append("Neverland Seal Keyhole Navi-G Piece Event")
regions["Neverland"].locations.append("Neverland Seal Keyhole Tinker Bell Event")
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Clear Phil's Training Thunder Event")
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Cloud Sonic Blade Event")
@@ -310,16 +292,14 @@ def create_regions(kh1world):
regions["Traverse Town"].locations.append("Traverse Town 1st District Accessory Shop Roof Chest")
#regions["Traverse Town"].locations.append("Traverse Town 1st District Aerith Gift")
regions["Traverse Town"].locations.append("Traverse Town 1st District Blue Trinity Balcony Chest")
regions["Traverse Town"].locations.append("Traverse Town 1st District Blue Trinity by Exit Door")
regions["Traverse Town"].locations.append("Traverse Town 1st District Candle Puzzle Chest")
regions["Traverse Town"].locations.append("Traverse Town 1st District Leon Gift")
#regions["Traverse Town"].locations.append("Traverse Town 1st District Leon Gift")
regions["Traverse Town"].locations.append("Traverse Town 1st District Safe Postcard")
#regions["Traverse Town"].locations.append("Traverse Town 1st District Speak with Cid Event")
regions["Traverse Town"].locations.append("Traverse Town 1st District Speak with Cid Event")
regions["Traverse Town"].locations.append("Traverse Town 2nd District Boots and Shoes Awning Chest")
regions["Traverse Town"].locations.append("Traverse Town 2nd District Gizmo Shop Facade Chest")
regions["Traverse Town"].locations.append("Traverse Town 2nd District Rooftop Chest")
regions["Traverse Town"].locations.append("Traverse Town 3rd District Balcony Postcard")
regions["Traverse Town"].locations.append("Traverse Town 3rd District Blue Trinity")
regions["Traverse Town"].locations.append("Traverse Town Accessory Shop Chest")
regions["Traverse Town"].locations.append("Traverse Town Alleyway Balcony Chest")
regions["Traverse Town"].locations.append("Traverse Town Alleyway Behind Crates Chest")
@@ -330,7 +310,6 @@ def create_regions(kh1world):
regions["Traverse Town"].locations.append("Traverse Town Defeat Guard Armor Dodge Roll Event")
regions["Traverse Town"].locations.append("Traverse Town Defeat Guard Armor Fire Event")
regions["Traverse Town"].locations.append("Traverse Town Defeat Opposite Armor Aero Event")
regions["Traverse Town"].locations.append("Traverse Town Defeat Opposite Armor Navi-G Piece Event")
regions["Traverse Town"].locations.append("Traverse Town Geppetto's House Chest")
regions["Traverse Town"].locations.append("Traverse Town Geppetto's House Geppetto All Summons Reward")
regions["Traverse Town"].locations.append("Traverse Town Geppetto's House Geppetto Reward 1")
@@ -350,7 +329,6 @@ def create_regions(kh1world):
regions["Traverse Town"].locations.append("Traverse Town Item Workshop Right Chest")
regions["Traverse Town"].locations.append("Traverse Town Kairi Secret Waterway Oathkeeper Event")
regions["Traverse Town"].locations.append("Traverse Town Leon Secret Waterway Earthshine Event")
regions["Traverse Town"].locations.append("Traverse Town Magician's Study Blue Trinity")
regions["Traverse Town"].locations.append("Traverse Town Magician's Study Obtained All Arts Items")
regions["Traverse Town"].locations.append("Traverse Town Magician's Study Obtained All LV1 Magic")
regions["Traverse Town"].locations.append("Traverse Town Magician's Study Obtained All LV3 Magic")
@@ -379,62 +357,26 @@ def create_regions(kh1world):
regions["Traverse Town"].locations.append("Traverse Town Piano Room Return 99 Puppies Reward 1")
regions["Traverse Town"].locations.append("Traverse Town Piano Room Return 99 Puppies Reward 2")
regions["Traverse Town"].locations.append("Traverse Town Red Room Chest")
regions["Traverse Town"].locations.append("Traverse Town Secret Waterway Navi Gummi Event")
regions["Traverse Town"].locations.append("Traverse Town Secret Waterway Near Stairs Chest")
regions["Traverse Town"].locations.append("Traverse Town Secret Waterway White Trinity Chest")
regions["Traverse Town"].locations.append("Traverse Town Synth 15 Items")
regions["Traverse Town"].locations.append("Traverse Town Synth Item 01")
regions["Traverse Town"].locations.append("Traverse Town Synth Item 02")
regions["Traverse Town"].locations.append("Traverse Town Synth Item 03")
regions["Traverse Town"].locations.append("Traverse Town Synth Item 04")
regions["Traverse Town"].locations.append("Traverse Town Synth Item 05")
regions["Traverse Town"].locations.append("Traverse Town Synth Item 06")
regions["Traverse Town"].locations.append("Traverse Town Synth Item 07")
regions["Traverse Town"].locations.append("Traverse Town Synth Item 08")
regions["Traverse Town"].locations.append("Traverse Town Synth Item 09")
regions["Traverse Town"].locations.append("Traverse Town Synth Item 10")
regions["Traverse Town"].locations.append("Traverse Town Synth Item 11")
regions["Traverse Town"].locations.append("Traverse Town Synth Item 12")
regions["Traverse Town"].locations.append("Traverse Town Synth Item 13")
regions["Traverse Town"].locations.append("Traverse Town Synth Item 14")
regions["Traverse Town"].locations.append("Traverse Town Synth Item 15")
regions["Traverse Town"].locations.append("Traverse Town Synth Item 16")
regions["Traverse Town"].locations.append("Traverse Town Synth Item 17")
regions["Traverse Town"].locations.append("Traverse Town Synth Item 18")
regions["Traverse Town"].locations.append("Traverse Town Synth Item 19")
regions["Traverse Town"].locations.append("Traverse Town Synth Item 20")
regions["Traverse Town"].locations.append("Traverse Town Synth Item 21")
regions["Traverse Town"].locations.append("Traverse Town Synth Item 22")
regions["Traverse Town"].locations.append("Traverse Town Synth Item 23")
regions["Traverse Town"].locations.append("Traverse Town Synth Item 24")
regions["Traverse Town"].locations.append("Traverse Town Synth Item 25")
regions["Traverse Town"].locations.append("Traverse Town Synth Item 26")
regions["Traverse Town"].locations.append("Traverse Town Synth Item 27")
regions["Traverse Town"].locations.append("Traverse Town Synth Item 28")
regions["Traverse Town"].locations.append("Traverse Town Synth Item 29")
regions["Traverse Town"].locations.append("Traverse Town Synth Item 30")
regions["Traverse Town"].locations.append("Traverse Town Synth Item 31")
regions["Traverse Town"].locations.append("Traverse Town Synth Item 32")
regions["Traverse Town"].locations.append("Traverse Town Synth Item 33")
regions["Wonderland"].locations.append("Wonderland Bizarre Room Examine Flower Pot")
regions["Traverse Town"].locations.append("Traverse Town Synth Cloth")
regions["Traverse Town"].locations.append("Traverse Town Synth Fish")
regions["Traverse Town"].locations.append("Traverse Town Synth Log")
regions["Traverse Town"].locations.append("Traverse Town Synth Mushroom")
regions["Traverse Town"].locations.append("Traverse Town Synth Rope")
regions["Traverse Town"].locations.append("Traverse Town Synth Seagull Egg")
regions["Wonderland"].locations.append("Wonderland Bizarre Room Green Trinity Chest")
regions["Wonderland"].locations.append("Wonderland Bizarre Room Lamp Chest")
regions["Wonderland"].locations.append("Wonderland Bizarre Room Navi-G Piece Event")
regions["Wonderland"].locations.append("Wonderland Bizarre Room Read Book")
regions["Wonderland"].locations.append("Wonderland Defeat Trickmaster Blizzard Event")
regions["Wonderland"].locations.append("Wonderland Defeat Trickmaster Ifrit's Horn Event")
regions["Wonderland"].locations.append("Wonderland Lotus Forest Blue Trinity in Alcove")
regions["Wonderland"].locations.append("Wonderland Lotus Forest Blue Trinity by Moving Boulder")
regions["Wonderland"].locations.append("Wonderland Lotus Forest Corner Chest")
regions["Wonderland"].locations.append("Wonderland Lotus Forest Glide Chest")
regions["Wonderland"].locations.append("Wonderland Lotus Forest Nut Chest")
regions["Wonderland"].locations.append("Wonderland Lotus Forest Red Flower Raise Lily Pads")
regions["Wonderland"].locations.append("Wonderland Lotus Forest Red Flowers on the Main Path")
regions["Wonderland"].locations.append("Wonderland Lotus Forest Through the Painting Thunder Plant Chest")
regions["Wonderland"].locations.append("Wonderland Lotus Forest Through the Painting White Trinity Chest")
regions["Wonderland"].locations.append("Wonderland Lotus Forest Thunder Plant Chest")
regions["Wonderland"].locations.append("Wonderland Lotus Forest Yellow Elixir Flower Through Painting")
regions["Wonderland"].locations.append("Wonderland Lotus Forest Yellow Flowers in Middle Clearing and Through Painting")
regions["Wonderland"].locations.append("Wonderland Queen's Castle Hedge Left Red Chest")
regions["Wonderland"].locations.append("Wonderland Queen's Castle Hedge Right Blue Chest")
regions["Wonderland"].locations.append("Wonderland Queen's Castle Hedge Right Red Chest")
@@ -446,11 +388,6 @@ def create_regions(kh1world):
regions["Wonderland"].locations.append("Wonderland Tea Party Garden Above Lotus Forest Entrance 2nd Chest")
regions["Wonderland"].locations.append("Wonderland Tea Party Garden Across From Bizarre Room Entrance Chest")
regions["Wonderland"].locations.append("Wonderland Tea Party Garden Bear and Clock Puzzle Chest")
regions["Wonderland"].locations.append("Wonderland Tea Party Garden Left Cushioned Chair")
regions["Wonderland"].locations.append("Wonderland Tea Party Garden Left Gray Chair")
regions["Wonderland"].locations.append("Wonderland Tea Party Garden Left Pink Chair")
regions["Wonderland"].locations.append("Wonderland Tea Party Garden Right Brown Chair")
regions["Wonderland"].locations.append("Wonderland Tea Party Garden Right Yellow Chair")
if options.hundred_acre_wood:
regions["100 Acre Wood"].locations.append("100 Acre Wood Meadow Inside Log Chest")
regions["100 Acre Wood"].locations.append("100 Acre Wood Bouncing Spot Left Cliff Chest")
@@ -503,7 +440,7 @@ def create_regions(kh1world):
regions["Atlantica"].locations.append("Atlantica Undersea Cave Clam")
regions["Atlantica"].locations.append("Atlantica Sunken Ship Crystal Trident Event")
regions["Atlantica"].locations.append("Atlantica Defeat Ursula II Ansem's Report 3")
if options.cups.current_key != "off":
if options.cups:
regions["Olympus Coliseum"].locations.append("Complete Phil Cup")
regions["Olympus Coliseum"].locations.append("Complete Phil Cup Solo")
regions["Olympus Coliseum"].locations.append("Complete Phil Cup Time Trial")
@@ -513,84 +450,50 @@ def create_regions(kh1world):
regions["Olympus Coliseum"].locations.append("Complete Hercules Cup")
regions["Olympus Coliseum"].locations.append("Complete Hercules Cup Solo")
regions["Olympus Coliseum"].locations.append("Complete Hercules Cup Time Trial")
regions["Olympus Coliseum"].locations.append("Complete Hades Cup")
regions["Olympus Coliseum"].locations.append("Complete Hades Cup Solo")
regions["Olympus Coliseum"].locations.append("Complete Hades Cup Time Trial")
regions["Olympus Coliseum"].locations.append("Hades Cup Defeat Cloud and Leon Event")
regions["Olympus Coliseum"].locations.append("Hades Cup Defeat Yuffie Event")
regions["Olympus Coliseum"].locations.append("Hades Cup Defeat Cerberus Event")
regions["Olympus Coliseum"].locations.append("Hades Cup Defeat Behemoth Event")
regions["Olympus Coliseum"].locations.append("Hades Cup Defeat Hades Event")
regions["Olympus Coliseum"].locations.append("Hercules Cup Defeat Cloud Event")
regions["Olympus Coliseum"].locations.append("Hercules Cup Yellow Trinity Event")
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Defeat Hades Ansem's Report 8")
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Olympia Chest")
if options.cups.current_key == "hades_cup":
regions["Olympus Coliseum"].locations.append("Complete Hades Cup")
regions["Olympus Coliseum"].locations.append("Complete Hades Cup Solo")
regions["Olympus Coliseum"].locations.append("Complete Hades Cup Time Trial")
regions["Olympus Coliseum"].locations.append("Hades Cup Defeat Cloud and Leon Event")
regions["Olympus Coliseum"].locations.append("Hades Cup Defeat Yuffie Event")
regions["Olympus Coliseum"].locations.append("Hades Cup Defeat Cerberus Event")
regions["Olympus Coliseum"].locations.append("Hades Cup Defeat Behemoth Event")
regions["Olympus Coliseum"].locations.append("Hades Cup Defeat Hades Event")
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Defeat Hades Ansem's Report 8")
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Gates Purple Jar After Defeating Hades")
if options.cups.current_key == "hades_cup" and options.super_bosses:
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Defeat Ice Titan Diamond Dust Event")
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Defeat Ice Titan Diamond Dust Event")
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Gates Purple Jar After Defeating Hades")
if options.super_bosses:
regions["Neverland"].locations.append("Neverland Defeat Phantom Stop Event")
regions["Agrabah"].locations.append("Agrabah Defeat Kurt Zisa Zantetsuken Event")
regions["Agrabah"].locations.append("Agrabah Defeat Kurt Zisa Ansem's Report 11")
if options.super_bosses or options.final_rest_door_key.current_key == "sephiroth":
if options.super_bosses or options.goal.current_key == "sephiroth":
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Defeat Sephiroth Ansem's Report 12")
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Defeat Sephiroth One-Winged Angel Event")
if options.super_bosses or options.final_rest_door_key.current_key == "unknown":
if options.super_bosses or options.goal.current_key == "unknown":
regions["Hollow Bastion"].locations.append("Hollow Bastion Defeat Unknown Ansem's Report 13")
regions["Hollow Bastion"].locations.append("Hollow Bastion Defeat Unknown EXP Necklace Event")
if options.jungle_slider:
regions["Deep Jungle"].locations.append("Deep Jungle Jungle Slider 10 Fruits")
regions["Deep Jungle"].locations.append("Deep Jungle Jungle Slider 20 Fruits")
regions["Deep Jungle"].locations.append("Deep Jungle Jungle Slider 30 Fruits")
regions["Deep Jungle"].locations.append("Deep Jungle Jungle Slider 40 Fruits")
regions["Deep Jungle"].locations.append("Deep Jungle Jungle Slider 50 Fruits")
for i in range(1,options.level_checks+1):
regions["Levels"].locations.append("Level " + str(i+1).rjust(3, '0') + " (Slot 1)")
if i+1 in kh1world.get_slot_2_levels():
regions["Levels"].locations.append("Level " + str(i+1).rjust(3, '0') + " (Slot 2)")
if options.destiny_islands:
regions["Destiny Islands"].locations.append("Destiny Islands Seashore Capture Fish 1 (Day 2)")
regions["Destiny Islands"].locations.append("Destiny Islands Seashore Capture Fish 2 (Day 2)")
regions["Destiny Islands"].locations.append("Destiny Islands Seashore Capture Fish 3 (Day 2)")
regions["Destiny Islands"].locations.append("Destiny Islands Seashore Gather Seagull Egg (Day 2)")
regions["Destiny Islands"].locations.append("Destiny Islands Seashore Log on Riku's Island (Day 1)")
regions["Destiny Islands"].locations.append("Destiny Islands Seashore Log under Bridge (Day 1)")
regions["Destiny Islands"].locations.append("Destiny Islands Seashore Gather Cloth (Day 1)")
regions["Destiny Islands"].locations.append("Destiny Islands Seashore Gather Rope (Day 1)")
#regions["Destiny Islands"].locations.append("Destiny Islands Seashore Deliver Kairi Items (Day 1)")
regions["Destiny Islands"].locations.append("Destiny Islands Secret Place Gather Mushroom (Day 2)")
regions["Destiny Islands"].locations.append("Destiny Islands Cove Gather Mushroom Near Zip Line (Day 2)")
regions["Destiny Islands"].locations.append("Destiny Islands Cove Gather Mushroom in Small Cave (Day 2)")
regions["Destiny Islands"].locations.append("Destiny Islands Cove Talk to Kairi (Day 2)")
regions["Destiny Islands"].locations.append("Destiny Islands Gather Drinking Water (Day 2)")
#regions["Destiny Islands"].locations.append("Destiny Islands Cove Deliver Kairi Items (Day 2)")
regions["Destiny Islands"].locations.append("Destiny Islands Chest")
regions["Homecoming"].locations.append("Final Ansem")
for location in kh1world.get_starting_accessory_locations():
regions[location_table[location].category].locations.append(location)
for i in range(options.level_checks):
regions["Levels"].locations.append("Level " + str(i+1).rjust(3, '0'))
if options.goal.current_key == "final_ansem":
regions["End of the World"].locations.append("Final Ansem")
# Set up the regions correctly.
for name, data in regions.items():
multiworld.regions.append(create_region(multiworld, player, name, data))
def connect_entrances(kh1world):
multiworld = kh1world.multiworld
player = kh1world.player
options = kh1world.options
def connect_entrances(multiworld: MultiWorld, player: int):
multiworld.get_entrance("Awakening", player).connect(multiworld.get_region("Awakening", player))
if options.destiny_islands:
multiworld.get_entrance("Destiny Islands", player).connect(multiworld.get_region("Destiny Islands", player))
multiworld.get_entrance("Destiny Islands", player).connect(multiworld.get_region("Destiny Islands", player))
multiworld.get_entrance("Traverse Town", player).connect(multiworld.get_region("Traverse Town", player))
multiworld.get_entrance("Wonderland", player).connect(multiworld.get_region("Wonderland", player))
multiworld.get_entrance("Olympus Coliseum", player).connect(multiworld.get_region("Olympus Coliseum", player))
multiworld.get_entrance("Deep Jungle", player).connect(multiworld.get_region("Deep Jungle", player))
multiworld.get_entrance("Agrabah", player).connect(multiworld.get_region("Agrabah", player))
multiworld.get_entrance("Monstro", player).connect(multiworld.get_region("Monstro", player))
if options.atlantica:
multiworld.get_entrance("Atlantica", player).connect(multiworld.get_region("Atlantica", player))
multiworld.get_entrance("Atlantica", player).connect(multiworld.get_region("Atlantica", player))
multiworld.get_entrance("Halloween Town", player).connect(multiworld.get_region("Halloween Town", player))
multiworld.get_entrance("Neverland", player).connect(multiworld.get_region("Neverland", player))
multiworld.get_entrance("Hollow Bastion", player).connect(multiworld.get_region("Hollow Bastion", player))
@@ -598,7 +501,7 @@ def connect_entrances(kh1world):
multiworld.get_entrance("100 Acre Wood", player).connect(multiworld.get_region("100 Acre Wood", player))
multiworld.get_entrance("World Map", player).connect(multiworld.get_region("World Map", player))
multiworld.get_entrance("Levels", player).connect(multiworld.get_region("Levels", player))
multiworld.get_entrance("Homecoming", player).connect(multiworld.get_region("Homecoming", player))
def create_region(multiworld: MultiWorld, player: int, name: str, data: KH1RegionData):
region = Region(name, player, multiworld)

File diff suppressed because it is too large Load Diff

View File

@@ -1,29 +1,23 @@
import logging
import re
from typing import List
from math import ceil
from BaseClasses import Tutorial
from worlds.AutoWorld import WebWorld, World
from .Items import KH1Item, KH1ItemData, event_item_table, get_items_by_category, item_table, item_name_groups
from .Locations import KH1Location, location_table, get_locations_by_type, location_name_groups
from .Locations import KH1Location, location_table, get_locations_by_category, location_name_groups
from .Options import KH1Options, kh1_option_groups
from .Regions import connect_entrances, create_regions
from .Rules import set_rules
from .Presets import kh1_option_presets
from worlds.LauncherComponents import Component, components, Type, launch as launch_component, icon_paths
from .GenerateJSON import generate_json
from .Data import VANILLA_KEYBLADE_STATS, VANILLA_PUPPY_LOCATIONS, CHAR_TO_KH, VANILLA_ABILITY_AP_COSTS, WORLD_KEY_ITEMS
from worlds.LauncherComponents import Component, components, Type, launch_subprocess
from worlds.LauncherComponents import Component, components, Type, launch as launch_component
def launch_client():
from .Client import launch
launch_component(launch, name="KH1 Client")
components.append(Component("KH1 Client", func=launch_client, component_type=Type.CLIENT, icon="kh1_heart"))
icon_paths["kh1_heart"] = f"ap:{__name__}/icons/kh1_heart.png"
components.append(Component("KH1 Client", "KH1Client", func=launch_client, component_type=Type.CLIENT))
class KH1Web(WebWorld):
@@ -60,19 +54,6 @@ class KH1World(World):
fillers.update(get_items_by_category("Item"))
fillers.update(get_items_by_category("Camping"))
fillers.update(get_items_by_category("Stat Ups"))
slot_2_levels: list[int]
keyblade_stats: list[dict[str, int]]
starting_accessory_locations: list[str]
starting_accessories: list[str]
ap_costs: list[dict[str, str | int | bool]]
def __init__(self, multiworld, player):
super(KH1World, self).__init__(multiworld, player)
self.slot_2_levels = None
self.keyblade_stats = None
self.starting_accessory_locations = None
self.starting_accessories = None
self.ap_costs = None
def create_items(self):
self.place_predetermined_items()
@@ -82,29 +63,12 @@ class KH1World(World):
possible_starting_worlds = ["Wonderland", "Olympus Coliseum", "Deep Jungle", "Agrabah", "Monstro", "Halloween Town", "Neverland", "Hollow Bastion"]
if self.options.atlantica:
possible_starting_worlds.append("Atlantica")
if self.options.destiny_islands:
possible_starting_worlds.append("Destiny Islands")
if self.options.end_of_the_world_unlock == "item":
possible_starting_worlds.append("End of the World")
starting_worlds = self.random.sample(possible_starting_worlds, min(self.options.starting_worlds.value, len(possible_starting_worlds)))
for starting_world in starting_worlds:
self.multiworld.push_precollected(self.create_item(starting_world))
# Handle starting tools
starting_tools = []
if self.options.starting_tools:
starting_tools = ["Scan", "Dodge Roll"]
self.multiworld.push_precollected(self.create_item("Scan"))
self.multiworld.push_precollected(self.create_item("Dodge Roll"))
# Handle starting party member accessories
starting_party_member_accessories = []
starting_party_member_locations = []
starting_party_member_locations = self.get_starting_accessory_locations()
starting_party_member_accessories = self.get_starting_accessories()
for i in range(len(starting_party_member_locations)):
self.get_location(self.starting_accessory_locations[i]).place_locked_item(self.create_item(self.starting_accessories[i]))
item_pool: List[KH1Item] = []
possible_level_up_item_pool = []
level_up_item_pool = []
@@ -130,26 +94,19 @@ class KH1World(World):
# Fill remaining pool with items from other pool
self.random.shuffle(possible_level_up_item_pool)
level_up_item_pool = level_up_item_pool + possible_level_up_item_pool[:(99 - len(level_up_item_pool))]
level_up_locations = list(get_locations_by_type("Level Slot 1").keys())
level_up_item_pool = level_up_item_pool + possible_level_up_item_pool[:(100 - len(level_up_item_pool))]
level_up_locations = list(get_locations_by_category("Levels").keys())
self.random.shuffle(level_up_item_pool)
current_level_index_for_placing_stats = self.options.force_stats_on_levels.value - 2 # Level 2 is index 0, Level 3 is index 1, etc
if self.options.remote_items.current_key == "off" and self.options.force_stats_on_levels.value != 2:
logging.info(f"{self.player_name}'s value {self.options.force_stats_on_levels.value} for force_stats_on_levels was changed\n"
f"Set to 2 as remote_items if \"off\"")
self.options.force_stats_on_levels.value = 2
current_level_index_for_placing_stats = 0
while len(level_up_item_pool) > 0 and current_level_index_for_placing_stats < self.options.level_checks: # With all levels in location pool, 99 level ups so need to go index 0-98
self.get_location(level_up_locations[current_level_index_for_placing_stats]).place_locked_item(self.create_item(level_up_item_pool.pop()))
current_level_index_for_placing_stats += 1
current_level_for_placing_stats = self.options.force_stats_on_levels.value
while len(level_up_item_pool) > 0 and current_level_for_placing_stats <= self.options.level_checks:
self.get_location(level_up_locations[current_level_for_placing_stats - 1]).place_locked_item(self.create_item(level_up_item_pool.pop()))
current_level_for_placing_stats += 1
# Calculate prefilled locations and items
exclude_items = ["Final Door Key", "Lucky Emblem"]
if not self.options.randomize_emblem_pieces:
exclude_items = exclude_items + ["Emblem Piece (Flame)", "Emblem Piece (Chest)", "Emblem Piece (Fountain)", "Emblem Piece (Statue)"]
prefilled_items = []
if self.options.vanilla_emblem_pieces:
prefilled_items = prefilled_items + ["Emblem Piece (Flame)", "Emblem Piece (Chest)", "Emblem Piece (Fountain)", "Emblem Piece (Statue)"]
total_locations = len(self.multiworld.get_unfilled_locations(self.player))
@@ -160,29 +117,27 @@ class KH1World(World):
quantity = data.max_quantity
if data.category not in non_filler_item_categories:
continue
if name in starting_worlds or name in starting_tools or name in starting_party_member_accessories:
if name in starting_worlds:
continue
if self.options.stacking_world_items and name in WORLD_KEY_ITEMS.keys() and name not in ("Crystal Trident", "Jack-In-The-Box"): # Handling these special cases separately
item_pool += [self.create_item(WORLD_KEY_ITEMS[name]) for _ in range(0, 1)]
elif self.options.halloween_town_key_item_bundle and name == "Jack-In-The-Box":
continue
elif name == "Puppy":
if self.options.randomize_puppies:
item_pool += [self.create_item(name) for _ in range(ceil(99/self.options.puppy_value.value))]
if data.category == "Puppies":
if self.options.puppies == "triplets" and "-" in name:
item_pool += [self.create_item(name) for _ in range(quantity)]
if self.options.puppies == "individual" and "Puppy" in name:
item_pool += [self.create_item(name) for _ in range(0, quantity)]
if self.options.puppies == "full" and name == "All Puppies":
item_pool += [self.create_item(name) for _ in range(0, quantity)]
elif name == "Atlantica":
if self.options.atlantica:
item_pool += [self.create_item(name) for _ in range(0, quantity)]
elif name == "Mermaid Kick":
if self.options.atlantica and self.options.extra_shared_abilities:
item_pool += [self.create_item(name) for _ in range(0, 2)]
else:
item_pool += [self.create_item(name) for _ in range(0, quantity)]
elif name == "Crystal Trident":
if self.options.atlantica:
if self.options.stacking_world_items:
item_pool += [self.create_item(WORLD_KEY_ITEMS[name]) for _ in range(0, 1)]
if self.options.extra_shared_abilities:
item_pool += [self.create_item(name) for _ in range(0, 2)]
else:
item_pool += [self.create_item(name) for _ in range(0, quantity)]
elif name == "Crystal Trident":
if self.options.atlantica:
item_pool += [self.create_item(name) for _ in range(0, quantity)]
elif name == "High Jump":
if self.options.extra_shared_abilities:
item_pool += [self.create_item(name) for _ in range(0, 3)]
@@ -199,26 +154,11 @@ class KH1World(World):
elif name == "EXP Zero":
if self.options.exp_zero_in_pool:
item_pool += [self.create_item(name) for _ in range(0, quantity)]
elif name == "Postcard":
if self.options.randomize_postcards.current_key == "chests":
item_pool += [self.create_item(name) for _ in range(0, 3)]
if self.options.randomize_postcards.current_key == "all":
item_pool += [self.create_item(name) for _ in range(0, quantity)]
elif name == "Orichalcum":
item_pool += [self.create_item(name) for _ in range(0, self.options.orichalcum_in_pool.value)]
elif name == "Mythril":
item_pool += [self.create_item(name) for _ in range(0, self.options.mythril_in_pool.value)]
elif name == "Destiny Islands":
if self.options.destiny_islands:
item_pool += [self.create_item(name) for _ in range(0, quantity)]
elif name == "Raft Materials":
if self.options.destiny_islands:
item_pool += [self.create_item(name) for _ in range(0, self.options.materials_in_pool.value)]
elif name not in exclude_items:
elif name not in prefilled_items:
item_pool += [self.create_item(name) for _ in range(0, quantity)]
for i in range(self.determine_lucky_emblems_in_pool()):
item_pool += [self.create_item("Lucky Emblem")]
for i in range(self.determine_reports_in_pool()):
item_pool += [self.create_item("Ansem's Report " + str(i+1))]
while len(item_pool) < total_locations and len(level_up_item_pool) > 0:
item_pool += [self.create_item(level_up_item_pool.pop())]
@@ -230,117 +170,63 @@ class KH1World(World):
self.multiworld.itempool += item_pool
def place_predetermined_items(self) -> None:
if self.options.final_rest_door_key.current_key not in ["puppies", "postcards", "lucky_emblems"]:
goal_dict = {
"sephiroth": "Olympus Coliseum Defeat Sephiroth Ansem's Report 12",
"unknown": "Hollow Bastion Defeat Unknown Ansem's Report 13",
"final_rest": "End of the World Final Rest Chest"
}
goal_location_name = goal_dict[self.options.final_rest_door_key.current_key]
elif self.options.final_rest_door_key.current_key == "postcards":
lpad_number = str(self.options.required_postcards).rjust(2, "0")
goal_location_name = "Traverse Town Mail Postcard " + lpad_number + " Event"
elif self.options.final_rest_door_key.current_key == "puppies":
required_puppies = self.options.required_puppies.value
goal_location_name = "Traverse Town Piano Room Return " + str(required_puppies) + " Puppies"
if required_puppies == 50 or required_puppies == 99:
goal_location_name = goal_location_name + " Reward 2"
if self.options.final_rest_door_key.current_key != "lucky_emblems":
self.get_location(goal_location_name).place_locked_item(self.create_item("Final Door Key"))
self.get_location("Final Ansem").place_locked_item(self.create_event("Victory"))
if not self.options.randomize_emblem_pieces:
goal_dict = {
"sephiroth": "Olympus Coliseum Defeat Sephiroth Ansem's Report 12",
"unknown": "Hollow Bastion Defeat Unknown Ansem's Report 13",
"postcards": "Traverse Town Mail Postcard 10 Event",
"final_ansem": "Final Ansem",
"puppies": "Traverse Town Piano Room Return 99 Puppies Reward 2",
"final_rest": "End of the World Final Rest Chest"
}
self.get_location(goal_dict[self.options.goal.current_key]).place_locked_item(self.create_item("Victory"))
if self.options.vanilla_emblem_pieces:
self.get_location("Hollow Bastion Entrance Hall Emblem Piece (Flame)").place_locked_item(self.create_item("Emblem Piece (Flame)"))
self.get_location("Hollow Bastion Entrance Hall Emblem Piece (Statue)").place_locked_item(self.create_item("Emblem Piece (Statue)"))
self.get_location("Hollow Bastion Entrance Hall Emblem Piece (Fountain)").place_locked_item(self.create_item("Emblem Piece (Fountain)"))
self.get_location("Hollow Bastion Entrance Hall Emblem Piece (Chest)").place_locked_item(self.create_item("Emblem Piece (Chest)"))
if self.options.randomize_postcards != "all":
self.get_location("Traverse Town Item Shop Postcard").place_locked_item(self.create_item("Postcard"))
self.get_location("Traverse Town 1st District Safe Postcard").place_locked_item(self.create_item("Postcard"))
self.get_location("Traverse Town Gizmo Shop Postcard 1").place_locked_item(self.create_item("Postcard"))
self.get_location("Traverse Town Gizmo Shop Postcard 2").place_locked_item(self.create_item("Postcard"))
self.get_location("Traverse Town Item Workshop Postcard").place_locked_item(self.create_item("Postcard"))
self.get_location("Traverse Town 3rd District Balcony Postcard").place_locked_item(self.create_item("Postcard"))
self.get_location("Traverse Town Geppetto's House Postcard").place_locked_item(self.create_item("Postcard"))
if self.options.randomize_postcards.current_key == "vanilla":
self.get_location("Traverse Town 1st District Accessory Shop Roof Chest").place_locked_item(self.create_item("Postcard"))
self.get_location("Traverse Town 2nd District Boots and Shoes Awning Chest").place_locked_item(self.create_item("Postcard"))
self.get_location("Traverse Town 1st District Blue Trinity Balcony Chest").place_locked_item(self.create_item("Postcard"))
if not self.options.randomize_puppies:
if self.options.puppy_value.value != 3:
self.options.puppy_value.value = 3
logging.info(f"{self.player_name}'s value of {self.options.puppy_value.value} for puppy value was changed to 3 as Randomize Puppies is OFF")
for i, location in enumerate(VANILLA_PUPPY_LOCATIONS):
self.get_location(location).place_locked_item(self.create_item("Puppy"))
def get_filler_item_name(self) -> str:
weights = [data.weight for data in self.fillers.values()]
return self.random.choices([filler for filler in self.fillers.keys()], weights)[0]
def fill_slot_data(self) -> dict:
slot_data = {
"atlantica": bool(self.options.atlantica),
"auto_attack": bool(self.options.auto_attack),
"auto_save": bool(self.options.auto_save),
"bad_starting_weapons": bool(self.options.bad_starting_weapons),
"beep_hack": bool(self.options.beep_hack),
"consistent_finishers": bool(self.options.consistent_finishers),
"cups": str(self.options.cups.current_key),
"day_2_materials": int(self.options.day_2_materials.value),
"death_link": str(self.options.death_link.current_key),
"destiny_islands": bool(self.options.destiny_islands),
"donald_death_link": bool(self.options.donald_death_link),
"early_skip": bool(self.options.early_skip),
"end_of_the_world_unlock": str(self.options.end_of_the_world_unlock.current_key),
"exp_multiplier": int(self.options.exp_multiplier.value)/16,
"exp_zero_in_pool": bool(self.options.exp_zero_in_pool),
"extra_shared_abilities": bool(self.options.extra_shared_abilities),
"fast_camera": bool(self.options.fast_camera),
"faster_animations": bool(self.options.faster_animations),
"final_rest_door_key": str(self.options.final_rest_door_key.current_key),
"force_stats_on_levels": int(self.options.force_stats_on_levels.value),
"four_by_three": bool(self.options.four_by_three),
"goofy_death_link": bool(self.options.goofy_death_link),
"halloween_town_key_item_bundle": bool(self.options.halloween_town_key_item_bundle),
"homecoming_materials": int(self.options.homecoming_materials.value),
"hundred_acre_wood": bool(self.options.hundred_acre_wood),
"interact_in_battle": bool(self.options.interact_in_battle),
"jungle_slider": bool(self.options.jungle_slider),
"keyblades_unlock_chests": bool(self.options.keyblades_unlock_chests),
"level_checks": int(self.options.level_checks.value),
"logic_difficulty": str(self.options.logic_difficulty.current_key),
"materials_in_pool": int(self.options.materials_in_pool.value),
"max_ap_cost": int(self.options.max_ap_cost.value),
"min_ap_cost": int(self.options.min_ap_cost.value),
"mythril_in_pool": int(self.options.mythril_in_pool.value),
"mythril_price": int(self.options.mythril_price.value),
"one_hp": bool(self.options.one_hp),
"orichalcum_in_pool": int(self.options.orichalcum_in_pool.value),
"orichalcum_price": int(self.options.orichalcum_price.value),
"puppy_value": int(self.options.puppy_value.value),
"randomize_ap_costs": str(self.options.randomize_ap_costs.current_key),
"randomize_emblem_pieces": bool(self.options.exp_zero_in_pool),
"randomize_party_member_starting_accessories": bool(self.options.randomize_party_member_starting_accessories),
"randomize_postcards": str(self.options.randomize_postcards.current_key),
"randomize_puppies": str(self.options.randomize_puppies.current_key),
"remote_items": str(self.options.remote_items.current_key),
"remote_location_ids": self.get_remote_location_ids(),
"required_lucky_emblems_door": self.determine_lucky_emblems_required_to_open_final_rest_door(),
"required_lucky_emblems_eotw": self.determine_lucky_emblems_required_to_open_end_of_the_world(),
"required_postcards": int(self.options.required_postcards.value),
"required_puppies": int(self.options.required_puppies.value),
slot_data = {"xpmult": int(self.options.exp_multiplier)/16,
"required_reports_eotw": self.determine_reports_required_to_open_end_of_the_world(),
"required_reports_door": self.determine_reports_required_to_open_final_rest_door(),
"door": self.options.final_rest_door.current_key,
"seed": self.multiworld.seed_name,
"shorten_go_mode": bool(self.options.shorten_go_mode),
"slot_2_level_checks": int(self.options.slot_2_level_checks.value),
"stacking_world_items": bool(self.options.stacking_world_items),
"starting_items": [item.code for item in self.multiworld.precollected_items[self.player]],
"starting_tools": bool(self.options.starting_tools),
"super_bosses": bool(self.options.super_bosses),
"synthesis_item_name_byte_arrays": self.get_synthesis_item_name_byte_arrays(),
"unlock_0_volume": bool(self.options.unlock_0_volume),
"unskippable": bool(self.options.unskippable),
"warp_anywhere": bool(self.options.warp_anywhere)
}
"advanced_logic": bool(self.options.advanced_logic),
"hundred_acre_wood": bool(self.options.hundred_acre_wood),
"atlantica": bool(self.options.atlantica),
"goal": str(self.options.goal.current_key)}
if self.options.randomize_keyblade_stats:
min_str_bonus = min(self.options.keyblade_min_str.value, self.options.keyblade_max_str.value)
max_str_bonus = max(self.options.keyblade_min_str.value, self.options.keyblade_max_str.value)
self.options.keyblade_min_str.value = min_str_bonus
self.options.keyblade_max_str.value = max_str_bonus
min_mp_bonus = min(self.options.keyblade_min_mp.value, self.options.keyblade_max_mp.value)
max_mp_bonus = max(self.options.keyblade_min_mp.value, self.options.keyblade_max_mp.value)
self.options.keyblade_min_mp.value = min_mp_bonus
self.options.keyblade_max_mp.value = max_mp_bonus
slot_data["keyblade_stats"] = ""
for i in range(22):
if i < 4 and self.options.bad_starting_weapons:
slot_data["keyblade_stats"] = slot_data["keyblade_stats"] + "1,0,"
else:
str_bonus = int(self.random.randint(min_str_bonus, max_str_bonus))
mp_bonus = int(self.random.randint(min_mp_bonus, max_mp_bonus))
slot_data["keyblade_stats"] = slot_data["keyblade_stats"] + str(str_bonus) + "," + str(mp_bonus) + ","
slot_data["keyblade_stats"] = slot_data["keyblade_stats"][:-1]
if self.options.donald_death_link:
slot_data["donalddl"] = ""
if self.options.goofy_death_link:
slot_data["goofydl"] = ""
if self.options.keyblades_unlock_chests:
slot_data["chestslocked"] = ""
else:
slot_data["chestsunlocked"] = ""
if self.options.interact_in_battle:
slot_data["interactinbattle"] = ""
return slot_data
def create_item(self, name: str) -> KH1Item:
@@ -355,260 +241,45 @@ class KH1World(World):
set_rules(self)
def create_regions(self):
create_regions(self)
create_regions(self.multiworld, self.player, self.options)
def connect_entrances(self):
connect_entrances(self)
def generate_output(self, output_directory: str):
"""
Generates the json file for use with mod generator.
"""
generate_json(self, output_directory)
connect_entrances(self.multiworld, self.player)
def generate_early(self):
self.determine_level_checks()
value_names = ["Lucky Emblems to Open End of the World", "Lucky Emblems to Open Final Rest Door", "Lucky Emblems in Pool"]
initial_lucky_emblem_settings = [self.options.required_lucky_emblems_eotw.value, self.options.required_lucky_emblems_door.value, self.options.lucky_emblems_in_pool.value]
self.change_numbers_of_lucky_emblems_to_consider()
new_lucky_emblem_settings = [self.options.required_lucky_emblems_eotw.value, self.options.required_lucky_emblems_door.value, self.options.lucky_emblems_in_pool.value]
value_names = ["Reports to Open End of the World", "Reports to Open Final Rest Door", "Reports in Pool"]
initial_report_settings = [self.options.required_reports_eotw.value, self.options.required_reports_door.value, self.options.reports_in_pool.value]
self.change_numbers_of_reports_to_consider()
new_report_settings = [self.options.required_reports_eotw.value, self.options.required_reports_door.value, self.options.reports_in_pool.value]
for i in range(3):
if initial_lucky_emblem_settings[i] != new_lucky_emblem_settings[i]:
logging.info(f"{self.player_name}'s value {initial_lucky_emblem_settings[i]} for \"{value_names[i]}\" was invalid\n"
f"Setting \"{value_names[i]}\" value to {new_lucky_emblem_settings[i]}")
value_names = ["Day 2 Materials", "Homecoming Materials", "Materials in Pool"]
initial_materials_settings = [self.options.day_2_materials.value, self.options.homecoming_materials.value, self.options.materials_in_pool.value]
self.change_numbers_of_materials_to_consider()
new_materials_settings = [self.options.day_2_materials.value, self.options.homecoming_materials.value, self.options.materials_in_pool.value]
for i in range(3):
if initial_materials_settings[i] != new_materials_settings[i]:
logging.info(f"{self.player_name}'s value {initial_materials_settings[i]} for \"{value_names[i]}\" was invalid\n"
f"Setting \"{value_names[i]}\" value to {new_materials_settings[i]}")
if self.options.stacking_world_items.value and not self.options.halloween_town_key_item_bundle.value:
logging.info(f"{self.player_name}'s value {self.options.halloween_town_key_item_bundle.value} for Halloween Town Key Item Bundle must be TRUE when Stacking World Items is on. Setting to TRUE")
self.options.halloween_town_key_item_bundle.value = True
if initial_report_settings[i] != new_report_settings[i]:
logging.info(f"{self.player_name}'s value {initial_report_settings[i]} for \"{value_names[i]}\" was invalid\n"
f"Setting \"{value_names[i]}\" value to {new_report_settings[i]}")
def change_numbers_of_lucky_emblems_to_consider(self) -> None:
if self.options.end_of_the_world_unlock == "lucky_emblems" and self.options.final_rest_door_key == "lucky_emblems":
self.options.required_lucky_emblems_eotw.value, self.options.required_lucky_emblems_door.value, self.options.lucky_emblems_in_pool.value = sorted(
[self.options.required_lucky_emblems_eotw.value, self.options.required_lucky_emblems_door.value, self.options.lucky_emblems_in_pool.value])
def change_numbers_of_reports_to_consider(self) -> None:
if self.options.end_of_the_world_unlock == "reports" and self.options.final_rest_door == "reports":
self.options.required_reports_eotw.value, self.options.required_reports_door.value, self.options.reports_in_pool.value = sorted(
[self.options.required_reports_eotw.value, self.options.required_reports_door.value, self.options.reports_in_pool.value])
elif self.options.end_of_the_world_unlock == "lucky_emblems":
self.options.required_lucky_emblems_eotw.value, self.options.lucky_emblems_in_pool.value = sorted(
[self.options.required_lucky_emblems_eotw.value, self.options.lucky_emblems_in_pool.value])
elif self.options.end_of_the_world_unlock == "reports":
self.options.required_reports_eotw.value, self.options.reports_in_pool.value = sorted(
[self.options.required_reports_eotw.value, self.options.reports_in_pool.value])
elif self.options.final_rest_door_key == "lucky_emblems":
self.options.required_lucky_emblems_door.value, self.options.lucky_emblems_in_pool.value = sorted(
[self.options.required_lucky_emblems_door.value, self.options.lucky_emblems_in_pool.value])
elif self.options.final_rest_door == "reports":
self.options.required_reports_door.value, self.options.reports_in_pool.value = sorted(
[self.options.required_reports_door.value, self.options.reports_in_pool.value])
def determine_lucky_emblems_in_pool(self) -> int:
if self.options.end_of_the_world_unlock == "lucky_emblems" or self.options.final_rest_door_key == "lucky_emblems":
return self.options.lucky_emblems_in_pool.value
def determine_reports_in_pool(self) -> int:
if self.options.end_of_the_world_unlock == "reports" or self.options.final_rest_door == "reports":
return self.options.reports_in_pool.value
return 0
def determine_lucky_emblems_required_to_open_end_of_the_world(self) -> int:
if self.options.end_of_the_world_unlock == "lucky_emblems":
return self.options.required_lucky_emblems_eotw.value
return -1
def determine_reports_required_to_open_end_of_the_world(self) -> int:
if self.options.end_of_the_world_unlock == "reports":
return self.options.required_reports_eotw.value
return 14
def determine_lucky_emblems_required_to_open_final_rest_door(self) -> int:
if self.options.final_rest_door_key == "lucky_emblems":
return self.options.required_lucky_emblems_door.value
return -1
def change_numbers_of_materials_to_consider(self) -> None:
if self.options.destiny_islands:
self.options.day_2_materials.value, self.options.homecoming_materials.value, self.options.materials_in_pool.value = sorted(
[self.options.day_2_materials.value, self.options.homecoming_materials.value, self.options.materials_in_pool.value])
def get_remote_location_ids(self):
remote_location_ids = []
for location in self.multiworld.get_filled_locations(self.player):
if location.name != "Final Ansem":
location_data = location_table[location.name]
if self.options.remote_items.current_key == "full":
if location_data.type != "Starting Accessory":
remote_location_ids.append(location_data.code)
elif self.player == location.item.player and location.item.name != "Victory":
item_data = item_table[location.item.name]
if location_data.type == "Chest":
if item_data.type in ["Stats"]:
remote_location_ids.append(location_data.code)
if location_data.type == "Reward":
if item_data.type in ["Stats"]:
remote_location_ids.append(location_data.code)
if location_data.type == "Static":
if item_data.type not in ["Item"]:
remote_location_ids.append(location_data.code)
if location_data.type == "Level Slot 1":
if item_data.category not in ["Level Up", "Limited Level Up"]:
remote_location_ids.append(location_data.code)
if location_data.type == "Level Slot 2":
if item_data.category not in ["Level Up", "Limited Level Up", "Abilities"]:
remote_location_ids.append(location_data.code)
if location_data.type == "Synth":
if item_data.type not in ["Item"]:
remote_location_ids.append(location_data.code)
if location_data.type == "Prize":
if item_data.type not in ["Item"]:
remote_location_ids.append(location_data.code)
return remote_location_ids
def get_slot_2_levels(self):
if self.slot_2_levels is None:
self.slot_2_levels = []
if self.options.max_level_for_slot_2_level_checks - 1 > self.options.level_checks.value:
logging.info(f"{self.player_name}'s value of {self.options.max_level_for_slot_2_level_checks.value} for max level for slot 2 level checks is invalid as it exceeds their value of {self.options.level_checks.value} for Level Checks\n"
f"Setting max level for slot 2 level checks's value to {self.options.level_checks.value + 1}")
self.options.max_level_for_slot_2_level_checks.value = self.options.level_checks.value + 1
if self.options.slot_2_level_checks.value > self.options.level_checks.value:
logging.info(f"{self.player_name}'s value of {self.options.slot_2_level_checks.value} for slot 2 level checks is invalid as it exceeds their value of {self.options.level_checks.value} for Level Checks\n"
f"Setting slot 2 level check's value to {self.options.level_checks.value}")
self.options.slot_2_level_checks.value = self.options.level_checks.value
if self.options.slot_2_level_checks > self.options.max_level_for_slot_2_level_checks - 1:
logging.info(f"{self.player_name}'s value of {self.options.slot_2_level_checks.value} for slot 2 level checks is invalid as it exceeds their value of {self.options.max_level_for_slot_2_level_checks.value} for Max Level for Slot 2 Level Checks\n"
f"Setting slot 2 level check's value to {self.options.max_level_for_slot_2_level_checks.value - 1}")
self.options.slot_2_level_checks.value = self.options.max_level_for_slot_2_level_checks.value - 1
# Range is exclusive of the top, so if max_level_for_slot_2_level_checks is 2 then the top end of the range needs to be 3 as the only level it can choose is 2.
self.slot_2_levels = self.random.sample(range(2,self.options.max_level_for_slot_2_level_checks.value + 1), self.options.slot_2_level_checks.value)
return self.slot_2_levels
def get_keyblade_stats(self):
# Create keyblade stat array from vanilla
keyblade_stats = [x.copy() for x in VANILLA_KEYBLADE_STATS]
# Handle shuffling keyblade stats
if self.options.keyblade_stats != "vanilla":
if self.options.keyblade_stats == "randomize":
# Fix any minimum and max values from settings
min_str_bonus = min(self.options.keyblade_min_str.value, self.options.keyblade_max_str.value)
max_str_bonus = max(self.options.keyblade_min_str.value, self.options.keyblade_max_str.value)
self.options.keyblade_min_str.value = min_str_bonus
self.options.keyblade_max_str.value = max_str_bonus
min_crit_rate = min(self.options.keyblade_min_crit_rate.value, self.options.keyblade_max_crit_rate.value)
max_crit_rate = max(self.options.keyblade_min_crit_rate.value, self.options.keyblade_max_crit_rate.value)
self.options.keyblade_min_crit_rate.value = min_crit_rate
self.options.keyblade_max_crit_rate.value = max_crit_rate
min_crit_str = min(self.options.keyblade_min_crit_str.value, self.options.keyblade_max_crit_str.value)
max_crit_str = max(self.options.keyblade_min_crit_str.value, self.options.keyblade_max_crit_str.value)
self.options.keyblade_min_crit_str.value = min_crit_str
self.options.keyblade_max_crit_str.value = max_crit_str
min_recoil = min(self.options.keyblade_min_recoil.value, self.options.keyblade_max_recoil.value)
max_recoil = max(self.options.keyblade_min_recoil.value, self.options.keyblade_max_recoil.value)
self.options.keyblade_min_recoil.value = min_recoil
self.options.keyblade_max_recoil.value = max_recoil
min_mp_bonus = min(self.options.keyblade_min_mp.value, self.options.keyblade_max_mp.value)
max_mp_bonus = max(self.options.keyblade_min_mp.value, self.options.keyblade_max_mp.value)
self.options.keyblade_min_mp.value = min_mp_bonus
self.options.keyblade_max_mp.value = max_mp_bonus
if self.options.bad_starting_weapons:
starting_weapons = keyblade_stats[:4]
other_weapons = keyblade_stats[4:]
else:
starting_weapons = []
other_weapons = keyblade_stats
for keyblade in other_weapons:
keyblade["STR"] = self.random.randint(min_str_bonus, max_str_bonus)
keyblade["CRR"] = self.random.randint(min_crit_rate, max_crit_rate)
keyblade["CRB"] = self.random.randint(min_crit_str, max_crit_str)
keyblade["REC"] = self.random.randint(min_recoil, max_recoil)
keyblade["MP"] = self.random.randint(min_mp_bonus, max_mp_bonus)
keyblade_stats = starting_weapons + other_weapons
elif self.options.keyblade_stats == "shuffle":
if self.options.bad_starting_weapons:
starting_weapons = keyblade_stats[:4]
other_weapons = keyblade_stats[4:]
self.random.shuffle(other_weapons)
keyblade_stats = starting_weapons + other_weapons
else:
self.random.shuffle(keyblade_stats)
return keyblade_stats
def determine_level_checks(self):
# Handle if remote_items is off and level_checks > number of stats items
total_level_up_items = min(99,
self.options.strength_increase.value +\
self.options.defense_increase.value +\
self.options.hp_increase.value +\
self.options.mp_increase.value +\
self.options.ap_increase.value +\
self.options.accessory_slot_increase.value +\
self.options.item_slot_increase.value)
if self.options.level_checks.value > total_level_up_items and self.options.remote_items.current_key == "off":
logging.info(f"{self.player_name}'s value {self.options.level_checks.value} for level_checks was changed.\n"
f"This value cannot be more than the number of stat items in the pool when \"remote_items\" is \"off\".\n"
f"Set to be equal to number of stat items in pool, {total_level_up_items}.")
self.options.level_checks.value = total_level_up_items
def get_synthesis_item_name_byte_arrays(self):
# Get synth item names to show in synthesis menu
synthesis_byte_arrays = []
for location in self.multiworld.get_filled_locations(self.player):
if location.name != "Final Ansem":
location_data = location_table[location.name]
if location_data.type == "Synth":
item_name = re.sub('[^A-Za-z0-9 ]+', '',str(location.item.name.replace("Progressive", "Prog")))[:14]
byte_array = []
for character in item_name:
byte_array.append(CHAR_TO_KH[character])
synthesis_byte_arrays.append(byte_array)
return synthesis_byte_arrays
def get_starting_accessory_locations(self):
if self.starting_accessory_locations is None:
if self.options.randomize_party_member_starting_accessories:
self.starting_accessory_locations = list(get_locations_by_type("Starting Accessory").keys())
if not self.options.atlantica:
self.starting_accessory_locations.remove("Ariel Starting Accessory 1")
self.starting_accessory_locations.remove("Ariel Starting Accessory 2")
self.starting_accessory_locations.remove("Ariel Starting Accessory 3")
self.starting_accessory_locations = self.random.sample(self.starting_accessory_locations, 10)
else:
self.starting_accessory_locations = []
return self.starting_accessory_locations
def get_starting_accessories(self):
if self.starting_accessories is None:
if self.options.randomize_party_member_starting_accessories:
self.starting_accessories = list(get_items_by_category("Accessory").keys())
self.starting_accessories = self.random.sample(self.starting_accessories, 10)
else:
self.starting_accessories = []
return self.starting_accessories
def get_ap_costs(self):
if self.ap_costs is None:
ap_costs = VANILLA_ABILITY_AP_COSTS.copy()
if self.options.randomize_ap_costs.current_key == "shuffle":
possible_costs = []
for ap_cost in VANILLA_ABILITY_AP_COSTS:
if ap_cost["Randomize"]:
possible_costs.append(ap_cost["AP Cost"])
self.random.shuffle(possible_costs)
for ap_cost in ap_costs:
if ap_cost["Randomize"]:
ap_cost["AP Cost"] = possible_costs.pop(0)
elif self.options.randomize_ap_costs.current_key == "randomize":
for ap_cost in ap_costs:
if ap_cost["Randomize"]:
ap_cost["AP Cost"] = self.random.randint(self.options.min_ap_cost.value, self.options.max_ap_cost.value)
elif self.options.randomize_ap_costs.current_key == "distribute":
total_ap_value = 0
for ap_cost in VANILLA_ABILITY_AP_COSTS:
if ap_cost["Randomize"]:
total_ap_value = total_ap_value + ap_cost["AP Cost"]
for ap_cost in ap_costs:
if ap_cost["Randomize"]:
total_ap_value = total_ap_value - self.options.min_ap_cost.value
ap_cost["AP Cost"] = self.options.min_ap_cost.value
while total_ap_value > 0:
ap_cost = self.random.choice(ap_costs)
if ap_cost["Randomize"]:
if ap_cost["AP Cost"] < self.options.max_ap_cost.value:
amount_to_add = self.random.randint(1, min(self.options.max_ap_cost.value - ap_cost["AP Cost"], total_ap_value))
ap_cost["AP Cost"] = ap_cost["AP Cost"] + amount_to_add
total_ap_value = total_ap_value - amount_to_add
self.ap_costs = ap_costs
return self.ap_costs
def determine_reports_required_to_open_final_rest_door(self) -> int:
if self.options.final_rest_door == "reports":
return self.options.required_reports_door.value
return 14

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