🐐
This commit is contained in:
270
Sly3Callbacks.py
270
Sly3Callbacks.py
@@ -1,10 +1,13 @@
|
||||
from typing import TYPE_CHECKING, Dict, List
|
||||
from time import time
|
||||
from random import randint
|
||||
import asyncio
|
||||
|
||||
from NetUtils import ClientStatus
|
||||
from BaseClasses import ItemClassification
|
||||
|
||||
from .data.Constants import REQUIREMENTS, DEATH_TYPES, EPISODES, CHALLENGES
|
||||
from .Sly3Interface import Sly3Episode, PowerUps
|
||||
from .data.Constants import REQUIREMENTS, DEATH_TYPES, EPISODES, CHALLENGES, JOB_IDS
|
||||
from .data import Items, Locations
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -14,7 +17,7 @@ if TYPE_CHECKING:
|
||||
# Helpers #
|
||||
###########
|
||||
|
||||
def accessibility(ctx: "Sly3Context") -> Dict[str, Dict[str, List[List[bool]]]]:
|
||||
def accessibility(ctx: "Sly3Context") -> Dict[int, bool]:
|
||||
section_requirements = {
|
||||
episode_name: [
|
||||
list(set(sum([
|
||||
@@ -112,30 +115,73 @@ def accessibility(ctx: "Sly3Context") -> Dict[str, Dict[str, List[List[bool]]]]:
|
||||
}
|
||||
|
||||
return {
|
||||
"Jobs": job_accessibility,
|
||||
"Challenges": challenge_accessibility
|
||||
}
|
||||
JOB_IDS[ep][i][j]: avail
|
||||
for ep, ep_avail in job_accessibility.items()
|
||||
for i, section_avail in enumerate(ep_avail)
|
||||
for j, avail in enumerate(section_avail)
|
||||
} # TODO: Challenges
|
||||
|
||||
def set_thiefnet(ctx: "Sly3Context"):
|
||||
# TODO
|
||||
pass
|
||||
async def set_thiefnet(ctx: "Sly3Context"):
|
||||
if ctx.slot_data is None:
|
||||
return
|
||||
|
||||
def unset_thiefnet(ctx: "Sly3Context"):
|
||||
# TODO
|
||||
pass
|
||||
thiefnet_n = ctx.slot_data["thiefnet_locations"]
|
||||
|
||||
if ctx.thiefnet_items is None:
|
||||
info = ctx.locations_info
|
||||
ctx.thiefnet_items = []
|
||||
for i in range(thiefnet_n):
|
||||
location_info = info[Locations.location_dict[f"ThiefNet {i+1:02}"].code]
|
||||
|
||||
player_name = ctx.player_names[location_info.player]
|
||||
item_name = ctx.item_names.lookup_in_slot(location_info.item,location_info.player)
|
||||
string = f"{player_name}'s {item_name}"
|
||||
|
||||
ctx.thiefnet_items.append(string)
|
||||
|
||||
ctx.thiefnet_purchases = PowerUps(*[
|
||||
Locations.location_dict[f"ThiefNet {i+1:02}"].code in ctx.checked_locations
|
||||
for i in range(thiefnet_n)
|
||||
])
|
||||
|
||||
if ctx.slot_data["scout_thiefnet"]:
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "LocationScouts",
|
||||
"locations": [
|
||||
Locations.location_dict[f"ThiefNet {i+1:02}"].code
|
||||
for i in range(thiefnet_n)
|
||||
],
|
||||
"create_as_hint": 2
|
||||
}])
|
||||
|
||||
ctx.game_interface.set_powerups(ctx.thiefnet_purchases)
|
||||
thiefnet_data = [
|
||||
(ctx.slot_data["thiefnet_costs"][i], ctx.thiefnet_items[i])
|
||||
for i in range(thiefnet_n)
|
||||
]
|
||||
ctx.game_interface.set_thiefnet(thiefnet_data)
|
||||
|
||||
async def reset_thiefnet(ctx: "Sly3Context"):
|
||||
if ctx.in_hub:
|
||||
ctx.thiefnet_purchases = ctx.game_interface.get_powerups()
|
||||
set_powerups(ctx)
|
||||
ctx.game_interface.reset_thiefnet()
|
||||
|
||||
def check_jobs(ctx: "Sly3Context"):
|
||||
# TODO
|
||||
pass
|
||||
ctx.jobs_completed = ctx.game_interface.jobs_completed()
|
||||
|
||||
def check_challenges(ctx: "Sly3Context"):
|
||||
# TODO
|
||||
# TODO: Challenges
|
||||
pass
|
||||
|
||||
def set_powerups(ctx: "Sly3Context"):
|
||||
if not ctx.in_safehouse():
|
||||
if not ctx.in_safehouse:
|
||||
ctx.game_interface.set_powerups(ctx.powerups)
|
||||
|
||||
async def unlock_episodes(ctx):
|
||||
await asyncio.sleep(1)
|
||||
ctx.game_interface.unlock_episodes()
|
||||
|
||||
#########
|
||||
# Steps #
|
||||
#########
|
||||
@@ -144,29 +190,61 @@ async def update_in_safehouse(ctx: "Sly3Context"):
|
||||
in_safehouse = ctx.game_interface.in_safehouse()
|
||||
if in_safehouse and not ctx.in_safehouse:
|
||||
ctx.in_safehouse = True
|
||||
set_thiefnet(ctx)
|
||||
await set_thiefnet(ctx)
|
||||
elif ctx.in_safehouse and not in_safehouse:
|
||||
ctx.in_safehouse = False
|
||||
unset_thiefnet(ctx)
|
||||
await reset_thiefnet(ctx)
|
||||
|
||||
async def replace_text(ctx: "Sly3Context"):
|
||||
# TODO
|
||||
# I'm not totally sure yet which text I'm replacing
|
||||
pass
|
||||
if ctx.current_episode != Sly3Episode.Title_Screen:
|
||||
return
|
||||
|
||||
if ctx.current_map == 35:
|
||||
ctx.game_interface.set_text(
|
||||
"Press START (start)",
|
||||
"Connected to Archipelago"
|
||||
)
|
||||
ctx.game_interface.set_text(
|
||||
"Press START (resume)",
|
||||
"Connected to Archipelago"
|
||||
)
|
||||
|
||||
elif ctx.current_map == 0:
|
||||
for i in range(1,7):
|
||||
ep_name = Sly3Episode(i).name.replace("_"," ")
|
||||
|
||||
if ctx.available_episodes[Sly3Episode(i)]:
|
||||
rep_text = ep_name
|
||||
elif i == 6:
|
||||
obtained_crew = len([
|
||||
i for i in ctx.items_received
|
||||
if Items.from_id(i.item).category == "Crew"
|
||||
])
|
||||
rep_text = f"{obtained_crew}/7 crew members"
|
||||
else:
|
||||
rep_text = "Locked"
|
||||
|
||||
ctx.game_interface.set_text(
|
||||
ep_name,
|
||||
rep_text
|
||||
)
|
||||
|
||||
|
||||
async def kick_from_episode(ctx: "Sly3Context", availability: Dict):
|
||||
# TODO
|
||||
not_connected = ctx.current_map == 0 and not ctx.is_connected_to_server
|
||||
current_episode = ctx.game_interface.get_current_episode()
|
||||
ep_not_unlocked = current_episode == 0 or ctx.available_episodes[current_episode-1]
|
||||
job_not_available = False # if current job/challenge not available
|
||||
not_connected = ctx.current_episode != 0 and not ctx.is_connected_to_server
|
||||
|
||||
ep_not_unlocked = not ctx.available_episodes[Sly3Episode(ctx.current_episode)]
|
||||
|
||||
job_not_available = not availability.get(ctx.current_job,True)
|
||||
|
||||
if not_connected or ep_not_unlocked or job_not_available:
|
||||
print("Kicking")
|
||||
print(not_connected, ep_not_unlocked, job_not_available)
|
||||
ctx.game_interface.to_episode_menu()
|
||||
|
||||
async def check_jobs_and_challenges(ctx: "Sly3Context"):
|
||||
check_jobs()
|
||||
check_challenges()
|
||||
async def check_locations(ctx: "Sly3Context"):
|
||||
check_jobs(ctx)
|
||||
check_challenges(ctx)
|
||||
|
||||
async def send_checks(ctx: "Sly3Context"):
|
||||
if ctx.slot_data is None:
|
||||
@@ -174,7 +252,7 @@ async def send_checks(ctx: "Sly3Context"):
|
||||
|
||||
# ThiefNet purchases
|
||||
if ctx.in_safehouse:
|
||||
ctx.thiefnet_purchases = ctx.game_interface.read_powerups()
|
||||
ctx.thiefnet_purchases = ctx.game_interface.get_powerups()
|
||||
purchases = list(ctx.thiefnet_purchases)
|
||||
purchases = (
|
||||
purchases[4:32] +
|
||||
@@ -192,32 +270,106 @@ async def send_checks(ctx: "Sly3Context"):
|
||||
ctx.locations_checked.add(location_code)
|
||||
|
||||
# Jobs
|
||||
for i, episode in enumerate(ctx.jobs_completed):
|
||||
episode_name = list(EPISODES.keys())[i]
|
||||
for j, chapter in enumerate(episode):
|
||||
for k, job in enumerate(chapter):
|
||||
if job:
|
||||
job_name = EPISODES[episode_name][j][k]
|
||||
i = 0
|
||||
for episode_name, episode in EPISODES.items():
|
||||
for chapter in episode:
|
||||
for job_name in chapter:
|
||||
if ctx.jobs_completed[i]:
|
||||
location_name = f"{episode_name} - {job_name}"
|
||||
location_code = Locations.location_dict[location_name].code
|
||||
ctx.locations_checked.add(location_code)
|
||||
i += 1
|
||||
|
||||
# Challenges
|
||||
for i, episode in enumerate(ctx.challenges_completed):
|
||||
episode_name = list(CHALLENGES.keys())[i]
|
||||
for j, chapter in enumerate(episode):
|
||||
for k, job in enumerate(chapter):
|
||||
if job:
|
||||
job_name = CHALLENGES[episode_name][j][k]
|
||||
location_name = f"{episode_name} - {job_name}"
|
||||
i = 0
|
||||
for episode_name, episode in CHALLENGES.items():
|
||||
for chapter in episode:
|
||||
for challenge_name in chapter:
|
||||
if ctx.challenges_completed[i]:
|
||||
location_name = f"{episode_name} - {challenge_name}"
|
||||
location_code = Locations.location_dict[location_name].code
|
||||
ctx.locations_checked.add(location_code)
|
||||
i += 1
|
||||
|
||||
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": ctx.locations_checked}])
|
||||
|
||||
async def receive_items(ctx: "Sly3Context"):
|
||||
# TODO
|
||||
pass
|
||||
if ctx.slot_data is None:
|
||||
return
|
||||
|
||||
items_n = ctx.game_interface.get_items_received()
|
||||
|
||||
network_items = ctx.items_received
|
||||
available_episodes = {e: False for e in Sly3Episode}
|
||||
available_episodes[Sly3Episode.Title_Screen] = True
|
||||
available_episodes[Sly3Episode.Honor_Among_Thieves] = len([
|
||||
i for i in network_items
|
||||
if Items.from_id(i.item).category == "Crew"
|
||||
]) == 7
|
||||
|
||||
new_powerups = list(PowerUps(True))
|
||||
powerup_fields = PowerUps._fields
|
||||
|
||||
for i, network_item in enumerate(network_items):
|
||||
item = Items.from_id(network_item.item)
|
||||
player = ctx.player_names[network_item.player]
|
||||
|
||||
if i >= items_n:
|
||||
ctx.inventory[network_item.item] += 1
|
||||
ctx.notification(f"Received {item.name} from {player}")
|
||||
|
||||
if item.category == "Episode":
|
||||
episode = Sly3Episode[
|
||||
item.name.replace(" ","_")
|
||||
]
|
||||
|
||||
available_episodes[episode] = True
|
||||
elif item.category == "Power-Up":
|
||||
item_name = item.name.lower().replace(" ","_")
|
||||
if item_name == "Progressive Shadow Power":
|
||||
if new_powerups[30]:
|
||||
idx = 32
|
||||
else:
|
||||
idx = 30
|
||||
elif item_name == "Progressive Spin Attack":
|
||||
if new_powerups[40]:
|
||||
idx = 41
|
||||
elif new_powerups[39]:
|
||||
idx = 40
|
||||
else:
|
||||
idx = 39
|
||||
elif item_name == "Progressive Jump Power":
|
||||
if new_powerups[43]:
|
||||
idx = 44
|
||||
elif new_powerups[42]:
|
||||
idx = 43
|
||||
else:
|
||||
idx = 42
|
||||
elif item_name == "Progressive Push Attack":
|
||||
if new_powerups[46]:
|
||||
idx = 47
|
||||
elif new_powerups[45]:
|
||||
idx = 46
|
||||
else:
|
||||
idx = 45
|
||||
else:
|
||||
idx = powerup_fields.index(item_name)
|
||||
|
||||
new_powerups[idx] = True
|
||||
elif item.name == "Coins" and i >= items_n:
|
||||
amount = randint(
|
||||
ctx.slot_data["coins_minimum"],
|
||||
ctx.slot_data["coins_maximum"]
|
||||
)
|
||||
ctx.game_interface.add_coins(amount)
|
||||
|
||||
if ctx.current_episode != 0 and not ctx.in_safehouse:
|
||||
set_powerups(ctx)
|
||||
|
||||
ctx.game_interface.set_items_received(len(network_items))
|
||||
ctx.available_episodes = available_episodes
|
||||
ctx.powerups = PowerUps(*new_powerups)
|
||||
|
||||
|
||||
async def check_goal(ctx: "Sly3Context"):
|
||||
if ctx.slot_data is None:
|
||||
@@ -230,8 +382,19 @@ async def check_goal(ctx: "Sly3Context"):
|
||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||
|
||||
async def handle_job_markers(ctx: "Sly3Context", availability: Dict):
|
||||
# TODO
|
||||
pass
|
||||
episode = ctx.current_episode.name.replace("_"," ")
|
||||
job_ids = JOB_IDS[episode]
|
||||
active_jobs = []
|
||||
inactive_jobs = []
|
||||
for section in job_ids:
|
||||
for job_id in section:
|
||||
if availability[job_id]:
|
||||
active_jobs.append(job_id)
|
||||
else:
|
||||
inactive_jobs.append(job_id)
|
||||
|
||||
ctx.game_interface.activate_jobs(active_jobs)
|
||||
ctx.game_interface.activate_jobs(inactive_jobs)
|
||||
|
||||
async def handle_notifications(ctx: "Sly3Context"):
|
||||
if (
|
||||
@@ -283,8 +446,9 @@ async def init(ctx: "Sly3Context") -> None:
|
||||
return
|
||||
|
||||
if ctx.current_map == 0:
|
||||
ctx.game_interface.unlock_episodes()
|
||||
asyncio.create_task(unlock_episodes(ctx))
|
||||
|
||||
await receive_items(ctx)
|
||||
await replace_text(ctx)
|
||||
# Maybe fix jobs if they break?
|
||||
|
||||
@@ -293,19 +457,23 @@ async def update(ctx: "Sly3Context") -> None:
|
||||
if ctx.current_map is None:
|
||||
return
|
||||
|
||||
if not ctx.game_interface.is_game_started():
|
||||
return
|
||||
|
||||
availability = accessibility(ctx)
|
||||
kick_from_episode(ctx, availability)
|
||||
await kick_from_episode(ctx, availability)
|
||||
|
||||
if not ctx.is_connected_to_server:
|
||||
return
|
||||
|
||||
await update_in_safehouse(ctx)
|
||||
await check_jobs_and_challenges(ctx)
|
||||
await check_locations(ctx)
|
||||
await send_checks(ctx)
|
||||
await receive_items(ctx)
|
||||
await check_goal(ctx)
|
||||
await handle_job_markers(ctx, availability["Jobs"])
|
||||
if ctx.game_interface.in_hub():
|
||||
await handle_job_markers(ctx, availability)
|
||||
|
||||
if ctx.in_safehouse:
|
||||
if ctx.current_map != 0 and not ctx.in_safehouse:
|
||||
await handle_notifications(ctx)
|
||||
await handle_deathlink(ctx)
|
||||
|
||||
@@ -72,6 +72,7 @@ class Sly3Context(CommonContext): # type: ignore[misc]
|
||||
in_hub: bool = False
|
||||
current_map: Optional[int] = None
|
||||
current_job: Optional[int] = None
|
||||
current_episode: Sly3Episode = Sly3Episode.Title_Screen
|
||||
|
||||
# Items and checks
|
||||
inventory: Dict[int,int] = {l.code: 0 for l in Items.item_dict.values()}
|
||||
@@ -79,23 +80,32 @@ class Sly3Context(CommonContext): # type: ignore[misc]
|
||||
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()
|
||||
jobs_completed: list[bool] = [
|
||||
False for episode in EPISODES.values()
|
||||
for chapter in episode
|
||||
for _ in chapter
|
||||
]
|
||||
challenges_completed: list[list[list[bool]]] = [
|
||||
[[False for _ in chapter] for chapter in episode]
|
||||
for episode in CHALLENGES.values()
|
||||
challenges_completed: list[bool] = [
|
||||
False for episode in CHALLENGES.values()
|
||||
for chapter in episode
|
||||
for _ in chapter
|
||||
]
|
||||
|
||||
def __init__(self, server_address, password):
|
||||
super().__init__(server_address, password)
|
||||
self.version = [0,0,0]
|
||||
self.game_interface = Sly3Interface(logger)
|
||||
self.available_episodes[Sly3Episode.Title_Screen] = True
|
||||
|
||||
def on_deathlink(self, data: Utils.Dict[str, Utils.Any]) -> None:
|
||||
# TODO
|
||||
pass
|
||||
super().on_deathlink(data)
|
||||
if self.death_link_enabled:
|
||||
self.queued_deaths += 1
|
||||
cause = data.get("cause", "")
|
||||
if cause:
|
||||
self.notification(f"DeathLink: {cause}")
|
||||
else:
|
||||
self.notification(f"DeathLink: Received from {data['source']}")
|
||||
|
||||
def make_gui(self):
|
||||
ui = super().make_gui()
|
||||
@@ -136,13 +146,13 @@ class Sly3Context(CommonContext): # type: ignore[misc]
|
||||
Utils.async_start(self.send_msgs([{
|
||||
"cmd": "LocationScouts",
|
||||
"locations": [
|
||||
Locations.location_dict[location].code
|
||||
for location in Locations.location_groups["Purchase"]
|
||||
Locations.location_dict[f"ThiefNet {i+1:02}"].code
|
||||
for i in range(args["slot_data"]["thiefnet_locations"])
|
||||
]
|
||||
}]))
|
||||
|
||||
def notification(self):
|
||||
# TODO
|
||||
def notification(self, text: str):
|
||||
# TODO: Notifications
|
||||
pass
|
||||
|
||||
def update_connection_status(ctx: Sly3Context, status: bool):
|
||||
@@ -166,6 +176,7 @@ async def _handle_game_ready(ctx: Sly3Context) -> None:
|
||||
current_map = ctx.game_interface.get_current_map()
|
||||
ctx.in_hub = ctx.game_interface.in_hub()
|
||||
ctx.current_job = ctx.game_interface.get_current_job()
|
||||
ctx.current_episode = ctx.game_interface.get_current_episode()
|
||||
|
||||
ctx.game_interface.skip_cutscene()
|
||||
|
||||
@@ -208,8 +219,7 @@ async def pcsx2_sync_task(ctx: Sly3Context):
|
||||
try:
|
||||
is_connected = ctx.game_interface.get_connection_state()
|
||||
update_connection_status(ctx, is_connected)
|
||||
game_started = ctx.game_interface.is_game_started()
|
||||
if is_connected and game_started:
|
||||
if is_connected:
|
||||
await _handle_game_ready(ctx)
|
||||
else:
|
||||
await _handle_game_not_ready(ctx)
|
||||
|
||||
229
Sly3Interface.py
229
Sly3Interface.py
@@ -5,7 +5,7 @@ from enum import IntEnum
|
||||
import traceback
|
||||
|
||||
from .pcsx2_interface.pine import Pine
|
||||
from .data.Constants import ADDRESSES, MENU_RETURN_DATA
|
||||
from .data.Constants import ADDRESSES, MENU_RETURN_DATA, POWER_UP_TEXT
|
||||
|
||||
class Sly3Episode(IntEnum):
|
||||
Title_Screen = 0
|
||||
@@ -185,6 +185,18 @@ class Sly3Interface(GameInterface):
|
||||
|
||||
return pointer
|
||||
|
||||
def _find_string_address(self, _id: int) -> int:
|
||||
# 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 = self._read32(self.addresses["string table"])
|
||||
i = 0
|
||||
while True:
|
||||
string_id = self._read32(string_table_address+i*8)
|
||||
if string_id == _id:
|
||||
return self._read32(string_table_address+i*8+4)
|
||||
i += 1
|
||||
|
||||
###################
|
||||
## Current State ##
|
||||
###################
|
||||
@@ -196,35 +208,95 @@ class Sly3Interface(GameInterface):
|
||||
return self._read32(self.addresses["loading"]) == 2
|
||||
|
||||
def in_safehouse(self) -> bool:
|
||||
# TODO
|
||||
return False
|
||||
return (
|
||||
not self.is_loading() and
|
||||
self.in_hub() and
|
||||
self._read32(self.addresses["infobox string"]) in [
|
||||
5345,5346,5347,5348,5349,5350,5351
|
||||
]
|
||||
)
|
||||
|
||||
def in_hub(self) -> bool:
|
||||
# TODO
|
||||
return False
|
||||
return self.get_current_map() in [3,8,15,23,31,35]
|
||||
|
||||
def is_goaled(self) -> bool:
|
||||
# TODO
|
||||
def is_goaled(self, goal: int) -> bool:
|
||||
# TODO: Goal
|
||||
return False
|
||||
|
||||
def is_game_started(self) -> bool:
|
||||
# TODO
|
||||
return False
|
||||
world_id = self.get_current_episode()
|
||||
map_id = self.get_current_map()
|
||||
return not (
|
||||
world_id == 0 and
|
||||
map_id in [35,0xffffffff]
|
||||
)
|
||||
|
||||
def showing_infobox(self) -> bool:
|
||||
# TODO
|
||||
# TODO: Notifications
|
||||
return False
|
||||
|
||||
def alive(self) -> bool:
|
||||
# TODO
|
||||
return False
|
||||
active_character = self._read32(self.addresses["active character pointer"])
|
||||
if active_character == 0:
|
||||
return True
|
||||
|
||||
health_gui = self._read32(active_character+0x16c)
|
||||
health = self._read32(active_character+0x16c)
|
||||
return health_gui != 2 or health != 0
|
||||
|
||||
#######################
|
||||
## Getters & Setters ##
|
||||
#######################
|
||||
def set_thiefnet(self, data: list[tuple[int,str]]) -> None:
|
||||
addresses = [
|
||||
self.addresses["thiefnet start"]+i*0x3c
|
||||
for i in range(44)
|
||||
if i not in [28,36,37,39,40,42,43]
|
||||
][:len(data)]
|
||||
|
||||
for i, address in enumerate(addresses):
|
||||
self._write32(address,data[i][0])
|
||||
self._write32(address+0xC,0)
|
||||
name_id = self._read32(address+0x14)
|
||||
name_address = self._find_string_address(name_id)
|
||||
self.set_text(name_address, f"Check #{i+1}")
|
||||
|
||||
description_id = self._read32(address+0x18)
|
||||
description_address = self._find_string_address(description_id)
|
||||
self.set_text(description_address, data[i][1])
|
||||
|
||||
for i in [28,36,37,39,40,42,43]+list(range(len(data),44)):
|
||||
address = self.addresses["thiefnet start"]+i*0x3c
|
||||
self._write32(address+0xC,10)
|
||||
|
||||
def reset_thiefnet(self) -> None:
|
||||
for i in range(44):
|
||||
address = self.addresses["thiefnet start"]+i*0x3c
|
||||
name_id = self._read32(address+0x14)
|
||||
name_address = self._find_string_address(name_id)
|
||||
self.set_text(name_address, POWER_UP_TEXT[i][0])
|
||||
|
||||
description_id = self._read32(address+0x18)
|
||||
description_address = self._find_string_address(description_id)
|
||||
self.set_text(description_address, POWER_UP_TEXT[i][1])
|
||||
|
||||
def set_text(self, text: int|str, replacement: str) -> None:
|
||||
if isinstance(text,str):
|
||||
text_pointer = self.addresses["text"].get(text, None)
|
||||
if isinstance(text_pointer, dict):
|
||||
text_pointer = text_pointer.get(self.get_current_map(), None)
|
||||
|
||||
if not isinstance(text_pointer,int):
|
||||
return
|
||||
else:
|
||||
text_pointer = text
|
||||
|
||||
replacement_string = replacement.encode("utf-16-le")+b"\x00\x00"
|
||||
self._write_bytes(text_pointer,replacement_string)
|
||||
|
||||
def get_current_episode(self) -> Sly3Episode:
|
||||
episode_num = self._read32(self.addresses["world id"])
|
||||
return Sly3Episode(episode_num)
|
||||
episode_num = self._read32(self.addresses["world id"]) - 2
|
||||
return Sly3Episode(max(0,episode_num))
|
||||
|
||||
def get_current_map(self) -> int:
|
||||
return self._read32(self.addresses["map id"])
|
||||
@@ -235,6 +307,9 @@ class Sly3Interface(GameInterface):
|
||||
def set_current_job(self, job: int) -> None:
|
||||
self._write32(self.addresses["job id"], job)
|
||||
|
||||
def get_items_received(self) -> int:
|
||||
return self._read32(self.addresses["items received"])
|
||||
|
||||
def set_items_received(self, n:int) -> None:
|
||||
self._write32(self.addresses["items received"], n)
|
||||
|
||||
@@ -269,24 +344,26 @@ class Sly3Interface(GameInterface):
|
||||
return PowerUps(*relevant_bits)
|
||||
|
||||
def activate_jobs(self, job_ids: int|list[int]):
|
||||
# TODO
|
||||
# TODO: Job Markers
|
||||
pass
|
||||
|
||||
def deactivate_jobs(self, job_ids: int|list[int]):
|
||||
# TODO
|
||||
# TODO: Job Markers
|
||||
pass
|
||||
|
||||
def jobs_completed(self, job_ids: int|list[int]):
|
||||
# TODO
|
||||
pass
|
||||
def jobs_completed(self) -> list[bool]:
|
||||
addresses = [a for ep in self.addresses["job completed"].values() for c in ep for a in c]
|
||||
states = self._batch_read32(addresses)
|
||||
|
||||
return [s != 0 for s in states]
|
||||
|
||||
def current_infobox(self) -> int:
|
||||
# TODO
|
||||
pass
|
||||
# TODO: Notifications
|
||||
return 0
|
||||
|
||||
def get_damage_type(self) -> int:
|
||||
# TODO
|
||||
pass
|
||||
# TODO: Death Messages
|
||||
return 0
|
||||
|
||||
#################
|
||||
## Other Utils ##
|
||||
@@ -298,7 +375,7 @@ class Sly3Interface(GameInterface):
|
||||
self.get_current_job() == 1797
|
||||
):
|
||||
self.set_current_job(0xffffffff)
|
||||
# self.set_items_received(0)
|
||||
self.set_items_received(0)
|
||||
|
||||
self._reload(bytes.fromhex(MENU_RETURN_DATA))
|
||||
|
||||
@@ -317,16 +394,18 @@ class Sly3Interface(GameInterface):
|
||||
self._write32(self.addresses["coins"],new_amount)
|
||||
|
||||
def disable_infobox(self):
|
||||
# TODO
|
||||
# TODO: Notifications
|
||||
pass
|
||||
|
||||
def set_infobox(self):
|
||||
# TODO
|
||||
def set_infobox(self, text: str):
|
||||
# TODO: Notifications
|
||||
pass
|
||||
|
||||
def kill_player(self):
|
||||
# TODO
|
||||
pass
|
||||
if self.in_safehouse() or self.get_current_episode() == Sly3Episode.Title_Screen:
|
||||
return
|
||||
|
||||
self._write32(self.addresses["reload"],1)
|
||||
|
||||
#### TESTING ZONE ####
|
||||
|
||||
@@ -362,6 +441,55 @@ def find_string_id(interf: Sly3Interface, _id: int):
|
||||
return interf._read32(string_table_address+i*8+4)
|
||||
i += 1
|
||||
|
||||
def find_string_address(interf: Sly3Interface, address: 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_address = interf._read32(string_table_address+i*8+4)
|
||||
if string_address == address:
|
||||
return interf._read32(string_table_address+i*8)
|
||||
i += 1
|
||||
|
||||
def print_string_table(interf: Sly3Interface, n: int):
|
||||
"""Prints n entries in the string table"""
|
||||
|
||||
# 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)
|
||||
for i in range(n):
|
||||
address = interf._read32(string_table_address+i*8+4)
|
||||
print(read_text(interf, address), i)
|
||||
|
||||
def find_text(interf: Sly3Interface, text: str):
|
||||
"""Prints n entries in the string table"""
|
||||
|
||||
# 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)
|
||||
results = []
|
||||
for i in range(2000):
|
||||
address = interf._read32(string_table_address+i*8+4)
|
||||
try:
|
||||
if text in read_text(interf, address):
|
||||
results.append(address)
|
||||
except:
|
||||
return results
|
||||
|
||||
return results
|
||||
|
||||
def print_thiefnet_addresses(interf: Sly3Interface):
|
||||
print(" {")
|
||||
for i in range(44):
|
||||
@@ -384,6 +512,28 @@ def print_thiefnet_addresses(interf: Sly3Interface):
|
||||
|
||||
print(" }")
|
||||
|
||||
def print_thiefnet_text(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)
|
||||
description_text = read_text(interf, description_address)
|
||||
|
||||
print(
|
||||
" " +
|
||||
f"(\"{name_text}\",\"{description_text}\"),"
|
||||
)
|
||||
|
||||
print("]")
|
||||
|
||||
def current_job_info(interf: Sly3Interface):
|
||||
current_job = interf._read32(0x36DB98)
|
||||
|
||||
@@ -411,17 +561,30 @@ if __name__ == "__main__":
|
||||
# interf.skip_cutscene()
|
||||
|
||||
# Loading all power-ups (except the one I don't know)
|
||||
power_ups = PowerUps(True, True, True, False, *[True]*44)
|
||||
interf.set_powerups(power_ups)
|
||||
# power_ups = PowerUps(True, True, True, False, *[True]*44)
|
||||
# interf.set_powerups(power_ups)
|
||||
|
||||
# Adding 10000 coins
|
||||
#interf.add_coins(10000)
|
||||
|
||||
# === Testing Zone ===
|
||||
|
||||
# print_thiefnet_addresses(interf)
|
||||
# print_thiefnet_addresses(interf)
|
||||
|
||||
# disabling first job of episode 1 (0 = disabled, 1 = available, 2 = in progress, 3 = complete)
|
||||
# interf._write32(0x1335d10+0x44, 0)
|
||||
|
||||
current_job_info(interf)
|
||||
# current_job_info(interf)
|
||||
# find_string_id
|
||||
|
||||
# print_string_table(interf, 500)
|
||||
|
||||
# addresses = find_text(interf, "Press &2X&. to ")
|
||||
# print("======")
|
||||
# for address in addresses:
|
||||
# print(hex(address))
|
||||
# print(read_text(interf, address))
|
||||
# print(find_string_address(interf, address))
|
||||
# print("======")
|
||||
|
||||
print_thiefnet_text(interf)
|
||||
@@ -14,8 +14,8 @@ class StartingEpisode(Choice):
|
||||
"""
|
||||
Select Which episode to start with.
|
||||
|
||||
A Cold Alliance and Dead Men Tell No Tales require starting items, so
|
||||
starting with them will break a solo game.
|
||||
Flight of Fancy, A Cold Alliance and Dead Men Tell No Tales require starting
|
||||
items, so starting with them will break a solo game.
|
||||
"""
|
||||
|
||||
display_name = "Starting Episode"
|
||||
@@ -118,6 +118,14 @@ class ThiefNetCostMaximum(Range):
|
||||
range_end = 9999
|
||||
default = 2000
|
||||
|
||||
|
||||
class ScoutThiefnet(DefaultOnToggle):
|
||||
"""
|
||||
Whether to scout/hint ThiefNet checks. They will still be displayed in game.
|
||||
"""
|
||||
|
||||
display_name = "Scout Thiefnet"
|
||||
|
||||
@dataclass
|
||||
class Sly3Options(PerGameCommonOptions):
|
||||
start_inventory_from_pool: StartInventoryPool
|
||||
@@ -131,6 +139,7 @@ class Sly3Options(PerGameCommonOptions):
|
||||
thiefnet_minimum: ThiefNetCostMinimum
|
||||
thiefnet_maximum: ThiefNetCostMaximum
|
||||
bonus_crew_member: BonusCrewMember
|
||||
scout_thiefnet: ScoutThiefnet
|
||||
|
||||
sly3_option_groups = [
|
||||
OptionGroup("Goal",[
|
||||
@@ -145,6 +154,7 @@ sly3_option_groups = [
|
||||
OptionGroup("Locations",[
|
||||
ThiefNetLocations,
|
||||
ThiefNetCostMinimum,
|
||||
ThiefNetCostMaximum
|
||||
ThiefNetCostMaximum,
|
||||
ScoutThiefnet
|
||||
])
|
||||
]
|
||||
10
Sly3Rules.py
10
Sly3Rules.py
@@ -17,7 +17,7 @@ def set_rules_sly3(world: "Sly3World"):
|
||||
# Putting ThiefNet stuff out of logic, to make early game less slow.
|
||||
# Divides the items into groups that require a number of episode and crew
|
||||
# items to be in logic
|
||||
for i in range(1,thiefnet_items):
|
||||
for i in range(1,thiefnet_items+1):
|
||||
divisor = ceil(thiefnet_items/12)
|
||||
episode_items_n = ceil(i/divisor)
|
||||
add_rule(
|
||||
@@ -30,14 +30,6 @@ def set_rules_sly3(world: "Sly3World"):
|
||||
)
|
||||
)
|
||||
|
||||
def require(location: str, item: str|list[str]):
|
||||
add_rule(
|
||||
world.get_location(location),
|
||||
lambda state, i=item: (
|
||||
all(state.has(j, player) for j in i)
|
||||
)
|
||||
)
|
||||
|
||||
### Job requirements
|
||||
for episode, sections in EPISODES.items():
|
||||
if episode == "Honor Among Thieves":
|
||||
|
||||
@@ -110,6 +110,7 @@ class Sly3World(World):
|
||||
self.options.thiefnet_minimum.value = slot_data["thiefnet_minimum"]
|
||||
self.options.thiefnet_maximum.value = slot_data["thiefnet_maximum"]
|
||||
self.options.bonus_crew_member.value = slot_data["bonus_crew_member"]
|
||||
self.options.scout_thiefnet.value = slot_data["scout_thiefnet"]
|
||||
return
|
||||
|
||||
self.validate_options(self.options)
|
||||
@@ -161,6 +162,7 @@ class Sly3World(World):
|
||||
"thiefnet_minimum",
|
||||
"thiefnet_maximum",
|
||||
"bonus_crew_member",
|
||||
"scout_thiefnet",
|
||||
)
|
||||
|
||||
def fill_slot_data(self) -> Mapping[str, Any]:
|
||||
|
||||
@@ -240,7 +240,9 @@ REQUIREMENTS = {
|
||||
[[]]
|
||||
],
|
||||
"Flight of Fancy": [
|
||||
[[]],
|
||||
[
|
||||
["Bentley"]
|
||||
],
|
||||
[
|
||||
["Murray", "Bentley", "Guru", "Fishing Pole"],
|
||||
["Murray"],
|
||||
@@ -375,8 +377,45 @@ REQUIREMENTS = {
|
||||
}
|
||||
}
|
||||
|
||||
JOB_IDS = {
|
||||
"An Opera of Fear": [
|
||||
[2085],
|
||||
[2230,2283,2329],
|
||||
[2139,2168,2187,2352],
|
||||
[2419]
|
||||
],
|
||||
"Rumble Down Under": [
|
||||
[2577],
|
||||
[2596,2805,2695,2663],
|
||||
[2623,2730,2780],
|
||||
[2843]
|
||||
],
|
||||
"Flight of Fancy": [
|
||||
[2983],
|
||||
[3025,3061,3101,3140],
|
||||
[3202,3164,3225],
|
||||
[3259]
|
||||
],
|
||||
"A Cold Alliance": [
|
||||
[3381],
|
||||
[3449,3509,3540,3584],
|
||||
[3629,3672,3684],
|
||||
[3712]
|
||||
],
|
||||
"Dead Men Tell No Tales": [
|
||||
[3848],
|
||||
[3907,4038,3991],
|
||||
[4071,4101,4120],
|
||||
[4145]
|
||||
],
|
||||
"Honor Among Thieves": [
|
||||
[4327,4369,4396,4412,4436,4479,4505,4544]
|
||||
]
|
||||
}
|
||||
|
||||
ADDRESSES = {
|
||||
"SCUS-97464" : {
|
||||
"items received": 0x4695A0,
|
||||
"world id": 0x468D30,
|
||||
"map id": 0x47989C,
|
||||
"job id": 0x36DB98,
|
||||
@@ -390,117 +429,128 @@ ADDRESSES = {
|
||||
"gadgets": 0x468DCC,
|
||||
"coins": 0x468DDC,
|
||||
"DAG root": 0x478C8C,
|
||||
"jobs": [
|
||||
[
|
||||
[0x1335d10]
|
||||
"job completed": {
|
||||
"An Opera of Fear": [
|
||||
[
|
||||
0x468FEC,
|
||||
],
|
||||
[
|
||||
0x468FAC,
|
||||
0x468FBC,
|
||||
0x468F7C,
|
||||
],
|
||||
[
|
||||
0x46900C,
|
||||
0x468F6C,
|
||||
0x468FFC,
|
||||
0x468F9C,
|
||||
],
|
||||
[
|
||||
0x468FCC,
|
||||
],
|
||||
],
|
||||
[
|
||||
[0x1350560,0x1357f80,0x135aba0]
|
||||
"Rumble Down Under": [
|
||||
[
|
||||
0x46909C,
|
||||
],
|
||||
[
|
||||
0x46902C,
|
||||
0x46903C,
|
||||
0x46905C,
|
||||
0x4690AC,
|
||||
],
|
||||
[
|
||||
0x46904C,
|
||||
0x46901C,
|
||||
0x46906C,
|
||||
],
|
||||
[
|
||||
0x46907C,
|
||||
],
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[]
|
||||
],
|
||||
"Flight of Fancy": [
|
||||
[
|
||||
0x46916C,
|
||||
],
|
||||
[
|
||||
0x4690DC,
|
||||
0x4690EC,
|
||||
0x46910C,
|
||||
0x46917C,
|
||||
],
|
||||
[
|
||||
0x46919C,
|
||||
0x46918C,
|
||||
0x46914C,
|
||||
],
|
||||
[
|
||||
0x46911C,
|
||||
],
|
||||
],
|
||||
"A Cold Alliance": [
|
||||
[
|
||||
0x46920C,
|
||||
],
|
||||
[
|
||||
0x4691BC,
|
||||
0x46922C,
|
||||
0x46923C,
|
||||
0x46921C,
|
||||
],
|
||||
[
|
||||
0x46925C,
|
||||
0x4691FC,
|
||||
0x4691AC,
|
||||
],
|
||||
[
|
||||
0x4691CC,
|
||||
],
|
||||
],
|
||||
"Dead Men Tell No Tales": [
|
||||
[
|
||||
0x4692FC,
|
||||
],
|
||||
[
|
||||
0x4692CC,
|
||||
0x4692BC,
|
||||
0x46931C,
|
||||
],
|
||||
[
|
||||
0x46930C,
|
||||
0x46929C,
|
||||
0x46926C,
|
||||
],
|
||||
[
|
||||
0x4692AC,
|
||||
],
|
||||
],
|
||||
"Honor Among Thieves": [
|
||||
[
|
||||
0x46936C,
|
||||
0x46938C,
|
||||
0x46935C,
|
||||
0x46934C,
|
||||
0x46932C,
|
||||
0x46937C,
|
||||
0x46939C,
|
||||
0x46933C,
|
||||
]
|
||||
],
|
||||
},
|
||||
"active character pointer": 0x36F84C,
|
||||
"infobox string": 0x46F788,
|
||||
"thiefnet start": 0x343208,
|
||||
"string table": 0x47A2D8,
|
||||
"text": {
|
||||
"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),
|
||||
"Fishing Pole": (0x59b000,0x59b2d0),
|
||||
"Alarm Clock": (0x5962b0,0x5964c0),
|
||||
"Adrenaline Burst": (0x593410,0x5934f0),
|
||||
"Health Extractor": (0x5935c0,0x593660),
|
||||
"Hover Pack": (0x593750,0x593840),
|
||||
"Insanity Strike": (0x598480,0x598690),
|
||||
"Grapple-Cam": (0x59acd0,0x59ae50),
|
||||
"Size Destabilizer": (0x592f30,0x593010),
|
||||
"Rage Bomb": (0x599250,0x599420),
|
||||
"Reduction Bomb": (0x5939b0,0x593b00),
|
||||
"Be The Ball": (0x59a9c0,0x59ab70),
|
||||
"Berserker Charge": (0x5955a0,0x595700),
|
||||
"Juggernaut Throw": (0x594c50,0x594dd0),
|
||||
"Guttural Roar": (0x595830,0x595920),
|
||||
"Fists of Flame": (0x5944a0,0x594610),
|
||||
"Temporal Lock": (0x593c40,0x593e60),
|
||||
"Raging Inferno Flop": (0x595a50,0x595bd0),
|
||||
"Diablo Fire Slam": (0x595260,0x595450),
|
||||
"Smoke Bomb": (0x595d60,0x595ee0),
|
||||
"Combat Dodge": (0x596050,0x596190),
|
||||
"Paraglide": (0x5966d0,0x5968d0),
|
||||
"Silent Obliteration": (0x596ba0,0x596df0),
|
||||
"Feral Pounce": (0x597290,0x5973f0),
|
||||
"Mega Jump": (0x5975f0,0x597780),
|
||||
"Knockout Dive": (0x597e10,0x598130),
|
||||
"Shadow Power Level 1": (0x599c30,0x599eb0),
|
||||
"Thief Reflexes": (0x596f70,0x597110),
|
||||
"Shadow Power Level 2": (0x59a140,0x59a310),
|
||||
"Rocket Boots": (0x57aa00,0x57ace0),
|
||||
"Treasure Map": (0x57a3e0,0x57a780),
|
||||
"ENGLISHpowerup_shield_name": (0x59b550,0x579c50),
|
||||
"Venice Disguise": (0x57ae40,0x57b040),
|
||||
"Photographer Disguise": (0x57b220,0x57b3b0),
|
||||
"Pirate Disguise": (0x57b5d0,0x57b7b0),
|
||||
"Spin Attack Level 1": (0x57b9e0,0x57bc40),
|
||||
"Spin Attack Level 2": (0x57be80,0x57c130),
|
||||
"Spin Attack Level 3": (0x57c300,0x57c5b0),
|
||||
"Jump Attack Level 1": (0x57c7c0,0x57c970),
|
||||
"Jump Attack Level 2": (0x57cb00,0x57cc30),
|
||||
"Jump Attack Level 3": (0x57cdf0,0x57d010),
|
||||
"Push Attack Level 1": (0x57d370,0x57d680),
|
||||
"Push Attack Level 2": (0x57d940,0x57dc80),
|
||||
"Push Attack Level 3": (0x57e070,0x57e3b0),
|
||||
},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
"Press START (start)": 0x5639b0,
|
||||
"Press START (resume)": 0x563bd0,
|
||||
"Press SELECT": 0x564380,
|
||||
"An Opera of Fear": 0x53b380,
|
||||
"Rumble Down Under": 0x53b4e0,
|
||||
"Flight of Fancy": 0x53b5a0,
|
||||
"A Cold Alliance": 0x53b710,
|
||||
"Dead Men Tell No Tales": 0x53b7b0,
|
||||
"Honor Among Thieves": 0x53b900,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -524,4 +574,52 @@ MENU_RETURN_DATA = (
|
||||
"7B8274B1"
|
||||
)
|
||||
|
||||
DEATH_TYPES = {} # TODO
|
||||
# TODO: Death Messages
|
||||
DEATH_TYPES = {}
|
||||
|
||||
POWER_UP_TEXT = [
|
||||
("Trigger Bomb","Throwable bomb with remote detonation"),
|
||||
("Fishing Pole","Fish loot out of guards' pockets"),
|
||||
("Alarm Clock","Confuse your enemies with this distracting alarm clock"),
|
||||
("Adrenaline Burst","Move like no turtle has moved before"),
|
||||
("Health Extractor","Capture guards and extract medicine from them"),
|
||||
("Hover Pack","Extend your jumps by hovering in the air"),
|
||||
("Insanity Strike","Make enemies attack each other with the wheelchair spin attack"),
|
||||
("Grapple-Cam","A remote camera with amazing abilities"),
|
||||
("Size Destabilizer","Shrink guards by whacking them with your wheelchair"),
|
||||
("Rage Bomb","Confuse all enemies in the area into attacking each other"),
|
||||
("Reduction Bomb","Shrink enemies in the area"),
|
||||
("Be The Ball","Roll around like a ball"),
|
||||
("Berserker Charge","Scatter enemies with this powerful run"),
|
||||
("Juggernaut Throw","Thrown enemies explode on impact"),
|
||||
("Guttural Roar","Terrify your foes"),
|
||||
("Fists of Flame","Turn ordinary punches into fiery ones"),
|
||||
("Temporal Lock","Freeze time around the guards…temporarily, at least"),
|
||||
("Raging Inferno Flop","Use while jumping to create a wall of flame on impact"),
|
||||
("Diablo Fire Slam","Use while carrying an enemy to create a deadly firestorm"),
|
||||
("Smoke Bomb","Obscure the vision of your enemies for a hasty getaway"),
|
||||
("Combat Dodge","Sidestep enemies in combat"),
|
||||
("Paraglide","Fly through the air with this quick-deploy paraglider"),
|
||||
("Silent Obliteration","Juggle an unaware enemy with &2T&."),
|
||||
("Feral Pounce","Jump over vast distances"),
|
||||
("Mega Jump","Jump to impressive heights"),
|
||||
("Knockout Dive","Leap at enemies, leaving them stunned on the ground"),
|
||||
("Shadow Power Level 1","Move without being seen"),
|
||||
("Thief Reflexes","Slow time to a crawl"),
|
||||
("Shadow Power Level 2","Attack foes while invisible"),
|
||||
("Rocket Boots","Zoom through the world with these speedy boots"),
|
||||
("Treasure Map","Follow the trail to find buried treasure"),
|
||||
("ENGLISHpowerup_shield_name","ENGLISHpowerup_shield_desc"),
|
||||
("Venice Disguise","Fool guards in Venice with this disguise"),
|
||||
("Photographer Disguise","Fool guards in China with this disguise"),
|
||||
("Pirate Disguise","Fool Guards in Blood Bath Bay with this disguise"),
|
||||
("Spin Attack Level 1","Press &2T&. and &2S&. to do a spin attack"),
|
||||
("Spin Attack Level 2","Press &2T&., &2T&. and &2S&. to do a powerful spin attack"),
|
||||
("Spin Attack Level 3","Press &2T&., &2T&., &2T&. and &2S&. to do a devastating spin attack"),
|
||||
("Jump Attack Level 1","Press &2T&. and &2X&. to do a jump attack"),
|
||||
("Jump Attack Level 2","Press &2T&., &2T&. and &2X&. to do a powerful jump attack"),
|
||||
("Jump Attack Level 3","Press &2T&., &2T&., &2T&. and &2X&. to do a devastating jump attack"),
|
||||
("Push Attack Level 1","Press &2T&. then &2O&. to do a push attack"),
|
||||
("Push Attack Level 2","Press &2T&., &2T&. and &2O&. to do a powerful push attack"),
|
||||
("Push Attack Level 3","Press &2T&., &2T&., &2T&. and &2O&. to do a devastating push attack"),
|
||||
]
|
||||
|
||||
@@ -78,8 +78,8 @@ m6_dive - 46935C
|
||||
m6_car - 46934C
|
||||
m6_biplane - 46932C
|
||||
m6_gauntlet - 46937C
|
||||
m6_boss - 46933C
|
||||
m6_vault - 46939C
|
||||
m6_boss - 46933C
|
||||
|
||||
#################
|
||||
|
||||
|
||||
52
data/jobs.md
52
data/jobs.md
@@ -156,13 +156,6 @@ 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
|
||||
@@ -175,6 +168,12 @@ Job address: 0x63b4d0
|
||||
Job index: 70
|
||||
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
|
||||
|
||||
## Operation: Turbo Dominant Eagle (sly)
|
||||
Job ID: 3259
|
||||
Job address: 0x651eb0
|
||||
@@ -190,46 +189,41 @@ 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
|
||||
|
||||
## Grapple-Cam Break-in (sly)
|
||||
Job ID: 3540
|
||||
Job address: 0x1625780
|
||||
Job index: 63
|
||||
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
|
||||
|
||||
## Down the Line (murray)
|
||||
Job ID: 3672
|
||||
Job address: 0x16377e0
|
||||
Job index: 117
|
||||
Job state (should be 2): 2
|
||||
|
||||
## A Battery of Peril (bentley)
|
||||
Job ID: 3684
|
||||
@@ -270,6 +264,12 @@ Job address: 0x788780
|
||||
Job index: 59
|
||||
Job state (should be 2): 2
|
||||
|
||||
## Crusher from the Depths (sly)
|
||||
Job ID: 4071
|
||||
Job address: 0x602a30
|
||||
Job index: 98
|
||||
Job state (should be 2): 2
|
||||
|
||||
## Deep Sea Danger (sly)
|
||||
Job ID: 4101
|
||||
Job address: 0x6e52c0
|
||||
@@ -282,12 +282,6 @@ Job address: 0x60fa80
|
||||
Job index: 116
|
||||
Job state (should be 2): 2
|
||||
|
||||
## Crusher from the Depths (sly)
|
||||
Job ID: 4071
|
||||
Job address: 0x602a30
|
||||
Job index: 98
|
||||
Job state (should be 2): 2
|
||||
|
||||
## Operation: Reverse Double-Cross
|
||||
Job ID: 4145
|
||||
Job address: 0x7825e0
|
||||
|
||||
Reference in New Issue
Block a user