lufia2ac: coop support + update AP version number to 0.4.2 (#1868)

* Core: typing for async_start

* CommonClient: add a framework for clients to subscribe to data storage key notifications

* Core: update version to 0.4.2

* lufia2ac: coop support
This commit is contained in:
el-u 2023-06-29 15:06:58 +02:00 committed by GitHub
parent d0db728850
commit dfb3df4a8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 218 additions and 78 deletions

View File

@ -191,6 +191,10 @@ class CommonContext:
server_locations: typing.Set[int] # all locations the server knows of, missing_location | checked_locations server_locations: typing.Set[int] # all locations the server knows of, missing_location | checked_locations
locations_info: typing.Dict[int, NetworkItem] locations_info: typing.Dict[int, NetworkItem]
# data storage
stored_data: typing.Dict[str, typing.Any]
stored_data_notification_keys: typing.Set[str]
# internals # internals
# current message box through kvui # current message box through kvui
_messagebox: typing.Optional["kvui.MessageBox"] = None _messagebox: typing.Optional["kvui.MessageBox"] = None
@ -226,6 +230,9 @@ class CommonContext:
self.server_locations = set() # all locations the server knows of, missing_location | checked_locations self.server_locations = set() # all locations the server knows of, missing_location | checked_locations
self.locations_info = {} self.locations_info = {}
self.stored_data = {}
self.stored_data_notification_keys = set()
self.input_queue = asyncio.Queue() self.input_queue = asyncio.Queue()
self.input_requests = 0 self.input_requests = 0
@ -467,6 +474,21 @@ class CommonContext:
for game, game_data in data_package["games"].items(): for game, game_data in data_package["games"].items():
Utils.store_data_package_for_checksum(game, game_data) Utils.store_data_package_for_checksum(game, game_data)
# data storage
def set_notify(self, *keys: str) -> None:
"""Subscribe to be notified of changes to selected data storage keys.
The values can be accessed via the "stored_data" attribute of this context, which is a dictionary mapping the
names of the data storage keys to the latest values received from the server.
"""
if new_keys := (set(keys) - self.stored_data_notification_keys):
self.stored_data_notification_keys.update(new_keys)
async_start(self.send_msgs([{"cmd": "Get",
"keys": list(new_keys)},
{"cmd": "SetNotify",
"keys": list(new_keys)}]))
# DeathLink hooks # DeathLink hooks
def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None: def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None:
@ -737,6 +759,11 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
if ctx.locations_scouted: if ctx.locations_scouted:
msgs.append({"cmd": "LocationScouts", msgs.append({"cmd": "LocationScouts",
"locations": list(ctx.locations_scouted)}) "locations": list(ctx.locations_scouted)})
if ctx.stored_data_notification_keys:
msgs.append({"cmd": "Get",
"keys": list(ctx.stored_data_notification_keys)})
msgs.append({"cmd": "SetNotify",
"keys": list(ctx.stored_data_notification_keys)})
if msgs: if msgs:
await ctx.send_msgs(msgs) await ctx.send_msgs(msgs)
if ctx.finished_game: if ctx.finished_game:
@ -800,7 +827,12 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
# we can skip checking "DeathLink" in ctx.tags, as otherwise we wouldn't have been send this # we can skip checking "DeathLink" in ctx.tags, as otherwise we wouldn't have been send this
if "DeathLink" in tags and ctx.last_death_link != args["data"]["time"]: if "DeathLink" in tags and ctx.last_death_link != args["data"]["time"]:
ctx.on_deathlink(args["data"]) ctx.on_deathlink(args["data"])
elif cmd == "Retrieved":
ctx.stored_data.update(args["keys"])
elif cmd == "SetReply": elif cmd == "SetReply":
ctx.stored_data[args["key"]] = args["value"]
if args["key"] == "EnergyLink": if args["key"] == "EnergyLink":
ctx.current_energy_link_value = args["value"] ctx.current_energy_link_value = args["value"]
if ctx.ui: if ctx.ui:

View File

@ -42,7 +42,7 @@ class Version(typing.NamedTuple):
return ".".join(str(item) for item in self) return ".".join(str(item) for item in self)
__version__ = "0.4.1" __version__ = "0.4.2"
version_tuple = tuplize_version(__version__) version_tuple = tuplize_version(__version__)
is_linux = sys.platform.startswith("linux") is_linux = sys.platform.startswith("linux")
@ -766,10 +766,10 @@ def read_snes_rom(stream: BinaryIO, strip_header: bool = True) -> bytearray:
return buffer return buffer
_faf_tasks: "Set[asyncio.Task[None]]" = set() _faf_tasks: "Set[asyncio.Task[typing.Any]]" = set()
def async_start(co: Coroutine[typing.Any, typing.Any, bool], name: Optional[str] = None) -> None: def async_start(co: Coroutine[None, None, typing.Any], name: Optional[str] = None) -> None:
""" """
Use this to start a task when you don't keep a reference to it or immediately await it, Use this to start a task when you don't keep a reference to it or immediately await it,
to prevent early garbage collection. "fire-and-forget" to prevent early garbage collection. "fire-and-forget"
@ -782,7 +782,7 @@ def async_start(co: Coroutine[typing.Any, typing.Any, bool], name: Optional[str]
# ``` # ```
# This implementation follows the pattern given in that documentation. # This implementation follows the pattern given in that documentation.
task = asyncio.create_task(co, name=name) task: asyncio.Task[typing.Any] = asyncio.create_task(co, name=name)
_faf_tasks.add(task) _faf_tasks.add(task)
task.add_done_callback(_faf_tasks.discard) task.add_done_callback(_faf_tasks.discard)

View File

@ -1,14 +1,16 @@
import logging import logging
import time import time
import typing import typing
import uuid
from logging import Logger from logging import Logger
from typing import Optional from typing import Dict, List, Optional
from NetUtils import ClientStatus, NetworkItem from NetUtils import ClientStatus, NetworkItem
from worlds.AutoSNIClient import SNIClient from worlds.AutoSNIClient import SNIClient
from .Enemies import enemy_id_to_name from .Enemies import enemy_id_to_name
from .Items import start_id as items_start_id from .Items import start_id as items_start_id
from .Locations import start_id as locations_start_id from .Locations import start_id as locations_start_id
from .Options import BlueChestCount
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from SNIClient import SNIContext from SNIClient import SNIContext
@ -59,6 +61,18 @@ class L2ACSNIClient(SNIClient):
if signature != b"ArchipelagoLufia": if signature != b"ArchipelagoLufia":
return return
uuid_data: Optional[bytes] = await snes_read(ctx, L2AC_TX_ADDR + 16, 16)
if uuid_data is None:
return
coop_uuid: uuid.UUID = uuid.UUID(bytes=uuid_data)
if coop_uuid.version != 4:
coop_uuid = uuid.uuid4()
snes_buffered_write(ctx, L2AC_TX_ADDR + 16, coop_uuid.bytes)
blue_chests_key: str = f"lufia2ac_blue_chests_checked_T{ctx.team}_P{ctx.slot}"
ctx.set_notify(blue_chests_key)
# Goal # Goal
if not ctx.finished_game: if not ctx.finished_game:
goal_data: Optional[bytes] = await snes_read(ctx, L2AC_GOAL_ADDR, 10) goal_data: Optional[bytes] = await snes_read(ctx, L2AC_GOAL_ADDR, 10)
@ -78,29 +92,47 @@ class L2ACSNIClient(SNIClient):
await ctx.send_death(f"{player_name} was totally defeated by {enemy_name}.") await ctx.send_death(f"{player_name} was totally defeated by {enemy_name}.")
# TX # TX
tx_data: Optional[bytes] = await snes_read(ctx, L2AC_TX_ADDR, 8) tx_data: Optional[bytes] = await snes_read(ctx, L2AC_TX_ADDR, 12)
if tx_data is not None: if tx_data is not None:
snes_items_sent = int.from_bytes(tx_data[:2], "little") snes_blue_chests_checked: int = int.from_bytes(tx_data[:2], "little")
client_items_sent = int.from_bytes(tx_data[2:4], "little") snes_ap_items_found: int = int.from_bytes(tx_data[6:8], "little")
client_ap_items_found = int.from_bytes(tx_data[4:6], "little") snes_other_locations_checked: int = int.from_bytes(tx_data[10:12], "little")
if client_items_sent < snes_items_sent: blue_chests_checked: Dict[str, int] = ctx.stored_data.get(blue_chests_key) or {}
location_id: int = locations_start_id + client_items_sent if blue_chests_checked.get(str(coop_uuid), 0) < snes_blue_chests_checked:
location: str = ctx.location_names[location_id] blue_chests_checked[str(coop_uuid)] = snes_blue_chests_checked
client_items_sent += 1 if blue_chests_key in ctx.stored_data:
await ctx.send_msgs([{
"cmd": "Set",
"key": blue_chests_key,
"default": {},
"want_reply": True,
"operations": [{
"operation": "update",
"value": {str(coop_uuid): snes_blue_chests_checked},
}],
}])
total_blue_chests_checked: int = min(sum(blue_chests_checked.values()), BlueChestCount.range_end)
snes_buffered_write(ctx, L2AC_TX_ADDR + 8, total_blue_chests_checked.to_bytes(2, "little"))
location_ids: List[int] = [locations_start_id + i for i in range(total_blue_chests_checked)]
loc_data: Optional[bytes] = await snes_read(ctx, L2AC_TX_ADDR + 32, snes_other_locations_checked * 2)
if loc_data is not None:
location_ids.extend(locations_start_id + int.from_bytes(loc_data[2 * i:2 * i + 2], "little")
for i in range(snes_other_locations_checked))
if new_location_ids := [loc_id for loc_id in location_ids if loc_id not in ctx.locations_checked]:
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": new_location_ids}])
for location_id in new_location_ids:
ctx.locations_checked.add(location_id) ctx.locations_checked.add(location_id)
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": [location_id]}]) snes_logger.info("%d/%d blue chests" % (
len(list(loc for loc in ctx.locations_checked if not loc & 0x100)),
len(list(loc for loc in ctx.missing_locations | ctx.checked_locations if not loc & 0x100))))
snes_logger.info("New Check: %s (%d/%d)" % ( client_ap_items_found: int = sum(net_item.player != ctx.slot for net_item in ctx.locations_info.values())
location, if client_ap_items_found > snes_ap_items_found:
len(ctx.locations_checked), snes_buffered_write(ctx, L2AC_TX_ADDR + 4, client_ap_items_found.to_bytes(2, "little"))
len(ctx.missing_locations) + len(ctx.checked_locations)))
snes_buffered_write(ctx, L2AC_TX_ADDR + 2, client_items_sent.to_bytes(2, "little"))
ap_items_found: int = sum(net_item.player != ctx.slot for net_item in ctx.locations_info.values())
if client_ap_items_found < ap_items_found:
snes_buffered_write(ctx, L2AC_TX_ADDR + 4, ap_items_found.to_bytes(2, "little"))
# RX # RX
rx_data: Optional[bytes] = await snes_read(ctx, L2AC_RX_ADDR, 4) rx_data: Optional[bytes] = await snes_read(ctx, L2AC_RX_ADDR, 4)

