forked from NikolajDanger/APSly3
Compare commits
9 Commits
92289523ad
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 54b80d5a7d | |||
|
|
dc08eee6c8 | ||
|
|
5985e5d2f1 | ||
| 9cb7bcedf1 | |||
|
|
39ed75298f | ||
|
|
aa9562afe1 | ||
|
|
6688c76f34 | ||
| 6911df1f92 | |||
|
|
fd1405ada0 |
131
Sly3Callbacks.py
Normal file
131
Sly3Callbacks.py
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
from typing import TYPE_CHECKING, Dict, List
|
||||||
|
|
||||||
|
from BaseClasses import ItemClassification
|
||||||
|
|
||||||
|
from .data.Constants import REQUIREMENTS
|
||||||
|
from .data import Items
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .Sly3Client import Sly3Context
|
||||||
|
|
||||||
|
###########
|
||||||
|
# Helpers #
|
||||||
|
###########
|
||||||
|
|
||||||
|
def accessibility(ctx: "Sly3Context") -> Dict[str, Dict[str, List[List[bool]]]]:
|
||||||
|
section_requirements = {
|
||||||
|
episode_name: [
|
||||||
|
list(set(sum([
|
||||||
|
sum(
|
||||||
|
ep_reqs,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
for ep_reqs
|
||||||
|
in episode[:i-1]
|
||||||
|
], [])))
|
||||||
|
for i in range(1,5)
|
||||||
|
]
|
||||||
|
for episode_name, episode in REQUIREMENTS["Jobs"].items()
|
||||||
|
}
|
||||||
|
|
||||||
|
job_requirements = {
|
||||||
|
episode_name: [
|
||||||
|
[
|
||||||
|
list(set(reqs + section_requirements[episode_name][section_idx]))
|
||||||
|
for reqs in section
|
||||||
|
]
|
||||||
|
for section_idx, section in enumerate(episode)
|
||||||
|
]
|
||||||
|
for episode_name, episode in REQUIREMENTS["Jobs"].items()
|
||||||
|
}
|
||||||
|
|
||||||
|
job_requirements["Honor Among Thieves"] = [[
|
||||||
|
[
|
||||||
|
"Bentley",
|
||||||
|
"Murray",
|
||||||
|
"Guru",
|
||||||
|
"Penelope",
|
||||||
|
"Panda King",
|
||||||
|
"Dimitri",
|
||||||
|
"Carmelita"
|
||||||
|
]
|
||||||
|
for _ in range(8)
|
||||||
|
]]
|
||||||
|
|
||||||
|
challenge_requirements = {
|
||||||
|
episode_name: [
|
||||||
|
[
|
||||||
|
list(set(reqs + section_requirements[episode_name][section_idx]))
|
||||||
|
for reqs in section
|
||||||
|
]
|
||||||
|
for section_idx, section in enumerate(episode)
|
||||||
|
]
|
||||||
|
for episode_name, episode in REQUIREMENTS["Challenges"].items()
|
||||||
|
}
|
||||||
|
|
||||||
|
challenge_requirements["Honor Among Thieves"] = [[
|
||||||
|
[
|
||||||
|
"Bentley",
|
||||||
|
"Murray",
|
||||||
|
"Guru",
|
||||||
|
"Penelope",
|
||||||
|
"Panda King",
|
||||||
|
"Dimitri",
|
||||||
|
"Carmelita"
|
||||||
|
]
|
||||||
|
for _ in range(5)
|
||||||
|
]]
|
||||||
|
|
||||||
|
items_received = [
|
||||||
|
Items.from_id(i.item)
|
||||||
|
for i in ctx.items_received
|
||||||
|
]
|
||||||
|
|
||||||
|
progression_item_names = set([
|
||||||
|
i.name
|
||||||
|
for i in items_received
|
||||||
|
if i.classification == ItemClassification.progression
|
||||||
|
])
|
||||||
|
|
||||||
|
job_accessibility = {
|
||||||
|
episode_name: [
|
||||||
|
[
|
||||||
|
all(r in progression_item_names for r in reqs)
|
||||||
|
for reqs in section
|
||||||
|
]
|
||||||
|
for section in episode
|
||||||
|
]
|
||||||
|
for episode_name, episode in job_requirements.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
challenge_accessibility = {
|
||||||
|
episode_name: [
|
||||||
|
[
|
||||||
|
all(r in progression_item_names for r in reqs)
|
||||||
|
for reqs in section
|
||||||
|
]
|
||||||
|
for section in episode
|
||||||
|
]
|
||||||
|
for episode_name, episode in challenge_requirements.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"Jobs": job_accessibility,
|
||||||
|
"Challenges": challenge_accessibility
|
||||||
|
}
|
||||||
|
|
||||||
|
#########
|
||||||
|
# Steps #
|
||||||
|
#########
|
||||||
|
|
||||||
|
##################
|
||||||
|
# Main Functions #
|
||||||
|
##################
|
||||||
|
|
||||||
|
async def init(ctx: "Sly3Context", ap_connected: bool) -> None:
|
||||||
|
"""Called when the player connects to the AP server or changes map"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def update(ctx: "Sly3Context", ap_connected: bool) -> None:
|
||||||
|
"""Called continuously"""
|
||||||
|
pass
|
||||||
261
Sly3Client.py
261
Sly3Client.py
@@ -1,2 +1,261 @@
|
|||||||
def launch_client():
|
from typing import Optional, Dict
|
||||||
|
import asyncio
|
||||||
|
import multiprocessing
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from .data import Items, Locations
|
||||||
|
from .data.Constants import EPISODES, CHALLENGES
|
||||||
|
from CommonClient import logger, server_loop, gui_enabled, get_base_parser
|
||||||
|
import Utils
|
||||||
|
|
||||||
|
from .Sly3Interface import Sly3Interface, Sly3Episode, PowerUps
|
||||||
|
from .Sly3Callbacks import init, update
|
||||||
|
|
||||||
|
# Load Universal Tracker
|
||||||
|
tracker_loaded: bool = False
|
||||||
|
try:
|
||||||
|
from worlds.tracker.TrackerClient import (
|
||||||
|
TrackerCommandProcessor as ClientCommandProcessor,
|
||||||
|
TrackerGameContext as CommonContext,
|
||||||
|
UT_VERSION
|
||||||
|
)
|
||||||
|
|
||||||
|
tracker_loaded = True
|
||||||
|
except ImportError:
|
||||||
|
from CommonClient import ClientCommandProcessor, CommonContext
|
||||||
|
|
||||||
|
class Sly3CommandProcessor(ClientCommandProcessor): # type: ignore[misc]
|
||||||
|
def _cmd_deathlink(self):
|
||||||
|
"""Toggle deathlink from client. Overrides default setting."""
|
||||||
|
if isinstance(self.ctx, Sly3Context):
|
||||||
|
self.ctx.death_link_enabled = not self.ctx.death_link_enabled
|
||||||
|
Utils.async_start(
|
||||||
|
self.ctx.update_death_link(
|
||||||
|
self.ctx.death_link_enabled
|
||||||
|
),
|
||||||
|
name="Update Deathlink"
|
||||||
|
)
|
||||||
|
message = f"Deathlink {'enabled' if self.ctx.death_link_enabled else 'disabled'}"
|
||||||
|
logger.info(message)
|
||||||
|
self.ctx.notification(message)
|
||||||
|
|
||||||
|
def _cmd_menu(self):
|
||||||
|
"""Reload to the episode menu"""
|
||||||
|
if isinstance(self.ctx, Sly3Context):
|
||||||
|
self.ctx.game_interface.to_episode_menu()
|
||||||
|
|
||||||
|
def _cmd_coins(self, amount: str):
|
||||||
|
"""Add coins to game."""
|
||||||
|
if isinstance(self.ctx, Sly3Context):
|
||||||
|
self.ctx.game_interface.add_coins(int(amount))
|
||||||
|
|
||||||
|
class Sly3Context(CommonContext): # type: ignore[misc]
|
||||||
|
command_processor = Sly3CommandProcessor
|
||||||
|
game_interface: Sly3Interface
|
||||||
|
game = "Sly 3: Honor Among Thieves"
|
||||||
|
items_handling = 0b111
|
||||||
|
pcsx2_sync_task: Optional[asyncio.Task] = None
|
||||||
|
is_connected_to_game: bool = False
|
||||||
|
is_connected_to_server: bool = False
|
||||||
|
slot_data: Optional[Dict[str, Utils.Any]] = None
|
||||||
|
last_error_message: Optional[str] = None
|
||||||
|
# notification_queue: list[str] = []
|
||||||
|
# notification_timestamp: float = 0
|
||||||
|
# showing_notification: bool = False
|
||||||
|
deathlink_timestamp: float = 0
|
||||||
|
death_link_enabled = False
|
||||||
|
queued_deaths: int = 0
|
||||||
|
|
||||||
|
# Game state
|
||||||
|
is_loading: bool = False
|
||||||
|
in_safehouse: bool = False
|
||||||
|
in_hub: bool = False
|
||||||
|
current_map: Optional[int] = None
|
||||||
|
|
||||||
|
# Items and checks
|
||||||
|
inventory: Dict[int,int] = {l.code: 0 for l in Items.item_dict.values()}
|
||||||
|
available_episodes: Dict[Sly3Episode,int] = {e: 0 for e in Sly3Episode}
|
||||||
|
thiefnet_items: Optional[list[str]] = None
|
||||||
|
powerups: PowerUps = PowerUps()
|
||||||
|
thiefnet_purchases: PowerUps = PowerUps()
|
||||||
|
jobs_completed: list[list[list[bool]]] = [
|
||||||
|
[[False for _ in chapter] for chapter in episode]
|
||||||
|
for episode in EPISODES.values()
|
||||||
|
]
|
||||||
|
challenges_completed: list[list[list[bool]]] = [
|
||||||
|
[[False for _ in chapter] for chapter in episode]
|
||||||
|
for episode in CHALLENGES.values()
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, server_address, password):
|
||||||
|
super().__init__(server_address, password)
|
||||||
|
self.version = [0,0,0]
|
||||||
|
self.game_interface = Sly3Interface(logger)
|
||||||
|
|
||||||
|
def on_deathlink(self, data: Utils.Dict[str, Utils.Any]) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def make_gui(self):
|
||||||
|
ui = super().make_gui()
|
||||||
|
ui.base_title = f"Sly 3 Client v{'.'.join([str(i) for i in self.version])}"
|
||||||
|
if tracker_loaded:
|
||||||
|
ui.base_title += f" | Universal Tracker {UT_VERSION}"
|
||||||
|
|
||||||
|
# AP version is added behind this automatically
|
||||||
|
ui.base_title += " | Archipelago"
|
||||||
|
return ui
|
||||||
|
|
||||||
|
# async def server_auth(self, password_requested: bool = False) -> None:
|
||||||
|
# if password_requested and not self.password:
|
||||||
|
# await super(Sly3Context, self).server_auth(password_requested)
|
||||||
|
# await self.get_username()
|
||||||
|
# await self.send_connect()
|
||||||
|
|
||||||
|
def on_package(self, cmd: str, args: dict):
|
||||||
|
super().on_package(cmd, args)
|
||||||
|
if cmd == "Connected":
|
||||||
|
self.slot_data = args["slot_data"]
|
||||||
|
|
||||||
|
if self.version[:2] != args["slot_data"]["world_version"][:2]:
|
||||||
|
raise Exception(f"World generation version and client version don't match up. The world was generated with version {args["slot_data"]["world_version"]}, but the client is version {self.version}")
|
||||||
|
|
||||||
|
self.thiefnet_purchases = PowerUps(*[
|
||||||
|
Locations.location_dict[f"ThiefNet {i+1:02}"].code in self.checked_locations
|
||||||
|
for i in range(24)
|
||||||
|
])
|
||||||
|
|
||||||
|
# Set death link tag if it was requested in options
|
||||||
|
if "death_link" in args["slot_data"]:
|
||||||
|
self.death_link_enabled = bool(args["slot_data"]["death_link"])
|
||||||
|
Utils.async_start(self.update_death_link(
|
||||||
|
bool(args["slot_data"]["death_link"])
|
||||||
|
))
|
||||||
|
|
||||||
|
Utils.async_start(self.send_msgs([{
|
||||||
|
"cmd": "LocationScouts",
|
||||||
|
"locations": [
|
||||||
|
Locations.location_dict[location].code
|
||||||
|
for location in Locations.location_groups["Purchase"]
|
||||||
|
]
|
||||||
|
}]))
|
||||||
|
|
||||||
|
def update_connection_status(ctx: Sly3Context, status: bool):
|
||||||
|
if ctx.is_connected_to_game == status:
|
||||||
|
return
|
||||||
|
|
||||||
|
if status:
|
||||||
|
logger.info("Connected to Sly 3")
|
||||||
|
else:
|
||||||
|
logger.info("Unable to connect to the PCSX2 instance, attempting to reconnect...")
|
||||||
|
|
||||||
|
ctx.is_connected_to_game = status
|
||||||
|
|
||||||
|
async def _handle_game_not_ready(ctx: Sly3Context):
|
||||||
|
"""If the game is not connected, this will attempt to retry connecting to the game."""
|
||||||
|
if not ctx.exit_event.is_set():
|
||||||
|
ctx.game_interface.connect_to_game()
|
||||||
|
await asyncio.sleep(3)
|
||||||
|
|
||||||
|
async def _handle_game_ready(ctx: Sly3Context) -> None:
|
||||||
|
current_map = ctx.game_interface.get_current_map()
|
||||||
|
|
||||||
|
ctx.game_interface.skip_cutscene()
|
||||||
|
|
||||||
|
if ctx.game_interface.is_loading():
|
||||||
|
ctx.is_loading = True
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
return
|
||||||
|
elif ctx.is_loading:
|
||||||
|
ctx.is_loading = False
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
connected_to_server = (ctx.server is not None) and (ctx.slot is not None)
|
||||||
|
|
||||||
|
new_connection = ctx.is_connected_to_server != connected_to_server
|
||||||
|
if ctx.current_map != current_map or new_connection:
|
||||||
|
ctx.current_map = current_map
|
||||||
|
ctx.is_connected_to_server = connected_to_server
|
||||||
|
await init(ctx, connected_to_server)
|
||||||
|
|
||||||
|
await update(ctx, connected_to_server)
|
||||||
|
|
||||||
|
if ctx.server:
|
||||||
|
ctx.last_error_message = None
|
||||||
|
if not ctx.slot:
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
return
|
||||||
|
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
else:
|
||||||
|
message = "Waiting for player to connect to server"
|
||||||
|
if ctx.last_error_message is not message:
|
||||||
|
logger.info("Waiting for player to connect to server")
|
||||||
|
ctx.last_error_message = message
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
async def pcsx2_sync_task(ctx: Sly3Context):
|
||||||
|
logger.info("Starting Sly 3 Connector, attempting to connect to emulator...")
|
||||||
|
ctx.game_interface.connect_to_game()
|
||||||
|
while not ctx.exit_event.is_set():
|
||||||
|
try:
|
||||||
|
is_connected = ctx.game_interface.get_connection_state()
|
||||||
|
update_connection_status(ctx, is_connected)
|
||||||
|
if is_connected:
|
||||||
|
await _handle_game_ready(ctx)
|
||||||
|
else:
|
||||||
|
await _handle_game_not_ready(ctx)
|
||||||
|
except ConnectionError:
|
||||||
|
ctx.game_interface.disconnect_from_game()
|
||||||
|
except Exception as e:
|
||||||
|
if isinstance(e, RuntimeError):
|
||||||
|
logger.error(str(e))
|
||||||
|
else:
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
await asyncio.sleep(3)
|
||||||
|
continue
|
||||||
|
|
||||||
|
def launch_client():
|
||||||
|
Utils.init_logging("Sly 3 Client")
|
||||||
|
|
||||||
|
async def main(args):
|
||||||
|
multiprocessing.freeze_support()
|
||||||
|
logger.info("main")
|
||||||
|
ctx = Sly3Context(args.connect, args.password)
|
||||||
|
|
||||||
|
logger.info("Connecting to server...")
|
||||||
|
ctx.server_task = asyncio.create_task(server_loop(ctx), name="Server Loop")
|
||||||
|
|
||||||
|
# Runs Universal Tracker's internal generator
|
||||||
|
if tracker_loaded:
|
||||||
|
ctx.run_generator()
|
||||||
|
ctx.tags.remove("Tracker")
|
||||||
|
|
||||||
|
if gui_enabled:
|
||||||
|
ctx.run_gui()
|
||||||
|
ctx.run_cli()
|
||||||
|
|
||||||
|
logger.info("Running game...")
|
||||||
|
ctx.pcsx2_sync_task = asyncio.create_task(pcsx2_sync_task(ctx), name="PCSX2 Sync")
|
||||||
|
|
||||||
|
await ctx.exit_event.wait()
|
||||||
|
ctx.server_address = None
|
||||||
|
|
||||||
|
await ctx.shutdown()
|
||||||
|
|
||||||
|
if ctx.pcsx2_sync_task:
|
||||||
|
await asyncio.sleep(3)
|
||||||
|
await ctx.pcsx2_sync_task
|
||||||
|
|
||||||
|
import colorama
|
||||||
|
|
||||||
|
colorama.init()
|
||||||
|
|
||||||
|
|
||||||
|
parser = get_base_parser()
|
||||||
|
args, _ = parser.parse_known_args()
|
||||||
|
|
||||||
|
asyncio.run(main(args))
|
||||||
|
colorama.deinit()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
launch_client()
|
||||||
|
|||||||
429
Sly3Interface.py
429
Sly3Interface.py
@@ -2,6 +2,7 @@ from typing import Optional, Dict, NamedTuple
|
|||||||
import struct
|
import struct
|
||||||
from logging import Logger
|
from logging import Logger
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
|
import traceback
|
||||||
|
|
||||||
from .pcsx2_interface.pine import Pine
|
from .pcsx2_interface.pine import Pine
|
||||||
from .data.Constants import ADDRESSES, MENU_RETURN_DATA
|
from .data.Constants import ADDRESSES, MENU_RETURN_DATA
|
||||||
@@ -16,60 +17,60 @@ class Sly3Episode(IntEnum):
|
|||||||
Honor_Among_Thieves = 6
|
Honor_Among_Thieves = 6
|
||||||
|
|
||||||
class PowerUps(NamedTuple):
|
class PowerUps(NamedTuple):
|
||||||
attack = False
|
attack: bool = False
|
||||||
binocucom = False
|
binocucom: bool = False
|
||||||
bombs = False
|
bombs: bool = False
|
||||||
unknown = False
|
unknown: bool = False
|
||||||
trigger_Bomb = False
|
trigger_Bomb: bool = False
|
||||||
fishing_pole = False
|
fishing_pole: bool = False
|
||||||
|
|
||||||
alarm_clock = False
|
alarm_clock: bool = False
|
||||||
adrenaline_burst = False
|
adrenaline_burst: bool = False
|
||||||
health_extractor = False
|
health_extractor: bool = False
|
||||||
hover_pack = False
|
hover_pack: bool = False
|
||||||
insanity_strike = False
|
insanity_strike: bool = False
|
||||||
grapple_cam = False
|
grapple_cam: bool = False
|
||||||
size_destabilizer = False
|
size_destabilizer: bool = False
|
||||||
rage_bomb = False
|
rage_bomb: bool = False
|
||||||
|
|
||||||
reduction_bomb = False
|
reduction_bomb: bool = False
|
||||||
ball_form = False
|
ball_form: bool = False
|
||||||
berserker_charge = False
|
berserker_charge: bool = False
|
||||||
juggernaut_throw = False
|
juggernaut_throw: bool = False
|
||||||
guttural_roar = False
|
guttural_roar: bool = False
|
||||||
fists_of_flame = False
|
fists_of_flame: bool = False
|
||||||
temporal_lock = False
|
temporal_lock: bool = False
|
||||||
raging_inferno_flop = False
|
raging_inferno_flop: bool = False
|
||||||
|
|
||||||
diablo_fire_slam = False
|
diablo_fire_slam: bool = False
|
||||||
smoke_bomb = False
|
smoke_bomb: bool = False
|
||||||
combat_dodge = False
|
combat_dodge: bool = False
|
||||||
paraglider = False
|
paraglider: bool = False
|
||||||
silent_obliteration = False
|
silent_obliteration: bool = False
|
||||||
feral_pounce = False
|
feral_pounce: bool = False
|
||||||
mega_jump = False
|
mega_jump: bool = False
|
||||||
knockout_dive = False
|
knockout_dive: bool = False
|
||||||
|
|
||||||
shadow_power_1 = False
|
shadow_power_1: bool = False
|
||||||
thief_reflexes = False
|
thief_reflexes: bool = False
|
||||||
shadow_power_2 = False
|
shadow_power_2: bool = False
|
||||||
rocket_boots = False
|
rocket_boots: bool = False
|
||||||
treasure_map = False
|
treasure_map: bool = False
|
||||||
shield = False
|
shield: bool = False
|
||||||
venice_disguise = False
|
venice_disguise: bool = False
|
||||||
photographer_disguise = False
|
photographer_disguise: bool = False
|
||||||
|
|
||||||
pirate_disguise = False
|
pirate_disguise: bool = False
|
||||||
spin_1 = False
|
spin_1: bool = False
|
||||||
spin_2 = False
|
spin_2: bool = False
|
||||||
spin_3 = False
|
spin_3: bool = False
|
||||||
jump_1 = False
|
jump_1: bool = False
|
||||||
jump_2 = False
|
jump_2: bool = False
|
||||||
jump_3 = False
|
jump_3: bool = False
|
||||||
push_1 = False
|
push_1: bool = False
|
||||||
|
|
||||||
push_2 = False
|
push_2: bool = False
|
||||||
push_3 = False
|
push_3: bool = False
|
||||||
|
|
||||||
class GameInterface():
|
class GameInterface():
|
||||||
"""
|
"""
|
||||||
@@ -100,6 +101,15 @@ class GameInterface():
|
|||||||
def _read_float(self, address: int):
|
def _read_float(self, address: int):
|
||||||
return struct.unpack("f",self.pcsx2_interface.read_bytes(address, 4))[0]
|
return struct.unpack("f",self.pcsx2_interface.read_bytes(address, 4))[0]
|
||||||
|
|
||||||
|
def _batch_read8(self, addresses: list[int]) -> list[int]:
|
||||||
|
return self.pcsx2_interface.batch_read_int8(addresses)
|
||||||
|
|
||||||
|
def _batch_read16(self, addresses: list[int]) -> list[int]:
|
||||||
|
return self.pcsx2_interface.batch_read_int16(addresses)
|
||||||
|
|
||||||
|
def _batch_read32(self, addresses: list[int]) -> list[int]:
|
||||||
|
return self.pcsx2_interface.batch_read_int32(addresses)
|
||||||
|
|
||||||
def _write8(self, address: int, value: int):
|
def _write8(self, address: int, value: int):
|
||||||
self.pcsx2_interface.write_int8(address, value)
|
self.pcsx2_interface.write_int8(address, value)
|
||||||
|
|
||||||
@@ -113,7 +123,7 @@ class GameInterface():
|
|||||||
self.pcsx2_interface.write_bytes(address, value)
|
self.pcsx2_interface.write_bytes(address, value)
|
||||||
|
|
||||||
def _write_float(self, address: int, value: float):
|
def _write_float(self, address: int, value: float):
|
||||||
self.pcsx2_interface.write_bytes(address, struct.pack("f", value))
|
self.pcsx2_interface.write_float(address, value)
|
||||||
|
|
||||||
def connect_to_game(self):
|
def connect_to_game(self):
|
||||||
"""
|
"""
|
||||||
@@ -129,6 +139,10 @@ class GameInterface():
|
|||||||
game_id = self.pcsx2_interface.get_game_id()
|
game_id = self.pcsx2_interface.get_game_id()
|
||||||
# The first read of the address will be null if the client is faster than the emulator
|
# The first read of the address will be null if the client is faster than the emulator
|
||||||
self.current_game = None
|
self.current_game = None
|
||||||
|
if game_id == "":
|
||||||
|
self.logger.debug("No game connected")
|
||||||
|
return
|
||||||
|
|
||||||
if game_id in ADDRESSES.keys():
|
if game_id in ADDRESSES.keys():
|
||||||
self.current_game = game_id
|
self.current_game = game_id
|
||||||
self.addresses = ADDRESSES[game_id]
|
self.addresses = ADDRESSES[game_id]
|
||||||
@@ -137,9 +151,9 @@ class GameInterface():
|
|||||||
f"Connected to the wrong game ({game_id})")
|
f"Connected to the wrong game ({game_id})")
|
||||||
self.game_id_error = game_id
|
self.game_id_error = game_id
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
pass
|
self.logger.debug(traceback.format_exc())
|
||||||
except ConnectionError:
|
except ConnectionError:
|
||||||
pass
|
self.logger.debug(traceback.format_exc())
|
||||||
|
|
||||||
def disconnect_from_game(self):
|
def disconnect_from_game(self):
|
||||||
self.pcsx2_interface.disconnect()
|
self.pcsx2_interface.disconnect()
|
||||||
@@ -154,6 +168,9 @@ class GameInterface():
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
class Sly3Interface(GameInterface):
|
class Sly3Interface(GameInterface):
|
||||||
|
############################
|
||||||
|
## Private Helper Methods ##
|
||||||
|
############################
|
||||||
def _reload(self, reload_data: bytes):
|
def _reload(self, reload_data: bytes):
|
||||||
self._write_bytes(
|
self._write_bytes(
|
||||||
self.addresses["reload values"],
|
self.addresses["reload values"],
|
||||||
@@ -168,6 +185,23 @@ class Sly3Interface(GameInterface):
|
|||||||
|
|
||||||
return pointer
|
return pointer
|
||||||
|
|
||||||
|
###################
|
||||||
|
## Current State ##
|
||||||
|
###################
|
||||||
|
def in_cutscene(self) -> bool:
|
||||||
|
frame_counter = self._read16(self.addresses["frame counter"])
|
||||||
|
return frame_counter > 10
|
||||||
|
|
||||||
|
def is_loading(self) -> bool:
|
||||||
|
return self._read32(self.addresses["loading"]) == 2
|
||||||
|
|
||||||
|
#######################
|
||||||
|
## Getters & Setters ##
|
||||||
|
#######################
|
||||||
|
def get_current_episode(self) -> Sly3Episode:
|
||||||
|
episode_num = self._read32(self.addresses["world id"])
|
||||||
|
return Sly3Episode(episode_num)
|
||||||
|
|
||||||
def get_current_map(self) -> int:
|
def get_current_map(self) -> int:
|
||||||
return self._read32(self.addresses["map id"])
|
return self._read32(self.addresses["map id"])
|
||||||
|
|
||||||
@@ -180,6 +214,39 @@ class Sly3Interface(GameInterface):
|
|||||||
def set_items_received(self, n:int) -> None:
|
def set_items_received(self, n:int) -> None:
|
||||||
self._write32(self.addresses["items received"], n)
|
self._write32(self.addresses["items received"], n)
|
||||||
|
|
||||||
|
def set_powerups(self, powerups: PowerUps):
|
||||||
|
booleans = list(powerups)
|
||||||
|
byte_list = [
|
||||||
|
[False]*2+booleans[0:6],
|
||||||
|
booleans[6:14],
|
||||||
|
booleans[14:22],
|
||||||
|
booleans[22:30],
|
||||||
|
booleans[30:38],
|
||||||
|
booleans[38:46],
|
||||||
|
booleans[46:48]+[False]*2,
|
||||||
|
[False]*8
|
||||||
|
]
|
||||||
|
data = b''.join(
|
||||||
|
int(''.join(str(int(i)) for i in byte[::-1]),2).to_bytes(1,"big")
|
||||||
|
for byte in byte_list
|
||||||
|
)
|
||||||
|
|
||||||
|
self._write_bytes(self.addresses["gadgets"], data)
|
||||||
|
|
||||||
|
def get_powerups(self):
|
||||||
|
data = self._read_bytes(self.addresses["gadgets"], 8)
|
||||||
|
bits = [
|
||||||
|
bool(int(b))
|
||||||
|
for byte in data
|
||||||
|
for b in f"{byte:08b}"[::-1]
|
||||||
|
]
|
||||||
|
|
||||||
|
relevant_bits = bits[2:48]
|
||||||
|
return PowerUps(*relevant_bits)
|
||||||
|
|
||||||
|
#################
|
||||||
|
## Other Utils ##
|
||||||
|
#################
|
||||||
def to_episode_menu(self) -> None:
|
def to_episode_menu(self) -> None:
|
||||||
self.logger.info("Skipping to episode menu")
|
self.logger.info("Skipping to episode menu")
|
||||||
if (
|
if (
|
||||||
@@ -194,185 +261,111 @@ class Sly3Interface(GameInterface):
|
|||||||
def unlock_episodes(self) -> None:
|
def unlock_episodes(self) -> None:
|
||||||
self._write8(self.addresses["episode unlocks"], 8)
|
self._write8(self.addresses["episode unlocks"], 8)
|
||||||
|
|
||||||
def in_cutscene(self) -> bool:
|
|
||||||
frame_counter = self._read16(self.addresses["frame counter"])
|
|
||||||
return frame_counter > 10
|
|
||||||
|
|
||||||
def skip_cutscene(self) -> None:
|
def skip_cutscene(self) -> None:
|
||||||
pressing_x = self._read8(self.addresses["x pressed"]) == 255
|
pressing_x = self._read8(self.addresses["x pressed"]) == 255
|
||||||
|
|
||||||
if self.in_cutscene() and pressing_x:
|
if self.in_cutscene() and pressing_x:
|
||||||
self._write32(self.addresses["skip cutscene"],0)
|
self._write32(self.addresses["skip cutscene"],0)
|
||||||
|
|
||||||
|
def add_coins(self, to_add: int):
|
||||||
|
current_amount = self._read32(self.addresses["coins"])
|
||||||
|
new_amount = max(current_amount + to_add,0)
|
||||||
|
self._write32(self.addresses["coins"],new_amount)
|
||||||
|
|
||||||
|
#### TESTING ZONE ####
|
||||||
|
|
||||||
|
def read_text(interf: Sly3Interface, address: int):
|
||||||
|
"""Reads text at a specific address"""
|
||||||
|
text = ""
|
||||||
|
|
||||||
|
while True:
|
||||||
|
character = interf._read_bytes(address,2)
|
||||||
|
|
||||||
|
if character == b"\x00\x00":
|
||||||
|
break
|
||||||
|
|
||||||
|
text += character.decode("utf-16-le")
|
||||||
|
|
||||||
|
address += 2
|
||||||
|
|
||||||
|
return text
|
||||||
|
|
||||||
|
def find_string_id(interf: Sly3Interface, _id: int):
|
||||||
|
"""Searches for a specific string by ID"""
|
||||||
|
|
||||||
|
# String table starts at 0x47A2D8
|
||||||
|
|
||||||
|
# Each entry in the string table has 4 bytes of its ID and then 4 bytes of an
|
||||||
|
# address to the string
|
||||||
|
|
||||||
|
string_table_address = interf._read32(0x47A2D8)
|
||||||
|
i = 0
|
||||||
|
while True:
|
||||||
|
string_id = interf._read32(string_table_address+i*8)
|
||||||
|
if string_id == _id:
|
||||||
|
return interf._read32(string_table_address+i*8+4)
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
def print_thiefnet_addresses(interf: Sly3Interface):
|
||||||
|
print(" {")
|
||||||
|
for i in range(44):
|
||||||
|
address = 0x343208+i*0x3c
|
||||||
|
interf._write32(address,i+1)
|
||||||
|
interf._write32(address+0xC,0)
|
||||||
|
|
||||||
|
name_id = interf._read32(address+0x14)
|
||||||
|
name_address = find_string_id(interf, name_id)
|
||||||
|
name_text = read_text(interf, name_address)
|
||||||
|
|
||||||
|
description_id = interf._read32(address+0x18)
|
||||||
|
description_address = find_string_id(interf, description_id)
|
||||||
|
|
||||||
|
print(
|
||||||
|
" " +
|
||||||
|
f"\"{name_text}\": "+
|
||||||
|
f"({hex(name_address)},{hex(description_address)}),"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(" }")
|
||||||
|
|
||||||
|
def current_job_info(interf: Sly3Interface):
|
||||||
|
current_job = interf._read32(0x36DB98)
|
||||||
|
|
||||||
|
address = interf._read32(interf.addresses["DAG root"])
|
||||||
|
i = 0
|
||||||
|
while address != 0:
|
||||||
|
job_pointer = interf._read32(address+0x6c)
|
||||||
|
job_id = interf._read32(job_pointer+0x18)
|
||||||
|
if job_id == current_job:
|
||||||
|
break
|
||||||
|
|
||||||
|
address = interf._read32(address+0x20)
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
print("Job ID:", current_job)
|
||||||
|
print("Job address:", hex(address))
|
||||||
|
print("Job index:", i)
|
||||||
|
print("Job state (should be 2):", interf._read32(address+0x44))
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
interf = Sly3Interface(Logger("test"))
|
interf = Sly3Interface(Logger("test"))
|
||||||
interf.connect_to_game()
|
interf.connect_to_game()
|
||||||
|
#interf.to_episode_menu()
|
||||||
|
#interf.unlock_episodes()
|
||||||
|
# interf.skip_cutscene()
|
||||||
|
|
||||||
byte_list = [
|
# Loading all power-ups (except the one I don't know)
|
||||||
[
|
power_ups = PowerUps(True, True, True, False, *[True]*44)
|
||||||
False, # ???
|
interf.set_powerups(power_ups)
|
||||||
False, # ???
|
|
||||||
True, # Sly/Bentley square attack
|
|
||||||
True, # Binocucom
|
|
||||||
True, # Bentley's bombs
|
|
||||||
False, # ???
|
|
||||||
True, # Trigger Bomb
|
|
||||||
True # Fishing Pole
|
|
||||||
],
|
|
||||||
[
|
|
||||||
True, # Alarm Clock
|
|
||||||
True, # Adrenaline Burst
|
|
||||||
True, # Health Extractor
|
|
||||||
True, # Hover Pack (Doesn't activate until you reload)
|
|
||||||
True, # Insanity Strike
|
|
||||||
True, # Grapple Came
|
|
||||||
True, # Size Destabilizer
|
|
||||||
True # Rage Bomb
|
|
||||||
],
|
|
||||||
[
|
|
||||||
True, # Reduction Bomb
|
|
||||||
True, # Ball Form
|
|
||||||
True, # Berskerker Charge
|
|
||||||
True, # Juggernaut Throw
|
|
||||||
True, # Gutteral Roar
|
|
||||||
True, # Fists of Flame
|
|
||||||
True, # Temporal Lock
|
|
||||||
True # Raging Inferno Flop
|
|
||||||
],
|
|
||||||
[
|
|
||||||
True, # Diablo Fire Slam
|
|
||||||
True, # Smoke Bomb
|
|
||||||
True, # Combat Dodge
|
|
||||||
True, # Paraglider
|
|
||||||
True, # Silent Obliteration
|
|
||||||
True, # Feral Pounce
|
|
||||||
True, # Mega Jump
|
|
||||||
True # Knockout Dive
|
|
||||||
],
|
|
||||||
[
|
|
||||||
True, # Shadow Power Lvl 1
|
|
||||||
True, # Thief Reflexes
|
|
||||||
True, # Shadow Power Lvl 2
|
|
||||||
True, # Rocket Boots
|
|
||||||
True, # Treasure Map
|
|
||||||
False, # ???
|
|
||||||
True, # Venice Disguise
|
|
||||||
True # Photographer Disguise
|
|
||||||
],
|
|
||||||
[
|
|
||||||
True, # Pirate Disguise
|
|
||||||
True, # Spin Attack lvl 1
|
|
||||||
True, # Spin Attack lvl 2
|
|
||||||
True, # Spin Attack lvl 3
|
|
||||||
True, # Jump Attack lvl 1
|
|
||||||
True, # Jump Attack lvl 2
|
|
||||||
True, # Jump Attack lvl 3
|
|
||||||
True # Push Attack lvl 1
|
|
||||||
],
|
|
||||||
[
|
|
||||||
True, # Push Attack lvl 1
|
|
||||||
True, # Push Attack lvl 1
|
|
||||||
False,
|
|
||||||
False,
|
|
||||||
False,
|
|
||||||
False,
|
|
||||||
False,
|
|
||||||
False
|
|
||||||
],
|
|
||||||
[
|
|
||||||
False,
|
|
||||||
False,
|
|
||||||
False,
|
|
||||||
False,
|
|
||||||
False,
|
|
||||||
False,
|
|
||||||
False,
|
|
||||||
False
|
|
||||||
],
|
|
||||||
]
|
|
||||||
|
|
||||||
# byte_list = [[False for _ in range(8)] for _ in range(8)]
|
# Adding 10000 coins
|
||||||
|
#interf.add_coins(10000)
|
||||||
|
|
||||||
data = b''.join(
|
# === Testing Zone ===
|
||||||
int(''.join(str(int(i)) for i in byte[::-1]),2).to_bytes(1,"big")
|
|
||||||
for byte in byte_list
|
|
||||||
)
|
|
||||||
|
|
||||||
interf._write_bytes(0x468DCC,data)
|
# print_thiefnet_addresses(interf)
|
||||||
|
|
||||||
# data = interf._read_bytes(0x468DCC, 8)
|
# disabling first job of episode 1 (0 = disabled, 1 = available, 2 = in progress, 3 = complete)
|
||||||
# bits = [
|
# interf._write32(0x1335d10+0x44, 0)
|
||||||
# bool(int(b))
|
|
||||||
# for byte in data
|
|
||||||
# for b in f"{byte:08b}"[::-1]
|
|
||||||
# ]
|
|
||||||
|
|
||||||
# print(bits)
|
current_job_info(interf)
|
||||||
|
|
||||||
interf._write32(0x468DDC, 10000)
|
|
||||||
|
|
||||||
# thiefnet_values = list(range(9)) + list(range(10,))
|
|
||||||
|
|
||||||
|
|
||||||
# def read_text(address: int):
|
|
||||||
# text = ""
|
|
||||||
|
|
||||||
# while True:
|
|
||||||
# character = interf._read_bytes(address,2)
|
|
||||||
|
|
||||||
# if character == b"\x00\x00":
|
|
||||||
# break
|
|
||||||
|
|
||||||
# text += character.decode("utf-16-le")
|
|
||||||
|
|
||||||
# address += 2
|
|
||||||
|
|
||||||
# return text
|
|
||||||
|
|
||||||
# def find_string_id(_id: int):
|
|
||||||
# string_table_address = interf._read32(0x47A2D8)
|
|
||||||
# i = 0
|
|
||||||
# while True:
|
|
||||||
# string_id = interf._read32(string_table_address+i*8)
|
|
||||||
# if string_id == _id:
|
|
||||||
# return interf._read32(string_table_address+i*8+4)
|
|
||||||
# i += 1
|
|
||||||
|
|
||||||
|
|
||||||
# print(" {")
|
|
||||||
# for i in range(44):
|
|
||||||
# address = 0x343208+i*0x3c
|
|
||||||
# interf._write32(address,i+1)
|
|
||||||
# interf._write32(address+0xC,0)
|
|
||||||
|
|
||||||
# name_id = interf._read32(address+0x14)
|
|
||||||
# name_address = find_string_id(name_id)
|
|
||||||
# name_text = read_text(name_address)
|
|
||||||
|
|
||||||
# description_id = interf._read32(address+0x18)
|
|
||||||
# description_address = find_string_id(description_id)
|
|
||||||
|
|
||||||
# print(
|
|
||||||
# " " +
|
|
||||||
# f"\"{name_text}\": "+
|
|
||||||
# f"({hex(name_address)},{hex(description_address)}),"
|
|
||||||
# )
|
|
||||||
|
|
||||||
# print(" }")
|
|
||||||
|
|
||||||
# string_table_address = interf._read32(0x47A2D8)
|
|
||||||
# for i in range(10):
|
|
||||||
# print("----")
|
|
||||||
# string_id = interf._read32(string_table_address+i*8)
|
|
||||||
# print(string_id)
|
|
||||||
# string_address = interf._read32(string_table_address+i*8+4)
|
|
||||||
# print(hex(string_address))
|
|
||||||
# print(read_text(string_address))
|
|
||||||
|
|
||||||
# print(interf._read32(0x6b4110+0x44))
|
|
||||||
print(interf._read32(0x1365be0+0x44))
|
|
||||||
print()
|
|
||||||
print(interf._read32(0x1357f80+0x44))
|
|
||||||
print(interf._read32(0x1350560+0x44))
|
|
||||||
print(interf._read32(0x135aba0+0x44))
|
|
||||||
print(interf._read32(0x36DB98))
|
|
||||||
|
|||||||
@@ -69,6 +69,15 @@ class CoinsMaximum(Range):
|
|||||||
range_end = 1000
|
range_end = 1000
|
||||||
default = 200
|
default = 200
|
||||||
|
|
||||||
|
class ThiefNetLocations(Range):
|
||||||
|
"""
|
||||||
|
The number ThiefNet locations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
display_name = "ThiefNet Locations"
|
||||||
|
range_start = 0
|
||||||
|
range_end = 37
|
||||||
|
default = 25
|
||||||
|
|
||||||
class ThiefNetCostMinimum(Range):
|
class ThiefNetCostMinimum(Range):
|
||||||
"""
|
"""
|
||||||
@@ -100,6 +109,7 @@ class Sly3Options(PerGameCommonOptions):
|
|||||||
include_mega_jump: IncludeMegaJump
|
include_mega_jump: IncludeMegaJump
|
||||||
coins_minimum: CoinsMinimum
|
coins_minimum: CoinsMinimum
|
||||||
coins_maximum: CoinsMaximum
|
coins_maximum: CoinsMaximum
|
||||||
|
thiefnet_locations: ThiefNetLocations
|
||||||
thiefnet_minimum: ThiefNetCostMinimum
|
thiefnet_minimum: ThiefNetCostMinimum
|
||||||
thiefnet_maximum: ThiefNetCostMaximum
|
thiefnet_maximum: ThiefNetCostMaximum
|
||||||
|
|
||||||
@@ -113,6 +123,7 @@ sly3_option_groups = [
|
|||||||
CoinsMaximum
|
CoinsMaximum
|
||||||
]),
|
]),
|
||||||
OptionGroup("Locations",[
|
OptionGroup("Locations",[
|
||||||
|
ThiefNetLocations,
|
||||||
ThiefNetCostMinimum,
|
ThiefNetCostMinimum,
|
||||||
ThiefNetCostMaximum
|
ThiefNetCostMaximum
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -35,15 +35,14 @@ def gen_crew(world: "Sly3World") -> list[Item]:
|
|||||||
return crew
|
return crew
|
||||||
|
|
||||||
def gen_episodes(world: "Sly3World") -> list[Item]:
|
def gen_episodes(world: "Sly3World") -> list[Item]:
|
||||||
"""Generate the progressive episodes items for the item pool"""
|
"""Generate the episodes items for the item pool"""
|
||||||
all_episodes = [
|
all_episodes = [
|
||||||
item_name for item_name in item_groups["Episode"]
|
item_name for item_name in item_groups["Episode"]
|
||||||
for _ in range(4)
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# Make sure the starting episode is precollected
|
# Make sure the starting episode is precollected
|
||||||
starting_episode_n = world.options.starting_episode.value
|
starting_episode_n = world.options.starting_episode.value
|
||||||
starting_episode = f"Progressive {list(EPISODES.keys())[starting_episode_n]}"
|
starting_episode = list(EPISODES.keys())[starting_episode_n]
|
||||||
all_episodes.remove(starting_episode)
|
all_episodes.remove(starting_episode)
|
||||||
world.multiworld.push_precollected(world.create_item(starting_episode))
|
world.multiworld.push_precollected(world.create_item(starting_episode))
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import typing
|
import typing
|
||||||
|
|
||||||
from BaseClasses import Region, CollectionState, Location
|
from BaseClasses import Region, CollectionState
|
||||||
|
|
||||||
from .data.Locations import location_dict
|
from .data.Locations import location_dict
|
||||||
from .data.Constants import EPISODES, CHALLENGES
|
from .data.Constants import EPISODES, CHALLENGES, REQUIREMENTS
|
||||||
|
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
from . import Sly3World
|
from . import Sly3World
|
||||||
@@ -13,40 +13,21 @@ def create_access_rule(episode: str, n: int, options: "Sly3Options", player: int
|
|||||||
"""Returns a function that checks if the player has access to a specific region"""
|
"""Returns a function that checks if the player has access to a specific region"""
|
||||||
def rule(state: CollectionState):
|
def rule(state: CollectionState):
|
||||||
access = True
|
access = True
|
||||||
item_name = f"Progressive {episode}"
|
|
||||||
if episode == "Honor Among Thieves":
|
if episode == "Honor Among Thieves":
|
||||||
access = access and state.count_group("Crew", player) == 7
|
access = access and state.count_group("Crew", player) == 7
|
||||||
else:
|
else:
|
||||||
access = access and state.count(item_name, player) >= n
|
access = access and state.count(episode, player) == 1
|
||||||
|
|
||||||
if n > 1:
|
if n > 1:
|
||||||
requirements = sum({
|
section_requirements = [
|
||||||
"An Opera of Fear": [
|
sum(
|
||||||
[],
|
ep_reqs,
|
||||||
["Binocucom", "Bentley"],
|
[]
|
||||||
["Carmelita", "Murray", "Ball Form", "Disguise (Venice)"]
|
)
|
||||||
],
|
for ep_reqs
|
||||||
"Rumble Down Under": [
|
in REQUIREMENTS["Jobs"][episode][:n-1]
|
||||||
[],
|
|
||||||
["Murray", "Guru"],
|
|
||||||
["Bentley"]
|
|
||||||
],
|
|
||||||
"Flight of Fancy": [
|
|
||||||
[],
|
|
||||||
["Murray", "Bentley", "Guru", "Fishing Pole", "Penelope"],
|
|
||||||
["Hover Pack", "Carmelita", "Binocucom"]
|
|
||||||
],
|
|
||||||
"A Cold Alliance": [
|
|
||||||
["Bentley", "Murray", "Guru", "Penelope", "Binocucom"],
|
|
||||||
["Disguise (Photographer)", "Grapple-Cam", "Panda King"],
|
|
||||||
["Carmelita"]
|
|
||||||
],
|
|
||||||
"Dead Men Tell No Tales": [
|
|
||||||
[],
|
|
||||||
["Bentley", "Penelope", "Grapple-Cam", "Murray", "Silent Obliteration", "Treasure Map"],
|
|
||||||
["Panda King", "Dimitri"]
|
|
||||||
]
|
]
|
||||||
}[episode][:n-2], [])
|
requirements = list(set(sum(section_requirements, [])))
|
||||||
access = access and all(state.has(i, player) for i in requirements)
|
access = access and all(state.has(i, player) for i in requirements)
|
||||||
|
|
||||||
return access
|
return access
|
||||||
@@ -59,12 +40,13 @@ def create_regions_sly3(world: "Sly3World"):
|
|||||||
menu = Region("Menu", world.player, world.multiworld)
|
menu = Region("Menu", world.player, world.multiworld)
|
||||||
menu.add_locations({
|
menu.add_locations({
|
||||||
f"ThiefNet {i+1:02}": location_dict[f"ThiefNet {i+1:02}"].code
|
f"ThiefNet {i+1:02}": location_dict[f"ThiefNet {i+1:02}"].code
|
||||||
for i in range(37)
|
for i in range(world.options.thiefnet_locations)
|
||||||
})
|
})
|
||||||
|
|
||||||
world.multiworld.regions.append(menu)
|
world.multiworld.regions.append(menu)
|
||||||
|
|
||||||
for i, episode in enumerate(EPISODES.keys()):
|
for i, episode in enumerate(EPISODES.keys()):
|
||||||
|
print(f"==={episode}===")
|
||||||
for n in range(1,5):
|
for n in range(1,5):
|
||||||
if n == 2 and episode == "Honor Among Thieves":
|
if n == 2 and episode == "Honor Among Thieves":
|
||||||
break
|
break
|
||||||
@@ -80,6 +62,7 @@ def create_regions_sly3(world: "Sly3World"):
|
|||||||
})
|
})
|
||||||
|
|
||||||
world.multiworld.regions.append(region)
|
world.multiworld.regions.append(region)
|
||||||
|
|
||||||
menu.connect(
|
menu.connect(
|
||||||
region,
|
region,
|
||||||
None,
|
None,
|
||||||
|
|||||||
178
Sly3Rules.py
178
Sly3Rules.py
@@ -4,35 +4,32 @@ from math import ceil
|
|||||||
from BaseClasses import CollectionState
|
from BaseClasses import CollectionState
|
||||||
|
|
||||||
from worlds.generic.Rules import add_rule
|
from worlds.generic.Rules import add_rule
|
||||||
from .data.Constants import EPISODES
|
from .data.Constants import EPISODES, CHALLENGES, REQUIREMENTS
|
||||||
|
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
from . import Sly3World
|
from . import Sly3World
|
||||||
|
|
||||||
def set_rules_sly3(world: "Sly3World"):
|
def set_rules_sly3(world: "Sly3World"):
|
||||||
player = world.player
|
player = world.player
|
||||||
|
thiefnet_items = world.options.thiefnet_locations.value
|
||||||
|
|
||||||
# Putting ThiefNet stuff out of logic, to make early game less slow.
|
# Putting ThiefNet stuff out of logic, to make early game less slow.
|
||||||
# Divides the items into 8 groups of 3. First groups requires 2 episodes
|
# Divides the items into groups that require a number of episode and crew
|
||||||
# items to be in logic, second group requires 4, etc.
|
# items to be in logic
|
||||||
for i in range(1,35):
|
for i in range(1,thiefnet_items):
|
||||||
episode_items_n = ceil(i/4)*2
|
divisor = ceil(thiefnet_items/12)
|
||||||
|
episode_items_n = ceil(i/divisor)
|
||||||
add_rule(
|
add_rule(
|
||||||
world.get_location(f"ThiefNet {i:02}"),
|
world.get_location(f"ThiefNet {i:02}"),
|
||||||
lambda state, n=episode_items_n: (
|
lambda state, n=episode_items_n: (
|
||||||
state.has_group("Episode", player, n)
|
(
|
||||||
|
state.count_group("Episode", player) +
|
||||||
|
state.count_group("Crew", player)
|
||||||
|
) >= n
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def require(location: str, item: str|list[str]):
|
def require(location: str, item: str|list[str]):
|
||||||
if isinstance(item,str):
|
|
||||||
add_rule(
|
|
||||||
world.get_location(location),
|
|
||||||
lambda state, i=item: (
|
|
||||||
state.has(i, player)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
add_rule(
|
add_rule(
|
||||||
world.get_location(location),
|
world.get_location(location),
|
||||||
lambda state, i=item: (
|
lambda state, i=item: (
|
||||||
@@ -41,139 +38,34 @@ def set_rules_sly3(world: "Sly3World"):
|
|||||||
)
|
)
|
||||||
|
|
||||||
### Job requirements
|
### Job requirements
|
||||||
## An Opera of fear
|
for episode, sections in EPISODES.items():
|
||||||
# An Opera of Fear - Police HQ
|
if episode == "Honor Among Thieves":
|
||||||
|
continue
|
||||||
|
|
||||||
require("An Opera of Fear - Octavio Snap", "Binocucom")
|
for i, s in enumerate(sections):
|
||||||
# An Opera of Fear - Into the Depths
|
for j, job in enumerate(s):
|
||||||
require("An Opera of Fear - Canal Chase", "Bentley")
|
reqs = REQUIREMENTS["Jobs"][episode][i][j]
|
||||||
|
add_rule(
|
||||||
require("An Opera of Fear - Turf War!", "Carmelita")
|
world.get_location(f"{episode} - {job}"),
|
||||||
require("An Opera of Fear - Tar Ball", ["Murray", "Ball Form"])
|
lambda state, items=reqs: (
|
||||||
# An Opera of Fear - Run 'n Bomb
|
all(state.has(item, player) for item in items)
|
||||||
require("An Opera of Fear - Guard Duty", "Disguise (Venice)")
|
)
|
||||||
|
)
|
||||||
require("An Opera of Fear - Operation: Tar-Be Gone!", "Bombs")
|
|
||||||
|
|
||||||
## Rumble Down Under
|
|
||||||
# Rumble Down Under - Search for the Guru
|
|
||||||
|
|
||||||
require("Rumble Down Under - Spelunking", "Murray")
|
|
||||||
# Rumble Down Under - Dark Caves
|
|
||||||
# Rumble Down Under - Big Truck
|
|
||||||
require("Rumble Down Under - Unleash the Guru", "Guru")
|
|
||||||
|
|
||||||
# Rumble Down Under - The Claw
|
|
||||||
require("Rumble Down Under - Lemon Rage", "Bentley")
|
|
||||||
# Rumble Down Under - Hungry Croc
|
|
||||||
|
|
||||||
# Rumble Down Under - Operation: Moon Crash
|
|
||||||
|
|
||||||
## Flight of Fancy
|
|
||||||
# Flight of Fancy - Hidden Flight Roster
|
|
||||||
|
|
||||||
require("Flight of Fancy - Frame Team Belgium", ["Murray", "Bentley", "Guru", "Fishing Pole"])
|
|
||||||
require("Flight of Fancy - Frame Team Iceland", "Murray")
|
|
||||||
require("Flight of Fancy - Cooper Hangar Defense", "Penelope")
|
|
||||||
require("Flight of Fancy - ACES Semifinals", ["Murray", "Bentley", "Guru", "Fishing Pole", "Penelope"])
|
|
||||||
|
|
||||||
require("Flight of Fancy - Giant Wolf Massacre", "Binocucom")
|
|
||||||
require("Flight of Fancy - Windmill Firewall", "Hover Pack")
|
|
||||||
require("Flight of Fancy - Beauty and the Beast", "Carmelita")
|
|
||||||
|
|
||||||
require("Flight of Fancy - Operation: Turbo Dominant Eagle", "Paraglider")
|
|
||||||
|
|
||||||
## A Cold Alliance
|
|
||||||
require("A Cold Alliance - King of Fire", ["Bentley", "Murray", "Guru", "Penelope", "Binocucom"])
|
|
||||||
|
|
||||||
require("A Cold Alliance - Get a Job", "Disguise (Photographer)")
|
|
||||||
require("A Cold Alliance - Tearful Reunion", "Panda King")
|
|
||||||
require("A Cold Alliance - Grapple-Cam Break-In", "Grapple-Cam")
|
|
||||||
require("A Cold Alliance - Laptop Retrieval", ["Disguise (Photographer)", "Panda King", "Grapple-Cam"])
|
|
||||||
|
|
||||||
# A Cold Alliance - Vampiric Defense
|
|
||||||
# A Cold Alliance - Down the Line
|
|
||||||
require("A Cold Alliance - A Battery of Peril", "Carmelita")
|
|
||||||
|
|
||||||
# A Cold Alliance - Operation: Wedding Crasher
|
|
||||||
|
|
||||||
## Dead Men Tell No Tales
|
|
||||||
require("Dead Men Tell No Tales - The Talk of Pirates", "Disguise (Pirate)")
|
|
||||||
|
|
||||||
require("Dead Men Tell No Tales - Dynamic Duo", ["Bentley", "Penelope", "Grapple-Cam"])
|
|
||||||
require("Dead Men Tell No Tales - Jollyboat of Destruction", "Murray")
|
|
||||||
require("Dead Men Tell No Tales - X Marks the Spot", ["Bentley", "Penelope", "Grapple-Cam", "Murray", "Silent Obliteration", "Treasure Map"])
|
|
||||||
|
|
||||||
require("Dead Men Tell No Tales - Crusher from the Depths", "Panda King")
|
|
||||||
require("Dead Men Tell No Tales - Deep Sea Danger", "Dimitri")
|
|
||||||
# Dead Men Tell No Tales - Battle on the High Seas
|
|
||||||
|
|
||||||
require("Dead Men Tell No Tales - Operation: Reverse Double-Cross", "Guru")
|
|
||||||
|
|
||||||
## Honor Among Thieves
|
|
||||||
# Honor Among Thieves - Carmelita to the Rescue
|
|
||||||
# Honor Among Thieves - A Deadly Bite
|
|
||||||
# Honor Among Thieves - The Dark Current
|
|
||||||
# Honor Among Thieves - Bump-Charge-Jump
|
|
||||||
# Honor Among Thieves - Danger in the Skie
|
|
||||||
# Honor Among Thieves - The Ancestors' Gauntlet
|
|
||||||
# Honor Among Thieves - Stand your Ground
|
|
||||||
# Honor Among Thieves - Final Legacy
|
|
||||||
|
|
||||||
### Challenge requirements
|
### Challenge requirements
|
||||||
## An Opera of Fear
|
for episode, sections in CHALLENGES.items():
|
||||||
require("An Opera of Fear - Canal Chase - Expert Course", "Bentley")
|
if episode == "Honor Among Thieves":
|
||||||
require("An Opera of Fear - Air Time", ["Murray", "Ball Form"])
|
continue
|
||||||
# An Opera of Fear - Tower Scramble
|
|
||||||
# An Opera of Fear - Coin Chase
|
|
||||||
require("An Opera of Fear - Speed Bombing", "Bombs")
|
|
||||||
# An Opera of Fear - Octavio Canal Challenge
|
|
||||||
# An Opera of Fear - Octavio's Last Stand
|
|
||||||
require("An Opera of Fear - Venice Treasure Hunt", "Treasure Map")
|
|
||||||
|
|
||||||
## Rumble Down Under
|
for i, s in enumerate(sections):
|
||||||
# Rumble Down Under - Rock Run
|
for j, challenge in enumerate(s):
|
||||||
# Rumble Down Under - Cave Sprint
|
reqs = REQUIREMENTS["Challenges"][episode][i][j]
|
||||||
# Rumble Down Under - Cave Mayhem
|
add_rule(
|
||||||
# Rumble Down Under - Scaling the Drill
|
world.get_location(f"{episode} - {challenge}"),
|
||||||
require("Rumble Down Under - Guard Swappin'", "Guru")
|
lambda state, items=reqs: (
|
||||||
# Rumble Down Under - Quick Claw
|
all(state.has(item, player) for item in items)
|
||||||
require("Rumble Down Under - Pressure Brawl", "Bentley")
|
)
|
||||||
# Rumble Down Under - Croc and Coins
|
)
|
||||||
# Rumble Down Under - Carmelita Climb
|
|
||||||
require("Rumble Down Under - Outback Treasure Hunt", "Treasure Map")
|
|
||||||
|
|
||||||
## Flight of Fancy
|
|
||||||
# Flight of Fancy - Castle Quick Climb
|
|
||||||
require("Flight of Fancy - Muggshot Goon Attack", "Penelope")
|
|
||||||
require("Flight of Fancy - Security Breach", "Penelope")
|
|
||||||
require("Flight of Fancy - Defend the Hangar", "Penelope")
|
|
||||||
require("Flight of Fancy - Precision Air Duel", ["Murray", "Bentley", "Guru", "Fishing Pole", "Penelope"])
|
|
||||||
require("Flight of Fancy - Wolf Rampage", "Guru")
|
|
||||||
require("Flight of Fancy - One Woman Army", "Carmelita")
|
|
||||||
require("Flight of Fancy - Going Out On A Wing", "Paraglider")
|
|
||||||
require("Flight of Fancy - Holland Treasure Hunt", "Treasure Map")
|
|
||||||
|
|
||||||
## A Cold Alliance
|
|
||||||
require("A Cold Alliance - Big Air in China", ["Bentley", "Murray", "Guru", "Penelope", "Binocucom"])
|
|
||||||
require("A Cold Alliance - Sharpshooter", "Panda King")
|
|
||||||
# A Cold Alliance - Treetop Tangle
|
|
||||||
# A Cold Alliance - Tsao Showdown
|
|
||||||
require("A Cold Alliance - China Treasure Hunt", "Treasure Map")
|
|
||||||
|
|
||||||
## Dead Men Tell No Tales
|
|
||||||
# Dead Men Tell No Tales - Patch Grab
|
|
||||||
# Dead Men Tell No Tales - Stealth Challenge
|
|
||||||
require("Dead Men Tell No Tales - Boat Bash", "Murray")
|
|
||||||
require("Dead Men Tell No Tales - Last Ship Sailing", ["Bentley", "Penelope", "Grapple-Cam", "Murray", "Silent Obliteration", "Treasure Map"])
|
|
||||||
# Dead Men Tell No Tales - Pirate Treasure Hunt
|
|
||||||
|
|
||||||
## Honor Among Thieves
|
|
||||||
# Beauty versus the Beast
|
|
||||||
# Road Rage
|
|
||||||
# Dr. M Dogfight
|
|
||||||
# Ultimate Gauntlet
|
|
||||||
# Battle Against Time
|
|
||||||
|
|
||||||
if world.options.goal.value < 6:
|
if world.options.goal.value < 6:
|
||||||
victory_condition = [
|
victory_condition = [
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ class Sly3World(World):
|
|||||||
self.options.include_mega_jump.value = slot_data["include_mega_jump"]
|
self.options.include_mega_jump.value = slot_data["include_mega_jump"]
|
||||||
self.options.coins_minimum.value = slot_data["coins_minimum"]
|
self.options.coins_minimum.value = slot_data["coins_minimum"]
|
||||||
self.options.coins_maximum.value = slot_data["coins_maximum"]
|
self.options.coins_maximum.value = slot_data["coins_maximum"]
|
||||||
|
self.options.thiefnet_locations.value = slot_data["thiefnet_locations"]
|
||||||
self.options.thiefnet_minimum.value = slot_data["thiefnet_minimum"]
|
self.options.thiefnet_minimum.value = slot_data["thiefnet_minimum"]
|
||||||
self.options.thiefnet_maximum.value = slot_data["thiefnet_maximum"]
|
self.options.thiefnet_maximum.value = slot_data["thiefnet_maximum"]
|
||||||
return
|
return
|
||||||
@@ -155,6 +156,7 @@ class Sly3World(World):
|
|||||||
"include_mega_jump",
|
"include_mega_jump",
|
||||||
"coins_minimum",
|
"coins_minimum",
|
||||||
"coins_maximum",
|
"coins_maximum",
|
||||||
|
"thiefnet_locations",
|
||||||
"thiefnet_minimum",
|
"thiefnet_minimum",
|
||||||
"thiefnet_maximum",
|
"thiefnet_maximum",
|
||||||
)
|
)
|
||||||
@@ -162,6 +164,7 @@ class Sly3World(World):
|
|||||||
def fill_slot_data(self) -> Mapping[str, Any]:
|
def fill_slot_data(self) -> Mapping[str, Any]:
|
||||||
slot_data = self.get_options_as_dict()
|
slot_data = self.get_options_as_dict()
|
||||||
slot_data["thiefnet_costs"] = self.thiefnet_costs
|
slot_data["thiefnet_costs"] = self.thiefnet_costs
|
||||||
|
slot_data["world_version"] = self.world_version
|
||||||
|
|
||||||
return slot_data
|
return slot_data
|
||||||
|
|
||||||
|
|||||||
6
archipelago.json
Normal file
6
archipelago.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"compatible_version": 7,
|
||||||
|
"version": 7,
|
||||||
|
"game": "Sly 3: Honor Among Thieves",
|
||||||
|
"world_version": "0.0.0"
|
||||||
|
}
|
||||||
@@ -203,16 +203,193 @@ CHALLENGES = {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Jobs/Challenges -> episode -> section -> job
|
||||||
|
# dict[ list[ list[ list[]]]]
|
||||||
|
REQUIREMENTS = {
|
||||||
|
"Jobs": {
|
||||||
|
"An Opera of Fear": [
|
||||||
|
[[]],
|
||||||
|
[
|
||||||
|
["Binocucom"],
|
||||||
|
[],
|
||||||
|
["Bentley"],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["Carmelita"],
|
||||||
|
["Murray", "Ball Form"],
|
||||||
|
[],
|
||||||
|
["Disguise (Venice)"],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["Bombs"]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"Rumble Down Under" :[
|
||||||
|
[[]],
|
||||||
|
[
|
||||||
|
["Murray"],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
["Guru"],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[],
|
||||||
|
["Bentley"],
|
||||||
|
[]
|
||||||
|
],
|
||||||
|
[[]]
|
||||||
|
],
|
||||||
|
"Flight of Fancy": [
|
||||||
|
[[]],
|
||||||
|
[
|
||||||
|
["Murray", "Bentley", "Guru", "Fishing Pole"],
|
||||||
|
["Murray"],
|
||||||
|
["Penelope"],
|
||||||
|
["Murray", "Bentley", "Guru", "Fishing Pole", "Penelope"]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["Binocucom"],
|
||||||
|
["Hover Pack"],
|
||||||
|
["Carmelita"]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["Paraglider"]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"A Cold Alliance": [
|
||||||
|
[
|
||||||
|
["Bentley", "Murray", "Guru", "Penelope", "Binocucom"]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["Disguise (Photographer)"],
|
||||||
|
["Panda King"],
|
||||||
|
["Grapple-Cam"],
|
||||||
|
["Disguise (Photographer)", "Panda King", "Grapple-Cam"]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
["Carmelita"]
|
||||||
|
],
|
||||||
|
[[]]
|
||||||
|
],
|
||||||
|
"Dead Men Tell No Tales": [
|
||||||
|
[
|
||||||
|
["Disguise (Pirate)"]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["Bentley", "Penelope", "Grapple-Cam"],
|
||||||
|
["Murray"],
|
||||||
|
["Bentley", "Penelope", "Grapple-Cam", "Murray", "Silent Obliteration", "Treasure Map"]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["Panda King"],
|
||||||
|
["Dimitri"],
|
||||||
|
[]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["Guru"]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"Challenges": {
|
||||||
|
"An Opera of Fear": [
|
||||||
|
[],
|
||||||
|
[
|
||||||
|
["Bentley"]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["Murray", "Ball Form"],
|
||||||
|
[],
|
||||||
|
[]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["Bombs"],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
["Treasure Map"]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"Rumble Down Under" :[
|
||||||
|
[[]],
|
||||||
|
[
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
["Guru"]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[],
|
||||||
|
["Bentley"],
|
||||||
|
[]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[],
|
||||||
|
["Treasure Map"]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"Flight of Fancy": [
|
||||||
|
[[]],
|
||||||
|
[
|
||||||
|
["Penelope"],
|
||||||
|
["Penelope"],
|
||||||
|
["Penelope"],
|
||||||
|
["Murray", "Bentley", "Guru", "Fishing Pole", "Penelope"],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[],
|
||||||
|
["Carmelita"]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["Paraglider"],
|
||||||
|
["Treasure Map"]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"A Cold Alliance": [
|
||||||
|
[
|
||||||
|
["Bentley", "Murray", "Guru", "Penelope", "Binocucom"]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["Panda King"],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
[]
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
[
|
||||||
|
["Treasure Map"]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"Dead Men Tell No Tales": [
|
||||||
|
[
|
||||||
|
["Disguise (Pirate)"],
|
||||||
|
["Disguise (Pirate)"]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["Murray"],
|
||||||
|
["Bentley", "Penelope", "Grapple-Cam", "Murray", "Silent Obliteration", "Treasure Map"]
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
[[]]
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ADDRESSES = {
|
ADDRESSES = {
|
||||||
"SCUS-97464" : {
|
"SCUS-97464" : {
|
||||||
|
"world id": 0x468D30,
|
||||||
"map id": 0x47989C,
|
"map id": 0x47989C,
|
||||||
"job id": 0x36DB98,
|
"job id": 0x36DB98,
|
||||||
|
"loading": 0x467B00,
|
||||||
"reload": 0x4797C4,
|
"reload": 0x4797C4,
|
||||||
"reload values": 0x4797CC,
|
"reload values": 0x4797CC,
|
||||||
"episode unlocks": 0x56AEC8,
|
"episode unlocks": 0x56AEC8,
|
||||||
"frame counter": 0x389BE0,
|
"frame counter": 0x389BE0,
|
||||||
"x pressed": 0x36E78E,
|
"x pressed": 0x36E78E,
|
||||||
"skip cutscene": 0x389C20,
|
"skip cutscene": 0x389C20,
|
||||||
|
"gadgets": 0x468DCC,
|
||||||
|
"coins": 0x468DDC,
|
||||||
|
"DAG root": 0x478C8C,
|
||||||
"jobs": [
|
"jobs": [
|
||||||
[
|
[
|
||||||
[0x1335d10]
|
[0x1335d10]
|
||||||
@@ -227,7 +404,52 @@ ADDRESSES = {
|
|||||||
],
|
],
|
||||||
"text": {
|
"text": {
|
||||||
"powerups": [
|
"powerups": [
|
||||||
{},
|
{
|
||||||
|
"Trigger Bomb": (0x58db60,0x58dcf0),
|
||||||
|
"Fishing Pole": (0x595da0,0x595fc0),
|
||||||
|
"Alarm Clock": (0x591db0,0x591f40),
|
||||||
|
"Adrenaline Burst": (0x58e800,0x58e9c0),
|
||||||
|
"Health Extractor": (0x58ebe0,0x58ee00),
|
||||||
|
"Hover Pack": (0x58ef90,0x58f1b0),
|
||||||
|
"Insanity Strike": (0x593a40,0x593b70),
|
||||||
|
"Grapple-Cam": (0x5957d0,0x595ae0),
|
||||||
|
"Size Destabilizer": (0x58df70,0x58e170),
|
||||||
|
"Rage Bomb": (0x594160,0x5942d0),
|
||||||
|
"Reduction Bomb": (0x58f260,0x58f390),
|
||||||
|
"Be The Ball": (0x5955c0,0x595730),
|
||||||
|
"Berserker Charge": (0x5912d0,0x591380),
|
||||||
|
"Juggernaut Throw": (0x590730,0x590850),
|
||||||
|
"Guttural Roar": (0x5914e0,0x591610),
|
||||||
|
"Fists of Flame": (0x58f960,0x5900b0),
|
||||||
|
"Temporal Lock": (0x58f440,0x58f5a0),
|
||||||
|
"Raging Inferno Flop": (0x5916c0,0x5917f0),
|
||||||
|
"Diablo Fire Slam": (0x590fa0,0x591090),
|
||||||
|
"Smoke Bomb": (0x5918f0,0x591a00),
|
||||||
|
"Combat Dodge": (0x591b40,0x591c90),
|
||||||
|
"Paraglide": (0x5921f0,0x5924c0),
|
||||||
|
"Silent Obliteration": (0x592690,0x592870),
|
||||||
|
"Feral Pounce": (0x592c50,0x592de0),
|
||||||
|
"Mega Jump": (0x592fc0,0x593180),
|
||||||
|
"Knockout Dive": (0x5936d0,0x5938e0),
|
||||||
|
"Shadow Power Level 1": (0x594770,0x594880),
|
||||||
|
"Thief Reflexes": (0x592a10,0x592b50),
|
||||||
|
"Shadow Power Level 2": (0x5949e0,0x594d00),
|
||||||
|
"Rocket Boots": (0x577060,0x577300),
|
||||||
|
"Treasure Map": (0x576af0,0x576dc0),
|
||||||
|
"ENGLISHpowerup_shield_name": (0x596280,0x576450),
|
||||||
|
"Venice Disguise": (0x577510,0x577670),
|
||||||
|
"Photographer Disguise": (0x5778f0,0x577ac0),
|
||||||
|
"Pirate Disguise": (0x577ca0,0x577e20),
|
||||||
|
"Spin Attack Level 1": (0x577fe0,0x5781b0),
|
||||||
|
"Spin Attack Level 2": (0x578350,0x578500),
|
||||||
|
"Spin Attack Level 3": (0x578770,0x578af0),
|
||||||
|
"Jump Attack Level 1": (0x578d80,0x579070),
|
||||||
|
"Jump Attack Level 2": (0x579390,0x579620),
|
||||||
|
"Jump Attack Level 3": (0x5797b0,0x579950),
|
||||||
|
"Push Attack Level 1": (0x579ae0,0x579d70),
|
||||||
|
"Push Attack Level 2": (0x579f70,0x57a1f0),
|
||||||
|
"Push Attack Level 3": (0x57a670,0x57a940),
|
||||||
|
},
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
"Trigger Bomb": (0x592c40,0x592e00),
|
"Trigger Bomb": (0x592c40,0x592e00),
|
||||||
|
|||||||
@@ -70,16 +70,16 @@ crew_list = [
|
|||||||
("Carmelita", ItemClassification.progression, "Crew")
|
("Carmelita", ItemClassification.progression, "Crew")
|
||||||
]
|
]
|
||||||
|
|
||||||
progressive_episode_list = [
|
episode_list = [
|
||||||
(f"Progressive {e}", ItemClassification.progression, "Episode")
|
(episode, ItemClassification.progression, "Episode")
|
||||||
for e in list(EPISODES.keys())[:-1]
|
for episode in list(EPISODES.keys())[:-1]
|
||||||
]
|
]
|
||||||
|
|
||||||
item_list = (
|
item_list = (
|
||||||
filler_list +
|
filler_list +
|
||||||
powerup_list +
|
powerup_list +
|
||||||
crew_list +
|
crew_list +
|
||||||
progressive_episode_list
|
episode_list
|
||||||
)
|
)
|
||||||
|
|
||||||
base_code = 5318008
|
base_code = 5318008
|
||||||
|
|||||||
244
data/jobs.md
Normal file
244
data/jobs.md
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
# Opera of Fear
|
||||||
|
|
||||||
|
## Police HQ (sly)
|
||||||
|
Job ID: 2085
|
||||||
|
Job address: 0x1335d10
|
||||||
|
Job index: 2
|
||||||
|
Job state (should be 2): 2
|
||||||
|
|
||||||
|
## Octavio Snap (sly)
|
||||||
|
Job ID: 2230
|
||||||
|
Job address: 0x1350560
|
||||||
|
Job index: 54
|
||||||
|
Job state (should be 2): 2
|
||||||
|
|
||||||
|
## Into the Depths (sly)
|
||||||
|
Job ID: 2283
|
||||||
|
Job address: 0x1357f80
|
||||||
|
Job index: 68
|
||||||
|
Job state (should be 2): 2
|
||||||
|
|
||||||
|
## Canal Chase (bentley)
|
||||||
|
Job ID: 2329
|
||||||
|
Job address: 0x844ff0
|
||||||
|
Job index: 87
|
||||||
|
Job state (should be 2): 2
|
||||||
|
|
||||||
|
## Turf War! (sly)
|
||||||
|
Job ID: 2139
|
||||||
|
Job address: 0x1330c40
|
||||||
|
Job index: 19
|
||||||
|
Job state (should be 2): 2
|
||||||
|
|
||||||
|
## Tar Ball (bentley)
|
||||||
|
Job ID: 2168
|
||||||
|
Job address: 0x1335dc0
|
||||||
|
Job index: 29
|
||||||
|
Job state (should be 2): 2
|
||||||
|
|
||||||
|
## Run 'n Bomb (sly)
|
||||||
|
Job ID: 2187
|
||||||
|
Job address: 0x133e9b0
|
||||||
|
Job index: 39
|
||||||
|
Job state (should be 2): 2
|
||||||
|
|
||||||
|
## Guard Duty (sly)
|
||||||
|
Job ID: 2352
|
||||||
|
Job address: 0x1351520
|
||||||
|
Job index: 95
|
||||||
|
Job state (should be 2): 2
|
||||||
|
|
||||||
|
## Operation: Tar-Be Gone! (sly)
|
||||||
|
Job ID: 2419
|
||||||
|
Job address: 0x135e550
|
||||||
|
Job index: 120
|
||||||
|
Job state (should be 2): 2
|
||||||
|
|
||||||
|
# Rumble Down Under
|
||||||
|
|
||||||
|
## Search for the Guru
|
||||||
|
Job ID: 2577
|
||||||
|
Job address: 0x6b4250
|
||||||
|
Job index: 3
|
||||||
|
Job state (should be 2): 2
|
||||||
|
|
||||||
|
|
||||||
|
## Spelunking (Murray)
|
||||||
|
Job ID: 2596
|
||||||
|
Job address: 0x6b80f0
|
||||||
|
Job index: 14
|
||||||
|
Job state (should be 2): 2
|
||||||
|
|
||||||
|
## Dark Caves (sly)
|
||||||
|
Job ID: 2805
|
||||||
|
Job address: 0x6d0770
|
||||||
|
Job index: 84
|
||||||
|
Job state (should be 2): 2
|
||||||
|
|
||||||
|
## Big Truck (murray)
|
||||||
|
Job ID: 2695
|
||||||
|
Job address: 0x5d26f0
|
||||||
|
Job index: 59
|
||||||
|
Job state (should be 2): 2
|
||||||
|
|
||||||
|
## Unleash the Guru (bentley)
|
||||||
|
Job ID: 2663
|
||||||
|
Job address: 0x6bdaf0
|
||||||
|
Job index: 42
|
||||||
|
Job state (should be 2): 2
|
||||||
|
|
||||||
|
|
||||||
|
## The Claw (sly)
|
||||||
|
Job ID: 2623
|
||||||
|
Job address: 0x5caa20
|
||||||
|
Job index: 25
|
||||||
|
Job state (should be 2): 2
|
||||||
|
|
||||||
|
## Lemon Rage (sly)
|
||||||
|
Job ID: 2730
|
||||||
|
Job address: 0x5fe390
|
||||||
|
Job index: 72
|
||||||
|
Job state (should be 2): 2
|
||||||
|
|
||||||
|
## Hungry Croc (murray)
|
||||||
|
Job ID: 2780
|
||||||
|
Job address: 0x6ca940
|
||||||
|
Job index: 80
|
||||||
|
Job state (should be 2): 2
|
||||||
|
|
||||||
|
|
||||||
|
## Operation: Moon Crash (sly)
|
||||||
|
(during mission)
|
||||||
|
Job ID: 2843
|
||||||
|
Job address: 0x6d4330
|
||||||
|
Job index: 102
|
||||||
|
Job state (should be 2): 2
|
||||||
|
|
||||||
|
or
|
||||||
|
(carmelita fight)
|
||||||
|
Job ID: 2843
|
||||||
|
Job address: 0x5deb20
|
||||||
|
Job index: 102
|
||||||
|
Job state (should be 2): 2
|
||||||
|
|
||||||
|
|
||||||
|
# Flight of Fancy
|
||||||
|
|
||||||
|
## Hidden Flight Roster
|
||||||
|
Job ID: 2983
|
||||||
|
Job address: 0x794360
|
||||||
|
Job index: 1
|
||||||
|
Job state (should be 2): 2
|
||||||
|
|
||||||
|
## Frame Team Belgium
|
||||||
|
Job ID: 3025
|
||||||
|
Job address: 0x7a0c20
|
||||||
|
Job index: 15
|
||||||
|
Job state (should be 2): 2
|
||||||
|
|
||||||
|
## Frame Team Iceland
|
||||||
|
Job ID: 3061
|
||||||
|
Job address: 0x65f3d0
|
||||||
|
Job index: 29
|
||||||
|
Job state (should be 2): 2
|
||||||
|
|
||||||
|
|
||||||
|
## Cooper Hangar Defense (murray)
|
||||||
|
Job ID: 3101
|
||||||
|
Job address: 0x663d80
|
||||||
|
Job index: 49
|
||||||
|
Job state (should be 2): 2
|
||||||
|
|
||||||
|
|
||||||
|
## ACES Semifinals (sly)
|
||||||
|
Job ID: 3140
|
||||||
|
Job address: 0xecb450
|
||||||
|
Job index: 62
|
||||||
|
Job state (should be 2): 2
|
||||||
|
|
||||||
|
|
||||||
|
## Beauty and the Beast (bentley)
|
||||||
|
Job ID: 3225
|
||||||
|
Job address: 0x7adf50
|
||||||
|
Job index: 95
|
||||||
|
Job state (should be 2): 2
|
||||||
|
|
||||||
|
## Giant Wolf Massacre (bentley)
|
||||||
|
Job ID: 3202
|
||||||
|
Job address: 0x642a90
|
||||||
|
Job index: 87
|
||||||
|
Job state (should be 2): 2
|
||||||
|
|
||||||
|
## Windmill Firewall (bentley)
|
||||||
|
Job ID: 3164
|
||||||
|
Job address: 0x63b4d0
|
||||||
|
Job index: 70
|
||||||
|
Job state (should be 2): 2
|
||||||
|
|
||||||
|
## Operation: Turbo Dominant Eagle (sly)
|
||||||
|
Job ID: 3259
|
||||||
|
Job address: 0x651eb0
|
||||||
|
Job index: 108
|
||||||
|
Job state (should be 2): 2
|
||||||
|
|
||||||
|
|
||||||
|
# A Cold Alliance
|
||||||
|
|
||||||
|
##
|
||||||
|
Job ID: 3381
|
||||||
|
Job address: 0x67de40
|
||||||
|
Job index: 1
|
||||||
|
Job state (should be 2): 2
|
||||||
|
|
||||||
|
## Grapple-Cam Break-in (sly)
|
||||||
|
Job ID: 3540
|
||||||
|
Job address: 0x1625780
|
||||||
|
Job index: 63
|
||||||
|
Job state (should be 2): 2
|
||||||
|
|
||||||
|
|
||||||
|
## Get a Job (bentley)
|
||||||
|
Job ID: 3449
|
||||||
|
Job address: 0x682ba0
|
||||||
|
Job index: 36
|
||||||
|
Job state (should be 2): 2
|
||||||
|
|
||||||
|
|
||||||
|
## Tearful Reunion (murray)
|
||||||
|
Job ID: 3509
|
||||||
|
Job address: 0x16200f0
|
||||||
|
Job index: 52
|
||||||
|
Job state (should be 2): 2
|
||||||
|
|
||||||
|
## Laptop Retrieval (bentley)
|
||||||
|
Job ID: 3584
|
||||||
|
Job address: 0x68e560
|
||||||
|
Job index: 82
|
||||||
|
Job state (should be 2): 2
|
||||||
|
|
||||||
|
|
||||||
|
## Down the Line (murray)
|
||||||
|
Job ID: 3672
|
||||||
|
Job address: 0x16377e0
|
||||||
|
Job index: 117
|
||||||
|
Job state (should be 2): 2
|
||||||
|
|
||||||
|
|
||||||
|
## Vampiric Demise (sly)
|
||||||
|
Job ID: 3629
|
||||||
|
Job address: 0x1630060
|
||||||
|
Job index: 97
|
||||||
|
Job state (should be 2): 2
|
||||||
|
|
||||||
|
|
||||||
|
## A Battery of Peril (bentley)
|
||||||
|
Job ID: 3684
|
||||||
|
Job address: 0x163a930
|
||||||
|
Job index: 121
|
||||||
|
Job state (should be 2): 2
|
||||||
|
|
||||||
|
## Operation: Wedding Crasher! (sly)
|
||||||
|
Job ID: 3712
|
||||||
|
Job address: 0x6bf580
|
||||||
|
Job index: 129
|
||||||
|
Job state (should be 2): 2
|
||||||
5364
data/savefile_data.txt
Normal file
5364
data/savefile_data.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -34,33 +34,33 @@ class Pine:
|
|||||||
""" IPC result codes. A list of possible result codes the IPC can send back. Each one of them is what we call an
|
""" IPC result codes. A list of possible result codes the IPC can send back. Each one of them is what we call an
|
||||||
"opcode" or "tag" and is the first byte sent by the IPC to differentiate between results.
|
"opcode" or "tag" and is the first byte sent by the IPC to differentiate between results.
|
||||||
"""
|
"""
|
||||||
IPC_OK = 0, # IPC command successfully completed.
|
IPC_OK = 0 # IPC command successfully completed.
|
||||||
IPC_FAIL = 0xFF # IPC command failed to complete.
|
IPC_FAIL = 0xFF # IPC command failed to complete.
|
||||||
|
|
||||||
class IPCCommand(IntEnum):
|
class IPCCommand(IntEnum):
|
||||||
READ8 = 0,
|
READ8 = 0
|
||||||
READ16 = 1,
|
READ16 = 1
|
||||||
READ32 = 2,
|
READ32 = 2
|
||||||
READ64 = 3,
|
READ64 = 3
|
||||||
WRITE8 = 4,
|
WRITE8 = 4
|
||||||
WRITE16 = 5,
|
WRITE16 = 5
|
||||||
WRITE32 = 6,
|
WRITE32 = 6
|
||||||
WRITE64 = 7,
|
WRITE64 = 7
|
||||||
VERSION = 8,
|
VERSION = 8
|
||||||
SAVE_STATE = 9,
|
SAVE_STATE = 9
|
||||||
LOAD_STATE = 0xA,
|
LOAD_STATE = 0xA
|
||||||
TITLE = 0xB,
|
TITLE = 0xB
|
||||||
ID = 0xC,
|
ID = 0xC
|
||||||
UUID = 0xD,
|
UUID = 0xD
|
||||||
GAME_VERSION = 0xE,
|
GAME_VERSION = 0xE
|
||||||
STATUS = 0xF,
|
STATUS = 0xF
|
||||||
UNIMPLEMENTED = 0xFF,
|
UNIMPLEMENTED = 0xFF
|
||||||
|
|
||||||
class DataSize(IntEnum):
|
class DataSize(IntEnum):
|
||||||
INT8 = 1,
|
INT8 = 1
|
||||||
INT16 = 2,
|
INT16 = 2
|
||||||
INT32 = 4,
|
INT32 = 4
|
||||||
INT64 = 8,
|
INT64 = 8
|
||||||
|
|
||||||
def __init__(self, slot: int = 28011):
|
def __init__(self, slot: int = 28011):
|
||||||
if not 0 < slot <= 65536:
|
if not 0 < slot <= 65536:
|
||||||
@@ -161,7 +161,7 @@ class Pine:
|
|||||||
|
|
||||||
def write_float(self, address: int, value: float) -> None:
|
def write_float(self, address: int, value: float) -> None:
|
||||||
request = Pine._create_request(Pine.IPCCommand.WRITE32, address, 9 + Pine.DataSize.INT32)
|
request = Pine._create_request(Pine.IPCCommand.WRITE32, address, 9 + Pine.DataSize.INT32)
|
||||||
request + struct.pack("<f", value)
|
request += struct.pack("<f", value)
|
||||||
self._send_request(request)
|
self._send_request(request)
|
||||||
|
|
||||||
def write_bytes(self, address: int, data: bytes) -> None:
|
def write_bytes(self, address: int, data: bytes) -> None:
|
||||||
@@ -189,9 +189,124 @@ class Pine:
|
|||||||
self._send_request(request)
|
self._send_request(request)
|
||||||
bytes_written += 1
|
bytes_written += 1
|
||||||
|
|
||||||
|
def batch_read_int32(self, addresses: list[int]) -> list[int]:
|
||||||
|
"""Read multiple int32 values in a single request. Much faster than individual reads."""
|
||||||
|
if not addresses:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Build batched request
|
||||||
|
request = b''
|
||||||
|
for address in addresses:
|
||||||
|
request += Pine.to_bytes(Pine.IPCCommand.READ32, 1)
|
||||||
|
request += Pine.to_bytes(address, 4)
|
||||||
|
|
||||||
|
# Prepend total size
|
||||||
|
total_size = len(request) + 4
|
||||||
|
request = Pine.to_bytes(total_size, 4) + request
|
||||||
|
|
||||||
|
response = self._send_request(request)
|
||||||
|
|
||||||
|
# Parse results - one status byte for the entire batch, then data
|
||||||
|
results = []
|
||||||
|
offset = 4 # Skip overall size header
|
||||||
|
offset += 1 # Skip single status byte for entire batch
|
||||||
|
for _ in addresses:
|
||||||
|
value = Pine.from_bytes(response[offset:offset + 4])
|
||||||
|
results.append(value)
|
||||||
|
offset += 4
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def batch_read_int16(self, addresses: list[int]) -> list[int]:
|
||||||
|
"""Read multiple int16 values in a single request."""
|
||||||
|
if not addresses:
|
||||||
|
return []
|
||||||
|
|
||||||
|
request = b''
|
||||||
|
for address in addresses:
|
||||||
|
request += Pine.to_bytes(Pine.IPCCommand.READ16, 1)
|
||||||
|
request += Pine.to_bytes(address, 4)
|
||||||
|
|
||||||
|
total_size = len(request) + 4
|
||||||
|
request = Pine.to_bytes(total_size, 4) + request
|
||||||
|
|
||||||
|
response = self._send_request(request)
|
||||||
|
|
||||||
|
# Parse results - one status byte for the entire batch, then data
|
||||||
|
results = []
|
||||||
|
offset = 4 # Skip overall size header
|
||||||
|
offset += 1 # Skip single status byte for entire batch
|
||||||
|
for _ in addresses:
|
||||||
|
value = Pine.from_bytes(response[offset:offset + 2])
|
||||||
|
results.append(value)
|
||||||
|
offset += 2
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def batch_read_int8(self, addresses: list[int]) -> list[int]:
|
||||||
|
"""Read multiple int8 values in a single request."""
|
||||||
|
if not addresses:
|
||||||
|
return []
|
||||||
|
|
||||||
|
request = b''
|
||||||
|
for address in addresses:
|
||||||
|
request += Pine.to_bytes(Pine.IPCCommand.READ8, 1)
|
||||||
|
request += Pine.to_bytes(address, 4)
|
||||||
|
|
||||||
|
total_size = len(request) + 4
|
||||||
|
request = Pine.to_bytes(total_size, 4) + request
|
||||||
|
|
||||||
|
response = self._send_request(request)
|
||||||
|
|
||||||
|
# Parse results - one status byte for the entire batch, then data
|
||||||
|
results = []
|
||||||
|
offset = 4 # Skip overall size header
|
||||||
|
offset += 1 # Skip single status byte for entire batch
|
||||||
|
for _ in addresses:
|
||||||
|
value = Pine.from_bytes(response[offset:offset + 1])
|
||||||
|
results.append(value)
|
||||||
|
offset += 1
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def batch_write_int32(self, operations: list[tuple[int, int]]) -> None:
|
||||||
|
"""Write multiple int32 values in a single request. Each operation is (address, value)."""
|
||||||
|
if not operations:
|
||||||
|
return
|
||||||
|
|
||||||
|
request = b''
|
||||||
|
for address, value in operations:
|
||||||
|
request += Pine.to_bytes(Pine.IPCCommand.WRITE32, 1)
|
||||||
|
request += Pine.to_bytes(address, 4)
|
||||||
|
request += value.to_bytes(length=4, byteorder="little")
|
||||||
|
|
||||||
|
total_size = len(request) + 4
|
||||||
|
request = Pine.to_bytes(total_size, 4) + request
|
||||||
|
|
||||||
|
self._send_request(request)
|
||||||
|
|
||||||
|
def batch_write_float(self, operations: list[tuple[int, float]]) -> None:
|
||||||
|
"""Write multiple float values in a single request. Each operation is (address, value)."""
|
||||||
|
if not operations:
|
||||||
|
return
|
||||||
|
|
||||||
|
request = b''
|
||||||
|
for address, value in operations:
|
||||||
|
request += Pine.to_bytes(Pine.IPCCommand.WRITE32, 1)
|
||||||
|
request += Pine.to_bytes(address, 4)
|
||||||
|
request += struct.pack("<f", value)
|
||||||
|
|
||||||
|
total_size = len(request) + 4
|
||||||
|
request = Pine.to_bytes(total_size, 4) + request
|
||||||
|
|
||||||
|
self._send_request(request)
|
||||||
|
|
||||||
def get_game_id(self) -> str:
|
def get_game_id(self) -> str:
|
||||||
request = Pine.to_bytes(5, 4) + Pine.to_bytes(Pine.IPCCommand.ID, 1)
|
request = Pine.to_bytes(5, 4) + Pine.to_bytes(Pine.IPCCommand.ID, 1)
|
||||||
|
try:
|
||||||
response = self._send_request(request)
|
response = self._send_request(request)
|
||||||
|
except ConnectionError:
|
||||||
|
return ""
|
||||||
return response[9:-1].decode("ascii")
|
return response[9:-1].decode("ascii")
|
||||||
|
|
||||||
def _send_request(self, request: bytes) -> bytes:
|
def _send_request(self, request: bytes) -> bytes:
|
||||||
@@ -247,4 +362,3 @@ class Pine:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def from_bytes(arr: bytes) -> int:
|
def from_bytes(arr: bytes) -> int:
|
||||||
return int.from_bytes(arr, byteorder="little")
|
return int.from_bytes(arr, byteorder="little")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user