🐐 stuff

This commit is contained in:
Nikolaj
2026-03-03 15:25:30 +01:00
parent c9f4b4c949
commit 072b9a0503
11 changed files with 467 additions and 36 deletions

View File

@@ -1,9 +1,11 @@
from typing import TYPE_CHECKING, Dict, List
from time import time
from NetUtils import ClientStatus
from BaseClasses import ItemClassification
from .data.Constants import REQUIREMENTS
from .data import Items
from .data.Constants import REQUIREMENTS, DEATH_TYPES, EPISODES, CHALLENGES
from .data import Items, Locations
if TYPE_CHECKING:
from .Sly3Client import Sly3Context
@@ -114,18 +116,196 @@ def accessibility(ctx: "Sly3Context") -> Dict[str, Dict[str, List[List[bool]]]]:
"Challenges": challenge_accessibility
}
def set_thiefnet(ctx: "Sly3Context"):
# TODO
pass
def unset_thiefnet(ctx: "Sly3Context"):
# TODO
pass
def check_jobs(ctx: "Sly3Context"):
# TODO
pass
def check_challenges(ctx: "Sly3Context"):
# TODO
pass
def set_powerups(ctx: "Sly3Context"):
if not ctx.in_safehouse():
ctx.game_interface.set_powerups(ctx.powerups)
#########
# Steps #
#########
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)
elif ctx.in_safehouse and not in_safehouse:
ctx.in_safehouse = False
unset_thiefnet(ctx)
async def replace_text(ctx: "Sly3Context"):
# TODO
# I'm not totally sure yet which text I'm replacing
pass
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
if not_connected or ep_not_unlocked or job_not_available:
ctx.game_interface.to_episode_menu()
async def check_jobs_and_challenges(ctx: "Sly3Context"):
check_jobs()
check_challenges()
async def send_checks(ctx: "Sly3Context"):
if ctx.slot_data is None:
return
# ThiefNet purchases
if ctx.in_safehouse:
ctx.thiefnet_purchases = ctx.game_interface.read_powerups()
purchases = list(ctx.thiefnet_purchases)
purchases = (
purchases[4:32] +
purchases[33:40] +
purchases[42:43] +
purchases[45:46]
)
thiefnet_n = ctx.slot_data["thiefnet_locations"]
purchases = purchases[:thiefnet_n]
for i, purchased in enumerate(purchases):
if purchased:
location_name = f"ThiefNet {i+1:02}"
location_code = Locations.location_dict[location_name].code
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]
location_name = f"{episode_name} - {job_name}"
location_code = Locations.location_dict[location_name].code
ctx.locations_checked.add(location_code)
# 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}"
location_code = Locations.location_dict[location_name].code
ctx.locations_checked.add(location_code)
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": ctx.locations_checked}])
async def receive_items(ctx: "Sly3Context"):
# TODO
pass
async def check_goal(ctx: "Sly3Context"):
if ctx.slot_data is None:
return
goal = ctx.slot_data["goal"]
goaled = ctx.game_interface.is_goaled(goal)
if goaled:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
async def handle_job_markers(ctx: "Sly3Context", availability: Dict):
# TODO
pass
async def handle_notifications(ctx: "Sly3Context"):
if (
(ctx.showing_notification and time() - ctx.notification_timestamp < 10) or
(
(not ctx.showing_notification) and
ctx.game_interface.showing_infobox() and
ctx.game_interface.current_infobox() != 0xffffffff
) or
ctx.game_interface.in_cutscene()
):
return
ctx.game_interface.disable_infobox()
ctx.showing_notification = False
if len(ctx.notification_queue) > 0 and ctx.game_interface.in_hub():
new_notification = ctx.notification_queue.pop(0)
ctx.notification_timestamp = time()
ctx.showing_notification = True
ctx.game_interface.set_infobox(new_notification)
async def handle_deathlink(ctx: "Sly3Context"):
if not ctx.death_link_enabled:
return
if time()-ctx.deathlink_timestamp <= 20:
return
if ctx.game_interface.alive():
if ctx.queued_deaths > 0:
ctx.game_interface.kill_player()
ctx.queued_deaths = 0
ctx.deathlink_timestamp = time()
else:
damage_type = ctx.game_interface.get_damage_type()
player_name = ctx.player_names[ctx.slot if ctx.slot else 0]
death_message = DEATH_TYPES.get(damage_type, "{player} died").format(player=player_name)
await ctx.send_death(death_message)
ctx.deathlink_timestamp = time()
##################
# Main Functions #
##################
async def init(ctx: "Sly3Context", ap_connected: bool) -> None:
async def init(ctx: "Sly3Context") -> None:
"""Called when the player connects to the AP server or changes map"""
pass
if ctx.current_map is None or not ctx.is_connected_to_server:
return
async def update(ctx: "Sly3Context", ap_connected: bool) -> None:
if ctx.current_map == 0:
ctx.game_interface.unlock_episodes()
await replace_text(ctx)
# Maybe fix jobs if they break?
async def update(ctx: "Sly3Context") -> None:
"""Called continuously"""
pass
if ctx.current_map is None:
return
availability = accessibility(ctx)
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 send_checks(ctx)
await receive_items(ctx)
await check_goal(ctx)
await handle_job_markers(ctx, availability["Jobs"])
if ctx.in_safehouse:
await handle_notifications(ctx)
await handle_deathlink(ctx)