View File

@ -9,9 +9,11 @@ start_id: int = Locations.start_id
class ItemType(Enum): class ItemType(Enum):
BLUE_CHEST = auto() BLUE_CHEST = auto()
BOSS = auto()
CAPSULE_MONSTER = auto() CAPSULE_MONSTER = auto()
ENEMY_DROP = auto() ENEMY_DROP = auto()
ENTRANCE_CHEST = auto() ENTRANCE_CHEST = auto()
IRIS_TREASURE = auto()
PARTY_MEMBER = auto() PARTY_MEMBER = auto()
RED_CHEST = auto() RED_CHEST = auto()
RED_CHEST_PATCH = auto() RED_CHEST_PATCH = auto()
@ -451,15 +453,15 @@ l2ac_item_table: Dict[str, ItemData] = {
# 0x0199: "Bunnysuit" # 0x0199: "Bunnysuit"
# 0x019A: "Seethru cape" # 0x019A: "Seethru cape"
# 0x019B: "Seethru silk" # 0x019B: "Seethru silk"
# 0x019C: "Iris sword" "Iris sword": ItemData(0x039C, ItemType.IRIS_TREASURE, ItemClassification.progression_skip_balancing),
# 0x019D: "Iris shield" "Iris shield": ItemData(0x039D, ItemType.IRIS_TREASURE, ItemClassification.progression_skip_balancing),
# 0x019E: "Iris helmet" "Iris helmet": ItemData(0x039E, ItemType.IRIS_TREASURE, ItemClassification.progression_skip_balancing),
# 0x019F: "Iris armor" "Iris armor": ItemData(0x039F, ItemType.IRIS_TREASURE, ItemClassification.progression_skip_balancing),
# 0x01A0: "Iris ring" "Iris ring": ItemData(0x03A0, ItemType.IRIS_TREASURE, ItemClassification.progression_skip_balancing),
# 0x01A1: "Iris jewel" "Iris jewel": ItemData(0x03A1, ItemType.IRIS_TREASURE, ItemClassification.progression_skip_balancing),
# 0x01A2: "Iris staff" "Iris staff": ItemData(0x03A2, ItemType.IRIS_TREASURE, ItemClassification.progression_skip_balancing),
# 0x01A3: "Iris pot" "Iris pot": ItemData(0x03A3, ItemType.IRIS_TREASURE, ItemClassification.progression_skip_balancing),
# 0x01A4: "Iris tiara" "Iris tiara": ItemData(0x03A4, ItemType.IRIS_TREASURE, ItemClassification.progression_skip_balancing),
# 0x01A5: "Power jelly" # 0x01A5: "Power jelly"
# 0x01A6: "Jewel sonar" # 0x01A6: "Jewel sonar"
# 0x01A7: "Hook" # 0x01A7: "Hook"
@ -489,7 +491,7 @@ l2ac_item_table: Dict[str, ItemData] = {
# 0x01BF: "Truth key" # 0x01BF: "Truth key"
# 0x01C0: "Mermaid jade" # 0x01C0: "Mermaid jade"
# 0x01C1: "Engine" # 0x01C1: "Engine"
# 0x01C2: "Ancient key" "Ancient key": ItemData(0x01C2, ItemType.BOSS, ItemClassification.progression_skip_balancing),
# 0x01C3: "Pretty flwr." # 0x01C3: "Pretty flwr."
# 0x01C4: "Glass angel" # 0x01C4: "Glass angel"
# 0x01C5: "VIP card" # 0x01C5: "VIP card"

View File

@ -1,9 +1,15 @@
from typing import Dict from typing import Dict
from BaseClasses import Location from BaseClasses import Location
from .Options import BlueChestCount
start_id: int = 0xAC0000 start_id: int = 0xAC0000
l2ac_location_name_to_id: Dict[str, int] = {f"Blue chest {i + 1}": (start_id + i) for i in range(88)}
l2ac_location_name_to_id: Dict[str, int] = {
**{f"Blue chest {i + 1}": (start_id + i) for i in range(BlueChestCount.range_end + 7 + 6)},
**{f"Iris treasure {i + 1}": (start_id + 0x039C + i) for i in range(9)},
"Boss": start_id + 0x01C2,
}
class L2ACLocation(Location): class L2ACLocation(Location):

View File

@ -109,13 +109,13 @@ class BlueChestCount(Range):
more for each party member or capsule monster if you have shuffle_party_members/shuffle_capsule_monsters enabled. more for each party member or capsule monster if you have shuffle_party_members/shuffle_capsule_monsters enabled.
(You will still encounter blue chests in your world after all the multiworld location checks have been exhausted, (You will still encounter blue chests in your world after all the multiworld location checks have been exhausted,
but these chests will then generate items for yourself only.) but these chests will then generate items for yourself only.)
Supported values: 10 75 Supported values: 10 100
Default value: 25 Default value: 25
""" """
display_name = "Blue chest count" display_name = "Blue chest count"
range_start = 10 range_start = 10
range_end = 75 range_end = 100
default = 25 default = 25

View File

@ -5,7 +5,7 @@ from enum import IntFlag
from random import Random from random import Random
from typing import Any, ClassVar, Dict, get_type_hints, Iterator, List, Set, Tuple from typing import Any, ClassVar, Dict, get_type_hints, Iterator, List, Set, Tuple
from BaseClasses import Entrance, Item, ItemClassification, MultiWorld, Region, Tutorial from BaseClasses import Entrance, Item, ItemClassification, Location, MultiWorld, Region, Tutorial
from Options import AssembleOptions from Options import AssembleOptions
from Utils import __version__ from Utils import __version__
from worlds.AutoWorld import WebWorld, World from worlds.AutoWorld import WebWorld, World
@ -50,10 +50,11 @@ class L2ACWorld(World):
item_name_groups: ClassVar[Dict[str, Set[str]]] = { item_name_groups: ClassVar[Dict[str, Set[str]]] = {
"Blue chest items": {name for name, data in l2ac_item_table.items() if data.type is ItemType.BLUE_CHEST}, "Blue chest items": {name for name, data in l2ac_item_table.items() if data.type is ItemType.BLUE_CHEST},
"Capsule monsters": {name for name, data in l2ac_item_table.items() if data.type is ItemType.CAPSULE_MONSTER}, "Capsule monsters": {name for name, data in l2ac_item_table.items() if data.type is ItemType.CAPSULE_MONSTER},
"Iris treasures": {name for name, data in l2ac_item_table.items() if data.type is ItemType.IRIS_TREASURE},
"Party members": {name for name, data in l2ac_item_table.items() if data.type is ItemType.PARTY_MEMBER}, "Party members": {name for name, data in l2ac_item_table.items() if data.type is ItemType.PARTY_MEMBER},
} }
data_version: ClassVar[int] = 1 data_version: ClassVar[int] = 2
required_client_version: Tuple[int, int, int] = (0, 3, 6) required_client_version: Tuple[int, int, int] = (0, 4, 2)
# L2ACWorld specific properties # L2ACWorld specific properties
rom_name: bytearray rom_name: bytearray
@ -107,17 +108,20 @@ class L2ACWorld(World):
L2ACLocation(self.player, f"Chest access {i + 1}-{i + CHESTS_PER_SPHERE}", None, ancient_dungeon) L2ACLocation(self.player, f"Chest access {i + 1}-{i + CHESTS_PER_SPHERE}", None, ancient_dungeon)
chest_access.place_locked_item(prog_chest_access) chest_access.place_locked_item(prog_chest_access)
ancient_dungeon.locations.append(chest_access) ancient_dungeon.locations.append(chest_access)
treasures = L2ACLocation(self.player, "Iris Treasures", None, ancient_dungeon) for iris in self.item_name_groups["Iris treasures"]:
treasures.place_locked_item(L2ACItem("Treasures collected", ItemClassification.progression, None, self.player)) treasure_name: str = f"Iris treasure {self.item_name_to_id[iris] - self.item_name_to_id['Iris sword'] + 1}"
ancient_dungeon.locations.append(treasures) iris_treasure: Location = \
L2ACLocation(self.player, treasure_name, self.location_name_to_id[treasure_name], ancient_dungeon)
iris_treasure.place_locked_item(self.create_item(iris))
ancient_dungeon.locations.append(iris_treasure)
self.multiworld.regions.append(ancient_dungeon) self.multiworld.regions.append(ancient_dungeon)
final_floor = Region("FinalFloor", self.player, self.multiworld, "Ancient Cave Final Floor") final_floor = Region("FinalFloor", self.player, self.multiworld, "Ancient Cave Final Floor")
ff_reached = L2ACLocation(self.player, "Final Floor reached", None, final_floor) ff_reached = L2ACLocation(self.player, "Final Floor reached", None, final_floor)
ff_reached.place_locked_item(L2ACItem("Final Floor access", ItemClassification.progression, None, self.player)) ff_reached.place_locked_item(L2ACItem("Final Floor access", ItemClassification.progression, None, self.player))
final_floor.locations.append(ff_reached) final_floor.locations.append(ff_reached)
boss = L2ACLocation(self.player, "Boss", None, final_floor) boss: Location = L2ACLocation(self.player, "Boss", self.location_name_to_id["Boss"], final_floor)
boss.place_locked_item(L2ACItem("Boss victory", ItemClassification.progression, None, self.player)) boss.place_locked_item(self.create_item("Ancient key"))
final_floor.locations.append(boss) final_floor.locations.append(boss)
self.multiworld.regions.append(final_floor) self.multiworld.regions.append(final_floor)
@ -155,8 +159,9 @@ class L2ACWorld(World):
set_rule(self.multiworld.get_entrance("FinalFloorEntrance", self.player), set_rule(self.multiworld.get_entrance("FinalFloorEntrance", self.player),
lambda state: state.can_reach(f"Blue chest {self.o.blue_chest_count}", "Location", self.player)) lambda state: state.can_reach(f"Blue chest {self.o.blue_chest_count}", "Location", self.player))
set_rule(self.multiworld.get_location("Iris Treasures", self.player), for i in range(9):
lambda state: state.can_reach(f"Blue chest {self.o.blue_chest_count}", "Location", self.player)) set_rule(self.multiworld.get_location(f"Iris treasure {i + 1}", self.player),
lambda state: state.can_reach(f"Blue chest {self.o.blue_chest_count}", "Location", self.player))
set_rule(self.multiworld.get_location("Boss", self.player), set_rule(self.multiworld.get_location("Boss", self.player),
lambda state: state.can_reach(f"Blue chest {self.o.blue_chest_count}", "Location", self.player)) lambda state: state.can_reach(f"Blue chest {self.o.blue_chest_count}", "Location", self.player))
if self.o.shuffle_capsule_monsters: if self.o.shuffle_capsule_monsters:
@ -170,13 +175,14 @@ class L2ACWorld(World):
lambda state: state.has("Final Floor access", self.player) lambda state: state.has("Final Floor access", self.player)
elif self.o.goal == Goal.option_iris_treasure_hunt: elif self.o.goal == Goal.option_iris_treasure_hunt:
self.multiworld.completion_condition[self.player] = \ self.multiworld.completion_condition[self.player] = \
lambda state: state.has("Treasures collected", self.player) lambda state: state.has_group("Iris treasures", self.player, int(self.o.iris_treasures_required))
elif self.o.goal == Goal.option_boss: elif self.o.goal == Goal.option_boss:
self.multiworld.completion_condition[self.player] = \ self.multiworld.completion_condition[self.player] = \
lambda state: state.has("Boss victory", self.player) lambda state: state.has("Ancient key", self.player)
elif self.o.goal == Goal.option_boss_iris_treasure_hunt: elif self.o.goal == Goal.option_boss_iris_treasure_hunt:
self.multiworld.completion_condition[self.player] = \ self.multiworld.completion_condition[self.player] = \
lambda state: state.has("Boss victory", self.player) and state.has("Treasures collected", self.player) lambda state: (state.has("Ancient key", self.player) and
state.has_group("Iris treasures", self.player, int(self.o.iris_treasures_required)))
def generate_output(self, output_directory: str) -> None: def generate_output(self, output_directory: str) -> None:
rom_path: str = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.sfc") rom_path: str = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.sfc")

View File

@ -45,8 +45,8 @@ org $8EA721 ; skip master fight dialogue
DB $1C,$45,$01 ; L2SASM JMP $8EA5FA+$0145 DB $1C,$45,$01 ; L2SASM JMP $8EA5FA+$0145
org $8EA74B ; skip master victory dialogue org $8EA74B ; skip master victory dialogue
DB $1C,$AC,$01 ; L2SASM JMP $8EA5FA+$01AC DB $1C,$AC,$01 ; L2SASM JMP $8EA5FA+$01AC
org $8EA7AA ; skip master key dialogue org $8EA7AA ; skip master key dialogue and animation
DB $1C,$CA,$01 ; L2SASM JMP $8EA5FA+$01CA DB $1C,$EE,$01 ; L2SASM JMP $8EA5FA+$01EE
org $8EA7F4 ; skip master goodbye dialogue org $8EA7F4 ; skip master goodbye dialogue
DB $1C,$05,$02 ; L2SASM JMP $8EA5FA+$0205 DB $1C,$05,$02 ; L2SASM JMP $8EA5FA+$0205
org $8EA807 ; skip master not fight dialogue org $8EA807 ; skip master not fight dialogue
@ -126,7 +126,7 @@ Init:
; transmit checks ; transmit checks from chests
pushpc pushpc
org $8EC1EB org $8EC1EB
JML TX ; overwrites JSL $83F559 JML TX ; overwrites JSL $83F559
@ -136,11 +136,17 @@ TX:
JSL $83F559 ; (overwritten instruction) chest opening animation JSL $83F559 ; (overwritten instruction) chest opening animation
REP #$20 REP #$20
LDA $7FD4EF ; read chest item ID LDA $7FD4EF ; read chest item ID
BIT.w #$4000 ; test for blue chest flag BIT.w #$0200 ; test for iris item flag
BEQ + BEQ +
LDA $F02040 ; load check counter JSR ReportLocationCheck
SEP #$20
JML $8EC331 ; skip item get process
+: BIT.w #$4200 ; test for blue chest flag
BEQ +
LDA $F02048 ; load total blue chests checked
CMP $D08010 ; compare against max AP item number CMP $D08010 ; compare against max AP item number
BPL + BPL +
LDA $F02040 ; load check counter
INC ; increment check counter INC ; increment check counter
STA $F02040 ; store check counter STA $F02040 ; store check counter
SEP #$20 SEP #$20
@ -150,6 +156,41 @@ TX:
; transmit checks from script events
pushpc
org $80A435
; DB=$8E, x=0, m=1
JML ScriptTX ; overwrites STA $7FD4F1
pullpc
ScriptTX:
STA $7FD4F1 ; (overwritten instruction)
REP #$20
LDA $7FD4EF ; read script item id
CMP.w #$01C2 ; test for ancient key
BNE +
JSR ReportLocationCheck
SEP #$20
JML $80A47F ; skip item get process
+: SEP #$20
JML $80A439 ; continue item get process
ReportLocationCheck:
PHA ; remember item id
LDA $F0204A ; load other locations count
INC ; increment check counter
STA $F0204A ; store other locations count
DEC
ASL
TAX
PLA
STA $F02060,X ; store item id in checked locations list
RTS
; report event flag based goal completion ; report event flag based goal completion
pushpc pushpc
org $D09000 org $D09000
@ -173,9 +214,9 @@ pullpc
Goal: Goal:
TDC TDC
LDA $0797 ; load some event flags (iris sword, iris shield, ..., iris pot) LDA $0797 ; load EV flags $C8-$CF (iris sword, iris shield, ..., iris pot)
TAX TAX
LDA $0798 ; load some event flags (iris tiara, boss, others...) LDA $0798 ; load EV flags $D0-$D7 (iris tiara, boss, others...)
TAY TAY
AND.b #$02 ; test boss victory AND.b #$02 ; test boss victory
LSR LSR
@ -223,16 +264,32 @@ RX:
SpecialItemGet: SpecialItemGet:
BPL + ; spells have high bit set BPL + ; spells have high bit set
JSR LearnSpell JSR LearnSpell
+: BIT.w #$0200 ; iris items
BEQ +
SEC
SBC.w #$039C
ASL
TAX
LDA $8ED8C3,X ; load predefined bitmask with a single bit set
ORA $0797
STA $0797 ; set iris item EV flag ($C8-$D0)
BRA ++
+: CMP.w #$01C2 ; ancient key
BNE +
LDA.w #$0200
ORA $0797
STA $0797 ; set boss item EV flag ($D1)
BRA ++
+: CMP.w #$01BF ; capsule monster items range from $01B8 to $01BE +: CMP.w #$01BF ; capsule monster items range from $01B8 to $01BE
BPL + BPL ++
SBC.w #$01B1 ; party member items range from $01B2 to $01B7 SBC.w #$01B1 ; party member items range from $01B2 to $01B7
BMI + BMI ++
ASL ASL
TAX TAX
LDA $8ED8C7,X ; load predefined bitmask with a single bit set LDA $8ED8C7,X ; load predefined bitmask with a single bit set
ORA $F02018 ; set unlock bit for party member/capsule monster ORA $F02018 ; set unlock bit for party member/capsule monster
STA $F02018 STA $F02018
+: RTS ++: RTS
LearnSpell: LearnSpell:
STA $0A0B STA $0A0B
@ -634,7 +691,7 @@ StartInventory:
PHX PHX
JSR LearnSpell JSR LearnSpell
PLX PLX
+: BIT.w #$C000 ; ignore blue chest items (and spells) +: BIT.w #$C200 ; ignore spells, blue chest items, and iris items
BNE + BNE +
PHX PHX
STA $09CF ; specify item ID STA $09CF ; specify item ID
@ -1025,12 +1082,16 @@ pullpc
; $F0203D 1 death link enabled ; $F0203D 1 death link enabled
; $F0203E 1 death link sent (monster id + 1) ; $F0203E 1 death link sent (monster id + 1)
; $F0203F 1 death link received ; $F0203F 1 death link received
; $F02040 2 check counter (snes_items_sent) ; $F02040 2 check counter for this save file (snes_blue_chests_checked)
; $F02042 2 check counter (client_items_sent) ; $F02042 2 RESERVED
; $F02044 2 check counter (client_ap_items_found) ; $F02044 2 check counter (client_ap_items_found)
; $F02046 2 check counter (snes_ap_items_found) ; $F02046 2 check counter (snes_ap_items_found)
; $F02048 2 check counter for the slot (total_blue_chests_checked)
; $F0204A 2 check counter for this save file (snes_other_locations_checked)
; $F02050 16 coop uuid
; $F02060 var list of checked locations
; $F027E0 16 saved RX counters ; $F027E0 16 saved RX counters
; $F02800 2 received counter ; $F02800 2 received counter
; $F02802 2 processed counter ; $F02802 2 processed counter
; $F02804 inf list of received items ; $F02804 var list of received items
; $F06000 inf architect mode RNG state backups ; $F06000 var architect mode RNG state backups

View File

@ -39,9 +39,9 @@ Your Party Leader will hold up the item they received when not in a fight or in
###### Customization options: ###### Customization options:
- Choose a goal for your world. Possible goals are: 1) Reach the final floor; 2) Defeat the boss on the final floor; 3) - Choose a goal for your world. Possible goals are: 1) Reach the final floor; 2) Defeat the boss on the final floor; 3)
Retrieve a (customizable) number of iris treasures from the cave; 4) Retrieve the iris treasures *and* defeat the boss Retrieve a (customizable) number of Iris treasures from the cave; 4) Retrieve the Iris treasures *and* defeat the boss
- You can also randomize the goal; The blue-haired NPC in front of the cafe can tell you about the selected objective - You can also randomize the goal; The blue-haired NPC in front of the cafe can tell you about the selected objective
- Customize the chances of encountering blue chests, healing tiles, iris treasures, etc. - Customize the chances of encountering blue chests, healing tiles, Iris treasures, etc.
- Customize the default party lineup and capsule monster - Customize the default party lineup and capsule monster
- Customize the party starting level as well as capsule monster level and form - Customize the party starting level as well as capsule monster level and form
- Customize the initial and final floor numbers - Customize the initial and final floor numbers
@ -61,6 +61,7 @@ Your Party Leader will hold up the item they received when not in a fight or in
- You can elect to lock the cave layout for the next run, giving you exactly the same floors and red chest contents as - You can elect to lock the cave layout for the next run, giving you exactly the same floors and red chest contents as
on your previous attempt. This functionality is accessed via the bald NPC behind the counter at the Ancient Cave on your previous attempt. This functionality is accessed via the bald NPC behind the counter at the Ancient Cave
Entrance Entrance
- Multiple people can connect to the same slot and collaboratively search for Iris treasures and blue chests
- Always start with Providence already in your inventory. (It is no longer obtained from red chests) - Always start with Providence already in your inventory. (It is no longer obtained from red chests)
- (optional) Run button that allows you to move at faster than normal speed - (optional) Run button that allows you to move at faster than normal speed

