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:
parent
d0db728850
commit
dfb3df4a8f
|
@ -191,6 +191,10 @@ class CommonContext:
|
|||
server_locations: typing.Set[int] # all locations the server knows of, missing_location | checked_locations
|
||||
locations_info: typing.Dict[int, NetworkItem]
|
||||
|
||||
# data storage
|
||||
stored_data: typing.Dict[str, typing.Any]
|
||||
stored_data_notification_keys: typing.Set[str]
|
||||
|
||||
# internals
|
||||
# current message box through kvui
|
||||
_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.locations_info = {}
|
||||
|
||||
self.stored_data = {}
|
||||
self.stored_data_notification_keys = set()
|
||||
|
||||
self.input_queue = asyncio.Queue()
|
||||
self.input_requests = 0
|
||||
|
||||
|
@ -467,6 +474,21 @@ class CommonContext:
|
|||
for game, game_data in data_package["games"].items():
|
||||
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
|
||||
|
||||
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:
|
||||
msgs.append({"cmd": "LocationScouts",
|
||||
"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:
|
||||
await ctx.send_msgs(msgs)
|
||||
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
|
||||
if "DeathLink" in tags and ctx.last_death_link != args["data"]["time"]:
|
||||
ctx.on_deathlink(args["data"])
|
||||
|
||||
elif cmd == "Retrieved":
|
||||
ctx.stored_data.update(args["keys"])
|
||||
|
||||
elif cmd == "SetReply":
|
||||
ctx.stored_data[args["key"]] = args["value"]
|
||||
if args["key"] == "EnergyLink":
|
||||
ctx.current_energy_link_value = args["value"]
|
||||
if ctx.ui:
|
||||
|
|
8
Utils.py
8
Utils.py
|
@ -42,7 +42,7 @@ class Version(typing.NamedTuple):
|
|||
return ".".join(str(item) for item in self)
|
||||
|
||||
|
||||
__version__ = "0.4.1"
|
||||
__version__ = "0.4.2"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
|
||||
is_linux = sys.platform.startswith("linux")
|
||||
|
@ -766,10 +766,10 @@ def read_snes_rom(stream: BinaryIO, strip_header: bool = True) -> bytearray:
|
|||
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,
|
||||
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.
|
||||
|
||||
task = asyncio.create_task(co, name=name)
|
||||
task: asyncio.Task[typing.Any] = asyncio.create_task(co, name=name)
|
||||
_faf_tasks.add(task)
|
||||
task.add_done_callback(_faf_tasks.discard)
|
||||
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
import logging
|
||||
import time
|
||||
import typing
|
||||
import uuid
|
||||
from logging import Logger
|
||||
from typing import Optional
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from NetUtils import ClientStatus, NetworkItem
|
||||
from worlds.AutoSNIClient import SNIClient
|
||||
from .Enemies import enemy_id_to_name
|
||||
from .Items import start_id as items_start_id
|
||||
from .Locations import start_id as locations_start_id
|
||||
from .Options import BlueChestCount
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from SNIClient import SNIContext
|
||||
|
@ -59,6 +61,18 @@ class L2ACSNIClient(SNIClient):
|
|||
if signature != b"ArchipelagoLufia":
|
||||
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
|
||||
if not ctx.finished_game:
|
||||
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}.")
|
||||
|
||||
# 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:
|
||||
snes_items_sent = int.from_bytes(tx_data[:2], "little")
|
||||
client_items_sent = int.from_bytes(tx_data[2:4], "little")
|
||||
client_ap_items_found = int.from_bytes(tx_data[4:6], "little")
|
||||
snes_blue_chests_checked: int = int.from_bytes(tx_data[:2], "little")
|
||||
snes_ap_items_found: int = int.from_bytes(tx_data[6:8], "little")
|
||||
snes_other_locations_checked: int = int.from_bytes(tx_data[10:12], "little")
|
||||
|
||||
if client_items_sent < snes_items_sent:
|
||||
location_id: int = locations_start_id + client_items_sent
|
||||
location: str = ctx.location_names[location_id]
|
||||
client_items_sent += 1
|
||||
blue_chests_checked: Dict[str, int] = ctx.stored_data.get(blue_chests_key) or {}
|
||||
if blue_chests_checked.get(str(coop_uuid), 0) < snes_blue_chests_checked:
|
||||
blue_chests_checked[str(coop_uuid)] = snes_blue_chests_checked
|
||||
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)
|
||||
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)" % (
|
||||
location,
|
||||
len(ctx.locations_checked),
|
||||
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"))
|
||||
client_ap_items_found: int = sum(net_item.player != ctx.slot for net_item in ctx.locations_info.values())
|
||||
if client_ap_items_found > snes_ap_items_found:
|
||||
snes_buffered_write(ctx, L2AC_TX_ADDR + 4, client_ap_items_found.to_bytes(2, "little"))
|
||||
|
||||
# RX
|
||||
rx_data: Optional[bytes] = await snes_read(ctx, L2AC_RX_ADDR, 4)
|
||||
|
|
|
@ -9,9 +9,11 @@ start_id: int = Locations.start_id
|
|||
|
||||
class ItemType(Enum):
|
||||
BLUE_CHEST = auto()
|
||||
BOSS = auto()
|
||||
CAPSULE_MONSTER = auto()
|
||||
ENEMY_DROP = auto()
|
||||
ENTRANCE_CHEST = auto()
|
||||
IRIS_TREASURE = auto()
|
||||
PARTY_MEMBER = auto()
|
||||
RED_CHEST = auto()
|
||||
RED_CHEST_PATCH = auto()
|
||||
|
@ -451,15 +453,15 @@ l2ac_item_table: Dict[str, ItemData] = {
|
|||
# 0x0199: "Bunnysuit"
|
||||
# 0x019A: "Seethru cape"
|
||||
# 0x019B: "Seethru silk"
|
||||
# 0x019C: "Iris sword"
|
||||
# 0x019D: "Iris shield"
|
||||
# 0x019E: "Iris helmet"
|
||||
# 0x019F: "Iris armor"
|
||||
# 0x01A0: "Iris ring"
|
||||
# 0x01A1: "Iris jewel"
|
||||
# 0x01A2: "Iris staff"
|
||||
# 0x01A3: "Iris pot"
|
||||
# 0x01A4: "Iris tiara"
|
||||
"Iris sword": ItemData(0x039C, ItemType.IRIS_TREASURE, ItemClassification.progression_skip_balancing),
|
||||
"Iris shield": ItemData(0x039D, ItemType.IRIS_TREASURE, ItemClassification.progression_skip_balancing),
|
||||
"Iris helmet": ItemData(0x039E, ItemType.IRIS_TREASURE, ItemClassification.progression_skip_balancing),
|
||||
"Iris armor": ItemData(0x039F, ItemType.IRIS_TREASURE, ItemClassification.progression_skip_balancing),
|
||||
"Iris ring": ItemData(0x03A0, ItemType.IRIS_TREASURE, ItemClassification.progression_skip_balancing),
|
||||
"Iris jewel": ItemData(0x03A1, ItemType.IRIS_TREASURE, ItemClassification.progression_skip_balancing),
|
||||
"Iris staff": ItemData(0x03A2, ItemType.IRIS_TREASURE, ItemClassification.progression_skip_balancing),
|
||||
"Iris pot": ItemData(0x03A3, ItemType.IRIS_TREASURE, ItemClassification.progression_skip_balancing),
|
||||
"Iris tiara": ItemData(0x03A4, ItemType.IRIS_TREASURE, ItemClassification.progression_skip_balancing),
|
||||
# 0x01A5: "Power jelly"
|
||||
# 0x01A6: "Jewel sonar"
|
||||
# 0x01A7: "Hook"
|
||||
|
@ -489,7 +491,7 @@ l2ac_item_table: Dict[str, ItemData] = {
|
|||
# 0x01BF: "Truth key"
|
||||
# 0x01C0: "Mermaid jade"
|
||||
# 0x01C1: "Engine"
|
||||
# 0x01C2: "Ancient key"
|
||||
"Ancient key": ItemData(0x01C2, ItemType.BOSS, ItemClassification.progression_skip_balancing),
|
||||
# 0x01C3: "Pretty flwr."
|
||||
# 0x01C4: "Glass angel"
|
||||
# 0x01C5: "VIP card"
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
from typing import Dict
|
||||
|
||||
from BaseClasses import Location
|
||||
from .Options import BlueChestCount
|
||||
|
||||
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):
|
||||
|
|
|
@ -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.
|
||||
(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.)
|
||||
Supported values: 10 – 75
|
||||
Supported values: 10 – 100
|
||||
Default value: 25
|
||||
"""
|
||||
|
||||
display_name = "Blue chest count"
|
||||
range_start = 10
|
||||
range_end = 75
|
||||
range_end = 100
|
||||
default = 25
|
||||
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ from enum import IntFlag
|
|||
from random import Random
|
||||
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 Utils import __version__
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
|
@ -50,10 +50,11 @@ class L2ACWorld(World):
|
|||
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},
|
||||
"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},
|
||||
}
|
||||
data_version: ClassVar[int] = 1
|
||||
required_client_version: Tuple[int, int, int] = (0, 3, 6)
|
||||
data_version: ClassVar[int] = 2
|
||||
required_client_version: Tuple[int, int, int] = (0, 4, 2)
|
||||
|
||||
# L2ACWorld specific properties
|
||||
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)
|
||||
chest_access.place_locked_item(prog_chest_access)
|
||||
ancient_dungeon.locations.append(chest_access)
|
||||
treasures = L2ACLocation(self.player, "Iris Treasures", None, ancient_dungeon)
|
||||
treasures.place_locked_item(L2ACItem("Treasures collected", ItemClassification.progression, None, self.player))
|
||||
ancient_dungeon.locations.append(treasures)
|
||||
for iris in self.item_name_groups["Iris treasures"]:
|
||||
treasure_name: str = f"Iris treasure {self.item_name_to_id[iris] - self.item_name_to_id['Iris sword'] + 1}"
|
||||
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)
|
||||
|
||||
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.place_locked_item(L2ACItem("Final Floor access", ItemClassification.progression, None, self.player))
|
||||
final_floor.locations.append(ff_reached)
|
||||
boss = L2ACLocation(self.player, "Boss", None, final_floor)
|
||||
boss.place_locked_item(L2ACItem("Boss victory", ItemClassification.progression, None, self.player))
|
||||
boss: Location = L2ACLocation(self.player, "Boss", self.location_name_to_id["Boss"], final_floor)
|
||||
boss.place_locked_item(self.create_item("Ancient key"))
|
||||
final_floor.locations.append(boss)
|
||||
self.multiworld.regions.append(final_floor)
|
||||
|
||||
|
@ -155,8 +159,9 @@ class L2ACWorld(World):
|
|||
|
||||
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))
|
||||
set_rule(self.multiworld.get_location("Iris Treasures", self.player),
|
||||
lambda state: state.can_reach(f"Blue chest {self.o.blue_chest_count}", "Location", self.player))
|
||||
for i in range(9):
|
||||
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),
|
||||
lambda state: state.can_reach(f"Blue chest {self.o.blue_chest_count}", "Location", self.player))
|
||||
if self.o.shuffle_capsule_monsters:
|
||||
|
@ -170,13 +175,14 @@ class L2ACWorld(World):
|
|||
lambda state: state.has("Final Floor access", self.player)
|
||||
elif self.o.goal == Goal.option_iris_treasure_hunt:
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
rom_path: str = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.sfc")
|
||||
|
|
|
@ -45,8 +45,8 @@ org $8EA721 ; skip master fight dialogue
|
|||
DB $1C,$45,$01 ; L2SASM JMP $8EA5FA+$0145
|
||||
org $8EA74B ; skip master victory dialogue
|
||||
DB $1C,$AC,$01 ; L2SASM JMP $8EA5FA+$01AC
|
||||
org $8EA7AA ; skip master key dialogue
|
||||
DB $1C,$CA,$01 ; L2SASM JMP $8EA5FA+$01CA
|
||||
org $8EA7AA ; skip master key dialogue and animation
|
||||
DB $1C,$EE,$01 ; L2SASM JMP $8EA5FA+$01EE
|
||||
org $8EA7F4 ; skip master goodbye dialogue
|
||||
DB $1C,$05,$02 ; L2SASM JMP $8EA5FA+$0205
|
||||
org $8EA807 ; skip master not fight dialogue
|
||||
|
@ -126,7 +126,7 @@ Init:
|
|||
|
||||
|
||||
|
||||
; transmit checks
|
||||
; transmit checks from chests
|
||||
pushpc
|
||||
org $8EC1EB
|
||||
JML TX ; overwrites JSL $83F559
|
||||
|
@ -136,11 +136,17 @@ TX:
|
|||
JSL $83F559 ; (overwritten instruction) chest opening animation
|
||||
REP #$20
|
||||
LDA $7FD4EF ; read chest item ID
|
||||
BIT.w #$4000 ; test for blue chest flag
|
||||
BIT.w #$0200 ; test for iris item flag
|
||||
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
|
||||
BPL +
|
||||
LDA $F02040 ; load check counter
|
||||
INC ; increment check counter
|
||||
STA $F02040 ; store check counter
|
||||
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
|
||||
pushpc
|
||||
org $D09000
|
||||
|
@ -173,9 +214,9 @@ pullpc
|
|||
|
||||
Goal:
|
||||
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
|
||||
LDA $0798 ; load some event flags (iris tiara, boss, others...)
|
||||
LDA $0798 ; load EV flags $D0-$D7 (iris tiara, boss, others...)
|
||||
TAY
|
||||
AND.b #$02 ; test boss victory
|
||||
LSR
|
||||
|
@ -223,16 +264,32 @@ RX:
|
|||
SpecialItemGet:
|
||||
BPL + ; spells have high bit set
|
||||
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
|
||||
BPL +
|
||||
BPL ++
|
||||
SBC.w #$01B1 ; party member items range from $01B2 to $01B7
|
||||
BMI +
|
||||
BMI ++
|
||||
ASL
|
||||
TAX
|
||||
LDA $8ED8C7,X ; load predefined bitmask with a single bit set
|
||||
ORA $F02018 ; set unlock bit for party member/capsule monster
|
||||
STA $F02018
|
||||
+: RTS
|
||||
++: RTS
|
||||
|
||||
LearnSpell:
|
||||
STA $0A0B
|
||||
|
@ -634,7 +691,7 @@ StartInventory:
|
|||
PHX
|
||||
JSR LearnSpell
|
||||
PLX
|
||||
+: BIT.w #$C000 ; ignore blue chest items (and spells)
|
||||
+: BIT.w #$C200 ; ignore spells, blue chest items, and iris items
|
||||
BNE +
|
||||
PHX
|
||||
STA $09CF ; specify item ID
|
||||
|
@ -1025,12 +1082,16 @@ pullpc
|
|||
; $F0203D 1 death link enabled
|
||||
; $F0203E 1 death link sent (monster id + 1)
|
||||
; $F0203F 1 death link received
|
||||
; $F02040 2 check counter (snes_items_sent)
|
||||
; $F02042 2 check counter (client_items_sent)
|
||||
; $F02040 2 check counter for this save file (snes_blue_chests_checked)
|
||||
; $F02042 2 RESERVED
|
||||
; $F02044 2 check counter (client_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
|
||||
; $F02800 2 received counter
|
||||
; $F02802 2 processed counter
|
||||
; $F02804 inf list of received items
|
||||
; $F06000 inf architect mode RNG state backups
|
||||
; $F02804 var list of received items
|
||||
; $F06000 var architect mode RNG state backups
|
||||
|
|
Binary file not shown.
|
@ -39,9 +39,9 @@ Your Party Leader will hold up the item they received when not in a fight or in
|
|||
###### 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)
|
||||
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
|
||||
- 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 party starting level as well as capsule monster level and form
|
||||
- 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
|
||||
on your previous attempt. This functionality is accessed via the bald NPC behind the counter at the Ancient Cave
|
||||
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)
|
||||
- (optional) Run button that allows you to move at faster than normal speed
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ from . import L2ACTestBase
|
|||
class TestDefault(L2ACTestBase):
|
||||
|
||||
def test_everything(self) -> None:
|
||||
self.collect_all_but(["Boss victory"])
|
||||
self.collect_all_but(["Ancient key"])
|
||||
self.assertBeatable(True)
|
||||
|
||||
def test_nothing(self) -> None:
|
||||
|
@ -17,7 +17,7 @@ class TestShuffleCapsuleMonsters(L2ACTestBase):
|
|||
}
|
||||
|
||||
def test_everything(self) -> None:
|
||||
self.collect_all_but(["Boss victory"])
|
||||
self.collect_all_but(["Ancient key"])
|
||||
self.assertBeatable(True)
|
||||
|
||||
def test_best_party(self) -> None:
|
||||
|
@ -25,7 +25,7 @@ class TestShuffleCapsuleMonsters(L2ACTestBase):
|
|||
self.assertBeatable(True)
|
||||
|
||||
def test_no_darbi(self) -> None:
|
||||
self.collect_all_but(["Boss victory", "DARBI"])
|
||||
self.collect_all_but(["Ancient key", "DARBI"])
|
||||
self.assertBeatable(False)
|
||||
|
||||
|
||||
|
@ -35,7 +35,7 @@ class TestShufflePartyMembers(L2ACTestBase):
|
|||
}
|
||||
|
||||
def test_everything(self) -> None:
|
||||
self.collect_all_but(["Boss victory"])
|
||||
self.collect_all_but(["Ancient key"])
|
||||
self.assertBeatable(True)
|
||||
|
||||
def test_best_party(self) -> None:
|
||||
|
@ -43,15 +43,15 @@ class TestShufflePartyMembers(L2ACTestBase):
|
|||
self.assertBeatable(True)
|
||||
|
||||
def test_no_dekar(self) -> None:
|
||||
self.collect_all_but(["Boss victory", "Dekar"])
|
||||
self.collect_all_but(["Ancient key", "Dekar"])
|
||||
self.assertBeatable(False)
|
||||
|
||||
def test_no_guy(self) -> None:
|
||||
self.collect_all_but(["Boss victory", "Guy"])
|
||||
self.collect_all_but(["Ancient key", "Guy"])
|
||||
self.assertBeatable(False)
|
||||
|
||||
def test_no_arty(self) -> None:
|
||||
self.collect_all_but(["Boss victory", "Arty"])
|
||||
self.collect_all_but(["Ancient key", "Arty"])
|
||||
self.assertBeatable(False)
|
||||
|
||||
|
||||
|
@ -62,7 +62,7 @@ class TestShuffleBoth(L2ACTestBase):
|
|||
}
|
||||
|
||||
def test_everything(self) -> None:
|
||||
self.collect_all_but(["Boss victory"])
|
||||
self.collect_all_but(["Ancient key"])
|
||||
self.assertBeatable(True)
|
||||
|
||||
def test_best_party(self) -> None:
|
||||
|
@ -70,17 +70,17 @@ class TestShuffleBoth(L2ACTestBase):
|
|||
self.assertBeatable(True)
|
||||
|
||||
def test_no_dekar(self) -> None:
|
||||
self.collect_all_but(["Boss victory", "Dekar"])
|
||||
self.collect_all_but(["Ancient key", "Dekar"])
|
||||
self.assertBeatable(False)
|
||||
|
||||
def test_no_guy(self) -> None:
|
||||
self.collect_all_but(["Boss victory", "Guy"])
|
||||
self.collect_all_but(["Ancient key", "Guy"])
|
||||
self.assertBeatable(False)
|
||||
|
||||
def test_no_arty(self) -> None:
|
||||
self.collect_all_but(["Boss victory", "Arty"])
|
||||
self.collect_all_but(["Ancient key", "Arty"])
|
||||
self.assertBeatable(False)
|
||||
|
||||
def test_no_darbi(self) -> None:
|
||||
self.collect_all_but(["Boss victory", "DARBI"])
|
||||
self.collect_all_but(["Ancient key", "DARBI"])
|
||||
self.assertBeatable(False)
|
||||
|
|
Loading…
Reference in New Issue