diff --git a/src/control_any_sim/main.py b/src/control_any_sim/main.py index 59081b9..53bd02f 100644 --- a/src/control_any_sim/main.py +++ b/src/control_any_sim/main.py @@ -249,6 +249,6 @@ def canys_client_get_selector_visual_type( return original(self, sim_info) -Logger.log("starting control_any_sim") +Logger.log("starting control_any_sim...") InteractionsService.bootstrap() diff --git a/src/control_any_sim/services/selection_group.py b/src/control_any_sim/services/selection_group.py index 06b5d9a..1af12ab 100644 --- a/src/control_any_sim/services/selection_group.py +++ b/src/control_any_sim/services/selection_group.py @@ -131,6 +131,8 @@ def __init__( GameEvents.on_zone_teardown(self.on_zone_teardown) GameEvents.on_active_sim_changed(self.on_active_sim_changed) + GameEvents.on_post_spawn_sim(self.on_spawn_sim) + GameEvents.on_travel_sim_out(self.on_sim_travel_out) def persist_state(self: Self) -> None: """Write current state of the service to disk.""" @@ -311,3 +313,53 @@ def remove_household_npc(self: Self, sim_info: SimInfo) -> None: def is_household_npc(self: Self, sim_info: SimInfo) -> bool: """Check if a given SimInfo is a household NPC.""" return sim_info.id in self.household_npcs + + def on_spawn_sim( + self: Self, + sim: Sim, + ) -> None: + """ + Event handler for when a sim finished spawning. + + Send selectable sim update if the new sim is a controlled NPC. + """ + if not self.is_custom_sim(sim.id): + return + + Logger.log( + f'Sending selectable sim update for spawned NPC "{sim.first_name} {sim.last_name}"', + ) + Logger.log("".join(traceback.format_list(traceback.extract_stack()))) + self.client.send_selectable_sims_update() + + def on_sim_travel_out( + self: Self, + sim_info: SimInfo, + ) -> None: + """ + Event handler for when a sim travels out of the current zone. + + Send selectable sim update if the leaving sim is a controlled NPC. + """ + if not self.is_custom_sim(sim_info.id): + Logger.log( + "Traveling a sim out of the current zone, but it's not a custom selection NPC.", + ) + return + + sim_instance: Sim = sim_info.get_sim_instance( + allow_hidden_flags=ALL_HIDDEN_REASONS, + ) + + if not sim_instance: + Logger.log("there is no sim instance during travel") + if not sim_instance: + Logger.log("there is no sim instance during travel") + + Logger.log( + f'Sending selectable sim update for traveling NPC "{sim_info.first_name} {sim_info.last_name}"', + ) + + sim_instance.schedule_destroy_asap( + post_delete_func=self.client.send_selectable_sims_update, + ) diff --git a/src/control_any_sim/ts4_services/__init__.py b/src/control_any_sim/ts4_services/__init__.py index 4ee79d6..073ea36 100644 --- a/src/control_any_sim/ts4_services/__init__.py +++ b/src/control_any_sim/ts4_services/__init__.py @@ -1,14 +1,17 @@ """Wrapper for service module to add types.""" +from __future__ import annotations + from typing import TYPE_CHECKING import services -from sims.sim_info_manager import SimInfoManager from .clientmanager import ClientManager if TYPE_CHECKING: import server + from sims.sim_info_manager import SimInfoManager + from sims.sim_spawner_service import SimSpawnerService def client_manager() -> ClientManager: @@ -21,3 +24,8 @@ def client_manager() -> ClientManager: def sim_info_manager() -> SimInfoManager: """Typed version of services.sim_info_manager.""" return services.sim_info_manager() + + +def sim_spawner_service(zone_id: int | None = None) -> SimSpawnerService: + """Typed version of services.sim_spawner_manager.""" + return services.sim_spawner_service(zone_id) diff --git a/src/control_any_sim/util/game_events.py b/src/control_any_sim/util/game_events.py index daa1b70..8488568 100644 --- a/src/control_any_sim/util/game_events.py +++ b/src/control_any_sim/util/game_events.py @@ -7,10 +7,13 @@ import services from server.client import Client +from sims.self_interactions import TravelInteraction from sims.sim import Sim +from sims.sim_info import SimInfo from zone import Zone from zone_types import ZoneState +from control_any_sim import ts4_services from control_any_sim.util.inject import inject_method_to from control_any_sim.util.logger import Logger @@ -26,6 +29,8 @@ class GameEvents: OnAddSim: TypeAlias = Callable[[Sim], None] OnLoadingScreenAnimationFinished: TypeAlias = Callable[[Zone], None] OnActiveSimChanged: TypeAlias = Callable[[Sim, Sim], None] + OnTravelSimOut: TypeAlias = Callable[[SimInfo], None] + OnPostSpawnSim: TypeAlias = Callable[[Sim], None] C = TypeVar("C", bound="GameEvents") @@ -35,6 +40,7 @@ class GameEvents: loading_screen_animation_finished_handlers: ClassVar[ list[OnLoadingScreenAnimationFinished] ] = [] + travel_sim_out_handlers: ClassVar[list[OnTravelSimOut]] = [] @classmethod def on_zone_teardown(cls: type[C], handler: OnZoneTeardown) -> None: @@ -108,6 +114,22 @@ def emit_loading_screen_animation_finished( for handler in cls.loading_screen_animation_finished_handlers: handler(current_zone) + @classmethod + def on_travel_sim_out(cls: type[C], handler: OnTravelSimOut) -> None: + """Add a listener for the travel_sim_out event.""" + cls.travel_sim_out_handlers.append(handler) + + @classmethod + def emit_travel_sim_out(cls: type[C], sim_info: SimInfo) -> None: + """Emit the travel sim out event.""" + for handler in cls.travel_sim_out_handlers: + handler(sim_info) + + @staticmethod + def on_post_spawn_sim(handler: OnPostSpawnSim) -> None: + """Adda listener for the post spawn sim event.""" + ts4_services.sim_spawner_service().register_sim_spawned_callback(handler) + @inject_method_to(Zone, "on_teardown") def canys_zone_on_teardown( @@ -177,3 +199,22 @@ def canys_zone_on_loading_screen_animation_finished( Logger.error(traceback.format_exc()) return original(self) + + +@inject_method_to(TravelInteraction, "save_and_destroy_sim") +def canys_travel_internaction_save_and_destroy_sim( + original: Callable[[TravelInteraction, bool, SimInfo], None], + self: TravelInteraction, + on_reset: bool, # noqa: FBT001 + sim_info: SimInfo, +) -> None: + """Wrap the TravelInteraction::save_and_destroy_sim method to emit the corresponding event.""" + try: + result = original(self, on_reset, sim_info) + + GameEvents.emit_travel_sim_out(sim_info) + except Exception as err: + Logger.error(f"{err}") + Logger.error(traceback.format_exc()) + + return result