View File

@ -4,7 +4,7 @@ from . import L2ACTestBase
class TestDefault(L2ACTestBase): class TestDefault(L2ACTestBase):
def test_everything(self) -> None: def test_everything(self) -> None:
self.collect_all_but(["Boss victory"]) self.collect_all_but(["Ancient key"])
self.assertBeatable(True) self.assertBeatable(True)
def test_nothing(self) -> None: def test_nothing(self) -> None:
@ -17,7 +17,7 @@ class TestShuffleCapsuleMonsters(L2ACTestBase):
} }
def test_everything(self) -> None: def test_everything(self) -> None:
self.collect_all_but(["Boss victory"]) self.collect_all_but(["Ancient key"])
self.assertBeatable(True) self.assertBeatable(True)
def test_best_party(self) -> None: def test_best_party(self) -> None:
@ -25,7 +25,7 @@ class TestShuffleCapsuleMonsters(L2ACTestBase):
self.assertBeatable(True) self.assertBeatable(True)
def test_no_darbi(self) -> None: def test_no_darbi(self) -> None:
self.collect_all_but(["Boss victory", "DARBI"]) self.collect_all_but(["Ancient key", "DARBI"])
self.assertBeatable(False) self.assertBeatable(False)
@ -35,7 +35,7 @@ class TestShufflePartyMembers(L2ACTestBase):
} }
def test_everything(self) -> None: def test_everything(self) -> None:
self.collect_all_but(["Boss victory"]) self.collect_all_but(["Ancient key"])
self.assertBeatable(True) self.assertBeatable(True)
def test_best_party(self) -> None: def test_best_party(self) -> None:
@ -43,15 +43,15 @@ class TestShufflePartyMembers(L2ACTestBase):
self.assertBeatable(True) self.assertBeatable(True)
def test_no_dekar(self) -> None: def test_no_dekar(self) -> None:
self.collect_all_but(["Boss victory", "Dekar"]) self.collect_all_but(["Ancient key", "Dekar"])
self.assertBeatable(False) self.assertBeatable(False)
def test_no_guy(self) -> None: def test_no_guy(self) -> None:
self.collect_all_but(["Boss victory", "Guy"]) self.collect_all_but(["Ancient key", "Guy"])
self.assertBeatable(False) self.assertBeatable(False)
def test_no_arty(self) -> None: def test_no_arty(self) -> None:
self.collect_all_but(["Boss victory", "Arty"]) self.collect_all_but(["Ancient key", "Arty"])
self.assertBeatable(False) self.assertBeatable(False)
@ -62,7 +62,7 @@ class TestShuffleBoth(L2ACTestBase):
} }
def test_everything(self) -> None: def test_everything(self) -> None:
self.collect_all_but(["Boss victory"]) self.collect_all_but(["Ancient key"])
self.assertBeatable(True) self.assertBeatable(True)
def test_best_party(self) -> None: def test_best_party(self) -> None:
@ -70,17 +70,17 @@ class TestShuffleBoth(L2ACTestBase):
self.assertBeatable(True) self.assertBeatable(True)
def test_no_dekar(self) -> None: def test_no_dekar(self) -> None:
self.collect_all_but(["Boss victory", "Dekar"]) self.collect_all_but(["Ancient key", "Dekar"])
self.assertBeatable(False) self.assertBeatable(False)
def test_no_guy(self) -> None: def test_no_guy(self) -> None:
self.collect_all_but(["Boss victory", "Guy"]) self.collect_all_but(["Ancient key", "Guy"])
self.assertBeatable(False) self.assertBeatable(False)
def test_no_arty(self) -> None: def test_no_arty(self) -> None:
self.collect_all_but(["Boss victory", "Arty"]) self.collect_all_but(["Ancient key", "Arty"])
self.assertBeatable(False) self.assertBeatable(False)
def test_no_darbi(self) -> None: def test_no_darbi(self) -> None:
self.collect_all_but(["Boss victory", "DARBI"]) self.collect_all_but(["Ancient key", "DARBI"])
self.assertBeatable(False) self.assertBeatable(False)