View File

@@ -59,9 +59,9 @@ class Sly3Context(CommonContext): # type: ignore[misc]
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
notification_queue: list[str] = []
notification_timestamp: float = 0
showing_notification: bool = False
deathlink_timestamp: float = 0
death_link_enabled = False
queued_deaths: int = 0
@@ -71,10 +71,11 @@ class Sly3Context(CommonContext): # type: ignore[misc]
in_safehouse: bool = False
in_hub: bool = False
current_map: Optional[int] = None
current_job: 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}
available_episodes: Dict[Sly3Episode,bool] = {e: False for e in Sly3Episode}
thiefnet_items: Optional[list[str]] = None
powerups: PowerUps = PowerUps()
thiefnet_purchases: PowerUps = PowerUps()
@@ -93,6 +94,7 @@ class Sly3Context(CommonContext): # type: ignore[misc]
self.game_interface = Sly3Interface(logger)
def on_deathlink(self, data: Utils.Dict[str, Utils.Any]) -> None:
# TODO
pass
def make_gui(self):
@@ -139,6 +141,10 @@ class Sly3Context(CommonContext): # type: ignore[misc]
]
}]))
def notification(self):
# TODO
pass
def update_connection_status(ctx: Sly3Context, status: bool):
if ctx.is_connected_to_game == status:
return
@@ -158,6 +164,8 @@ async def _handle_game_not_ready(ctx: Sly3Context):
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.game_interface.skip_cutscene()
@@ -175,9 +183,9 @@ async def _handle_game_ready(ctx: Sly3Context) -> None:
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 init(ctx)
await update(ctx, connected_to_server)
await update(ctx)
if ctx.server:
ctx.last_error_message = None
@@ -200,7 +208,8 @@ async def pcsx2_sync_task(ctx: Sly3Context):
try:
is_connected = ctx.game_interface.get_connection_state()
update_connection_status(ctx, is_connected)
if is_connected:
game_started = ctx.game_interface.is_game_started()
if is_connected and game_started:
await _handle_game_ready(ctx)
else:
await _handle_game_not_ready(ctx)

View File

@@ -195,6 +195,30 @@ class Sly3Interface(GameInterface):
def is_loading(self) -> bool:
return self._read32(self.addresses["loading"]) == 2
def in_safehouse(self) -> bool:
# TODO
return False
def in_hub(self) -> bool:
# TODO
return False
def is_goaled(self) -> bool:
# TODO
return False
def is_game_started(self) -> bool:
# TODO
return False
def showing_infobox(self) -> bool:
# TODO
return False
def alive(self) -> bool:
# TODO
return False
#######################
## Getters & Setters ##
#######################
@@ -244,6 +268,26 @@ class Sly3Interface(GameInterface):
relevant_bits = bits[2:48]
return PowerUps(*relevant_bits)
def activate_jobs(self, job_ids: int|list[int]):
# TODO
pass
def deactivate_jobs(self, job_ids: int|list[int]):
# TODO
pass
def jobs_completed(self, job_ids: int|list[int]):
# TODO
pass
def current_infobox(self) -> int:
# TODO
pass
def get_damage_type(self) -> int:
# TODO
pass
#################
## Other Utils ##
#################
@@ -272,6 +316,18 @@ class Sly3Interface(GameInterface):
new_amount = max(current_amount + to_add,0)
self._write32(self.addresses["coins"],new_amount)
def disable_infobox(self):
# TODO
pass
def set_infobox(self):
# TODO
pass
def kill_player(self):
# TODO
pass
#### TESTING ZONE ####
def read_text(interf: Sly3Interface, address: int):

