diff --git a/src/control_any_sim/main.py b/src/control_any_sim/main.py index 9b3eb5d..59081b9 100644 --- a/src/control_any_sim/main.py +++ b/src/control_any_sim/main.py @@ -1,21 +1,25 @@ """Main entry point for canys mod.""" +from __future__ import annotations + import traceback -from typing import Callable +from typing import TYPE_CHECKING, Callable import services from distributor.ops import SetIsNpc from objects.components.sim_inventory_component import ( SimInventoryComponent, ) +from protocolbuffers import Sims_pb2 +from server.client import Client from sims.sim import Sim from sims.sim_info import SimInfo from venues.zone_director_residential import ( ZoneDirectorResidentialBase, ) -from zone import Zone import control_any_sim.cheats +from control_any_sim import ts4_services from control_any_sim.services.integrity import IntegrityService from control_any_sim.services.interactions_service import InteractionsService from control_any_sim.services.selection_group import SelectionGroupService @@ -27,6 +31,10 @@ ) from control_any_sim.util.logger import Logger +if TYPE_CHECKING: + from careers.career_enums import CareerCategory + from zone import Zone + @inject_field_to(SimInfo, "is_npc", (SetIsNpc)) def canys_sim_info_is_npc(original: Callable[[SimInfo], bool], self: SimInfo) -> bool: @@ -94,14 +102,33 @@ def canys_sim_info_get_is_enabled_in_skewer( try: selection_group = SelectionGroupService.get_existing() - if selection_group and selection_group.is_household_npc(self): + if not selection_group: + return original(self, consider_active_sim) + + if selection_group.is_household_npc(self): return False - return original(self, consider_active_sim) + if selection_group.is_custom_sim(self.id): + return True + + return original(self, consider_active_sim and can_consider_active_sim()) + except BaseException: - Logger.log(traceback.format_exc()) + Logger.error(traceback.format_exc()) + return original(self, consider_active_sim) + + +def can_consider_active_sim() -> bool: + """Check if the active sim is part of the current household.""" + client = ts4_services.client_manager().get_active_client() + + if client is None: return True + active_sim_info: SimInfo = client.active_sim_info + + return active_sim_info.household_id == services.active_household_id() + @inject_property_to(Sim, "is_selected") def canys_sim_info_is_selected(original: Callable[[Sim], bool], self: Sim) -> bool: @@ -111,15 +138,13 @@ def canys_sim_info_is_selected(original: Callable[[Sim], bool], self: Sim) -> bo Reduces the logic to wether the Sim is active in the client or not. """ try: - active_household_id = services.active_household_id() - client = services.client_manager().get_client_by_household_id( - active_household_id, - ) + client = ts4_services.client_manager().get_active_client() + + if client is None: + return False - if client is not None: - return self is client.active_sim + return self is client.active_sim - return False except BaseException: Logger.log(traceback.format_exc()) return original(self) @@ -131,9 +156,6 @@ def canys_init_services(_zone: Zone, household_id: int, _active_sim_id: int) -> SelectionGroupService.get(household_id) -InteractionsService.bootstrap() - - @inject_method_to(ZoneDirectorResidentialBase, "_is_any_sim_always_greeted") def canys_zone_director_residential_base_is_any_sim_always_greeted( original: Callable[[ZoneDirectorResidentialBase], bool], @@ -189,4 +211,44 @@ def canys_validate_version(_zone: Zone) -> None: IntegrityService.check_integrety(control_any_sim.__version__) +@inject_method_to(Client, "_get_selector_visual_type") +def canys_client_get_selector_visual_type( + original: Callable[[Client, SimInfo], tuple[int, CareerCategory]], + self: Client, + sim_info: SimInfo, +) -> tuple[int, CareerCategory]: + """ + Override for Client::_get_selector_visual_type method. + + Clear selector visual type override for controled sims. + """ + try: + Logger.log("getting selector visual type") + + selection_group = SelectionGroupService.get_existing() + + (original_type, original_career_category) = original(self, sim_info) + + if not selection_group: + return (original_type, original_career_category) + + if original_type == Sims_pb2.SimPB.OTHER: + Logger.log("original type is OTHER") + sim_zone_id = sim_info.zone_id + + # Override default behavior if the sim is in the current zone. + if sim_zone_id == services.current_zone_id(): + Logger.log("sim is in current zone so return NORMAL") + return (Sims_pb2.SimPB.NORMAL, None) + + return (original_type, original_career_category) + except Exception as err: + Logger.error(f"{err}") + Logger.error(traceback.format_exc()) + + return original(self, sim_info) + + 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 d537054..0276402 100644 --- a/src/control_any_sim/services/selection_group.py +++ b/src/control_any_sim/services/selection_group.py @@ -105,7 +105,8 @@ def _get_instance( return cls(household_id) return instance - except BaseException: + except BaseException as err: + Logger.error(f"Failed to deserialize state: {err}") return cls(household_id) @property @@ -143,9 +144,13 @@ def persist_state(self: Self) -> None: def update_selectable_sims(self: Self) -> None: """Set selection group to all currently selectable sims.""" - selectable_sims = self.client.selectable_sims + selectable_sims: list[SimInfo] = self.client.selectable_sims - self.selectable_sims = [sim_info.id for sim_info in selectable_sims] + self.selectable_sims = [ + sim_info.id + for sim_info in selectable_sims + if sim_info.household_id != self.household_id + ] def on_zone_teardown(self: Self, _zone: Zone, _client: Client) -> None: """ @@ -210,12 +215,18 @@ def setup_zone(self: Self) -> None: def is_selectable(self: Self, sim_id: int) -> bool: """Check if the sim id is currently selectable.""" - test = sim_id in self.selectable_sims + selectable_sims: list[SimInfo] = self.client.selectable_sims + + test = any(sim_info.sim_id == sim_id for sim_info in selectable_sims) Logger.log(f"is sim {sim_id} in selectable list: {test}") return test + def is_custom_sim(self: Self, sim_info_id: int) -> bool: + """Test if a sim is one of the custom sims in the group.""" + return sim_info_id in self.selectable_sims + def on_active_sim_changed(self: Self, _old_sim: Sim, _new_sim: Sim) -> None: """Event handler for when the active sim changes.""" if self.client is None: diff --git a/src/control_any_sim/ts4_services/__init__.py b/src/control_any_sim/ts4_services/__init__.py new file mode 100644 index 0000000..4ee79d6 --- /dev/null +++ b/src/control_any_sim/ts4_services/__init__.py @@ -0,0 +1,23 @@ +"""Wrapper for service module to add types.""" + +from typing import TYPE_CHECKING + +import services +from sims.sim_info_manager import SimInfoManager + +from .clientmanager import ClientManager + +if TYPE_CHECKING: + import server + + +def client_manager() -> ClientManager: + """Typed version of services.client_manager.""" + manager: server.clientmanager.ClientManager = services.client_manager() + + return ClientManager(manager) + + +def sim_info_manager() -> SimInfoManager: + """Typed version of services.sim_info_manager.""" + return services.sim_info_manager() diff --git a/src/control_any_sim/ts4_services/clientmanager.py b/src/control_any_sim/ts4_services/clientmanager.py new file mode 100644 index 0000000..aa1d2ad --- /dev/null +++ b/src/control_any_sim/ts4_services/clientmanager.py @@ -0,0 +1,30 @@ +"""Wrapper for server.clientmanager.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import services + +if TYPE_CHECKING: + from server import clientmanager + from server.client import Client + from typing_extensions import Self + + +class ClientManager: + """Wrapper for server.clientmanager.""" + + inner: clientmanager.ClientManager + + def __init__(self: Self, inner: clientmanager.ClientManager) -> None: + """Create a new wrapper instance.""" + self.inner = inner + + def get_client_by_household_id(self: Self, household_id: int) -> Client | None: + """Get a game client by household id.""" + return self.inner.get_client_by_household_id(household_id) + + def get_active_client(self: Self) -> Client | None: + """Get the client of the active household.""" + return self.get_client_by_household_id(services.active_household_id())