View File

@@ -13,6 +13,9 @@ from dataclasses import dataclass
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.
"""
display_name = "Starting Episode"
@@ -37,6 +40,21 @@ class Goal(Choice):
option_All_Bosses = 6
default = 5
class BonusCrewMember(Choice):
"""
Which crew member, in addition to Sly, you start with.
"""
display_name = "Bonus Crew Member"
option_None = 0
option_Bentley = 1
option_Murray = 2
option_Guru = 3
option_Penelope = 4
option_Panda_King = 5
option_Dimitri = 6
option_Carmelita = 7
default = 1
class IncludeMegaJump(Toggle):
"""
@@ -77,7 +95,7 @@ class ThiefNetLocations(Range):
display_name = "ThiefNet Locations"
range_start = 0
range_end = 37
default = 25
default = 20
class ThiefNetCostMinimum(Range):
"""
@@ -112,12 +130,14 @@ class Sly3Options(PerGameCommonOptions):
thiefnet_locations: ThiefNetLocations
thiefnet_minimum: ThiefNetCostMinimum
thiefnet_maximum: ThiefNetCostMaximum
bonus_crew_member: BonusCrewMember
sly3_option_groups = [
OptionGroup("Goal",[
Goal
]),
OptionGroup("Items",[
BonusCrewMember,
IncludeMegaJump,
CoinsMinimum,
CoinsMaximum

View File

@@ -28,11 +28,23 @@ def gen_powerups(world: "Sly3World") -> list[Item]:
def gen_crew(world: "Sly3World") -> list[Item]:
"""Generate the crew for the item pool"""
crew = []
for item_name in item_groups["Crew"]:
crew.append(world.create_item(item_name))
crew = list(item_groups["Crew"])
return crew
bonus_crew_n = world.options.bonus_crew_member.value
if bonus_crew_n != 0:
bonus_crew = [
"Bentley",
"Murray",
"Guru",
"Penelope",
"Panda King",
"Dimitri",
"Carmelita"
][bonus_crew_n-1]
crew.remove(bonus_crew)
world.multiworld.push_precollected(world.create_item(bonus_crew))
return [world.create_item(c) for c in crew]
def gen_episodes(world: "Sly3World") -> list[Item]:
"""Generate the episodes items for the item pool"""

View File

@@ -46,7 +46,6 @@ def create_regions_sly3(world: "Sly3World"):
world.multiworld.regions.append(menu)
for i, episode in enumerate(EPISODES.keys()):
print(f"==={episode}===")
for n in range(1,5):
if n == 2 and episode == "Honor Among Thieves":
break

View File

@@ -5,6 +5,7 @@ from BaseClasses import CollectionState
from worlds.generic.Rules import add_rule
from .data.Constants import EPISODES, CHALLENGES, REQUIREMENTS
from .data.Locations import location_dict
if typing.TYPE_CHECKING:
from . import Sly3World
@@ -78,23 +79,19 @@ def set_rules_sly3(world: "Sly3World"):
][world.options.goal.value]
victory_location = world.multiworld.get_location(victory_condition, world.player)
victory_location.address = None
victory_location.place_locked_item(world.create_event("Victory"))
world.multiworld.completion_condition[world.player] = lambda state: state.has("Victory", world.player)
elif world.options.goal.value == 6:
def access_rule(state: CollectionState):
victory_conditions = [
"An Opera of Fear - Operation: Tar-Be Gone!",
"Rumble Down Under - Operation: Moon Crash",
"Flight of Fancy - Operation: Turbo Dominant Eagle",
"A Cold Alliance - Operation: Wedding Crasher",
"Dead Men Tell No Tales - Operation: Reverse Double-Cross",
"Honor Among Thieves - Final Legacy"
]
all_requirements = list(set(sum([sum(sum(ep,[]),[]) for ep in REQUIREMENTS["Jobs"].values()],[])))
menu_region = world.multiworld.get_region("Menu", world.player)
menu_region.add_locations({"All Bosses": location_dict["All Bosses"].code})
return all(
world.multiworld.get_location(cond,world.player).access_rule(state)
for cond in victory_conditions
victory_location = world.multiworld.get_location("All Bosses", world.player)
add_rule(
victory_location,
lambda state, items=reqs: (
all(state.has(item, player) for item in all_requirements)
)
)
world.multiworld.completion_condition[world.player] = access_rule
victory_location.address = None
victory_location.place_locked_item(world.create_event("Victory"))
world.multiworld.completion_condition[world.player] = lambda state: state.has("Victory", world.player)

View File

@@ -109,6 +109,7 @@ class Sly3World(World):
self.options.thiefnet_locations.value = slot_data["thiefnet_locations"]
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"]
return
self.validate_options(self.options)
@@ -159,6 +160,7 @@ class Sly3World(World):
"thiefnet_locations",
"thiefnet_minimum",
"thiefnet_maximum",
"bonus_crew_member",
)
def fill_slot_data(self) -> Mapping[str, Any]:

View File

@@ -523,3 +523,5 @@ MENU_RETURN_DATA = (
"7F2319BC"+
"7B8274B1"
)
DEATH_TYPES = {} # TODO

View File

@@ -26,7 +26,7 @@ challenges_list = [
for challenge in challenges
]
location_list = jobs_list + purchases_list + challenges_list
location_list = jobs_list + purchases_list + challenges_list + [("All Bosses", "-")]
base_code = 8008135

154
data/completed.txt Normal file
View File

@@ -0,0 +1,154 @@
m1_recon - 468FEC
m1_follow - 468FAC
m1_gauntlet - 468FBC
m1_canal_chase - 468F7C
m1_turf_war - 46900C
m1_ball - 468F6C
m1_run - 468FFC
m1_disguise - 468F9C
m1_heist - 468FCC
#################
m2_recon - 46909C
m2_bridge - 46902C
m2_cave - 46903C
m2_dumptruck - 46905C
m2_shaman - 4690AC
m2_claw - 46904C
m2_bar_brawl - 46901C
m2_feed_croc - 46906C
m2_heist - 46907C
#################
m3_roster - 46916C
m3_frame_a - 4690DC
m3_frame_b - 4690EC
m3_hangar_defense - 46910C
m3_semifinals - 46917C
m3_wolf - 46919C
m3_windmill_hack - 46918C
m3_muggshot - 46914C
m3_heist - 46911C
#################
m4_recon - 46920C
m4_get_job - 4691BC
m4_van - 46922C
m4_webcam - 46923C
m4_stealing_thiefnet - 46921C
m4_zombies - 46925C
m4_rccar - 4691FC
m4_battery - 4691AC
m4_heist - 4691CC
#################
m5_recon - 4692FC
m5_love - 4692CC
m5_jollyboat - 4692BC
m5_treasure - 46931C
m5_squid - 46930C
m5_dive - 46929C
m5_battle - 46926C
m5_heist - 4692AC
#################
m6_freesly - 46936C
m6_shark - 46938C
m6_dive - 46935C
m6_car - 46934C
m6_biplane - 46932C
m6_gauntlet - 46937C
m6_boss - 46933C
m6_vault - 46939C
#################
w1_canal_chase_health - 46DF30
w1_ball_timed - 46DF20
w1_run_gauntlet - 46DF98
w1_run_chase - 46DF90
w1_heist_bombing - 46DF60
w1_heist_canal - 46DF70
w1_heist_boss - 46DF68o
w1_map - 46DF78
#################
w2_recon_gauntlet - 46E028
w2_cave_a_enter - 46DFC8
w2_cave_b_escape - 46DFD0
w2_dumptruck_tower - 46DFF0
w2_shaman_guards - 46E038
w2_claw_section2 - 46DFE0
w2_bar_brawl_timed - 46DFB0
w2_feed_croc_brawl - 46E000
w2_heist_c - 46E010
w2_map - 46E018
#################
w3_roster_ascension - 46E0B0
w3_hangar_defense_brawl - 46E068
w3_hangar_defense_security - 46E078
w3_hangar_defense_chopper - 46E070
w3_semifinals_spree - 46E0C0
w3_wolf_ride - 46E0D8
w3_muggshot_spree - 46E0A0
w3_heist_boss - 46E088
w3_map - 46E090
#################
w4_murray_bounce_timed - 46E100
w4_van_turret - 46E138
w4_stealing_thiefnet_treetop - 46E128
w4_stealing_thiefnet_ground - 46E120
w4_map - 46E0F8
#################
w5_recon_patch - 46E1B0
w5_recon_peg_leg - 46E1B8
w5_jollyboat_armor - 46E190
w5_boat_hunt - 46E160
w5_map - 46E1A0
#################
w6_freesly_battle - 46E210
w6_car_battle - 46E1F8
w6_biplane_boss - 46E1D8
w6_gauntlet_timed - 46E220
w6_boss_timed - 46E1E8