class TunableRewardBuff(AutoFactoryInit, TunableRewardBase): FACTORY_TUNABLES = { 'buff': TunableBuffReference( description= '\n Buff to be given as a reward.\n ') } @constproperty def reward_type(): return RewardType.BUFF def open_reward(self, sim_info, reward_destination=RewardDestination.HOUSEHOLD, **kwargs): if reward_destination == RewardDestination.HOUSEHOLD: household = sim_info.household for sim_info in household.sim_info_gen(): sim_info.add_buff_from_op(buff_type=self.buff.buff_type, buff_reason=self.buff.buff_reason) elif reward_destination == RewardDestination.SIM: sim_info.add_buff_from_op(buff_type=self.buff.buff_type, buff_reason=self.buff.buff_reason) else: logger.error( 'Attempting to open a RewardBuff with an invalid destination: {}. Reward buffs can only be given to households or Sims.', reward_destination)
class DayNightTracking(HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = { 'sunlight_buffs': TunableSet( description= "\n Allows a list of buffs to be added to the owning Sim when they're in\n the sunlight.\n \n These buffs are also guaranteed to be removed from the Sim when\n they're no longer in sunlight, regardless of where the buff was\n applied. For instance, if an interaction has a basic extra that also\n applied a buff in this list, but the Sim is given this trait and\n they're not in the sunlight. That buff will be removed.\n \n Do not rely on Sunlight Buffs and Shade Buffs to be perfectly\n mutually exclusive. It's possible, due to timing issues, that both\n buffs in Sunlight Buffs and buffs in Shade buffs can be on the sim\n at the same time, or neither on the sim, for a brief amount of time.\n If you need buff exclusivity, use the tuning on buffs.\n ", tunable=TunableBuffReference( description= "\n The buff to be added to the owning Sim when they're in the\n sunlight.\n ", pack_safe=True)), 'shade_buffs': TunableSet( description= "\n Allows a list of buffs to be added to the owning Sim when they're\n not in the sunlight.\n \n These buffs are also guaranteed to be removed from the Sim when\n they're no longer in the shade, regardless of where the buff was\n applied. For instance, if an interaction has a basic extra that also\n applied a buff in this list, but the Sim is given this trait and\n they're not in the shade. That buff will be removed.\n \n Do not rely on Sunlight Buffs and Shade Buffs to be perfectly\n mutually exclusive. It's possible, due to timing issues, that both\n Sunlight Buffs and Shade Buffs can be on the Sim at the same time,\n or neither on the Sim, for a brief amount of time. If you need buff\n exclusivity, use the tuning on buffs.\n ", tunable=TunableBuffReference( description= "\n The buff to be added to the owning Sim when they're not in the\n sunlight.\n ", pack_safe=True)), 'day_buffs': TunableSet( description= "\n Allows a list of buffs to be added to the owning Sim when it's\n currently day time in the region (based on Sunrise and Sunset time\n tuning for the Region).\n \n These buffs are also guaranteed to be removed from the Sim when it's\n no longer day time, regardless of where the buff was applied. For\n instance, if an interaction has a basic extra that also applied a\n buff in this list, but the Sim is given this trait and it's not day\n time. That buff will be removed.\n \n Do not rely on Day Buffs and Night Buffs to be perfectly\n mutually exclusive. It's possible, due to timing issues, that both\n Day Buffs and Night Buffs can be on the Sim at the same time,\n or neither on the Sim, for a brief amount of time. If you need buff\n exclusivity, use the tuning on buffs.\n ", tunable=TunableBuffReference( description= "\n The buff to be added to the owning Sim when it's day time.\n ", pack_safe=True)), 'night_buffs': TunableSet( description= "\n Allows a list of buffs to be added to the owning Sim when it's\n currently night time in the region (based on Sunrise and Sunset time\n tuning for the Region).\n \n These buffs are also guaranteed to be removed from the Sim when it's\n no longer night time, regardless of where the buff was applied. For\n instance, if an interaction has a basic extra that also applied a\n buff in this list, but the Sim is given this trait and it's not\n night time. That buff will be removed.\n \n Do not rely on Day Buffs and Night Buffs to be perfectly\n mutually exclusive. It's possible, due to timing issues, that both\n Day Buffs and Night Buffs can be on the Sim at the same time,\n or neither on the Sim, for a brief amount of time. If you need buff\n exclusivity, use the tuning on buffs.\n ", tunable=TunableBuffReference( description= "\n The buff to be added to the owning Sim when it's night time.\n ", pack_safe=True)), 'force_refresh_buffs': TunableSet( description= '\n This is the list of buffs, which upon removal, refreshes the status \n of day-night-sunlight buffs. This is needed because when the vampire \n resistance cocktail buff expires, we have no good way of adding the \n burnt-by-sun buff automatically. Any buff which should refresh the \n day-night-sunlight buff should be added to this list.\n ', tunable=TunableBuffReference( description= '\n The buff that upon removal will force a refresh on the \n ', pack_safe=True)) }
def __init__(self, *args, **kwargs): super().__init__( *args, schedule=TunableList(tunable=TunableTuple( description= "\n Define a Sim's sleep pattern by applying buffs at\n certain times before their scheduled work time. If Sim's\n don't have a job, define an arbitrary time and define\n buffs relative to that.\n ", time_from_work_start=Tunable( description= '\n The time relative to the start work time that the buff\n should be added. For example, if you want the Sim to\n gain this static commodity 10 hours before work, set\n this value to 10.\n ', tunable_type=float, default=0), buff=TunableBuffReference( description= '\n Buff that gets added to the Sim.\n ', allow_none=True))), default_work_time=TunableTimeOfDay( description= "\n The default time that the Sim assumes he needs to be at work\n if he doesn't have a career. This is only used for sleep.\n ", default_hour=9), **kwargs)
class BuffOp(BaseLootOperation): __qualname__ = 'BuffOp' @staticmethod def _verify_tunable_callback(instance_class, tunable_name, source, value): if value._buff.buff_type is None: logger.error('BuffOp: There is no buff set for loot op in {}:{}', instance_class, source, owner='msantander') FACTORY_TUNABLES = { 'buff': TunableBuffReference(), 'verify_tunable_callback': _verify_tunable_callback } def __init__(self, buff, **kwargs): super().__init__(**kwargs) self._buff = buff def _apply_to_subject_and_target(self, subject, target, resolver): subject.add_buff_from_op(self._buff.buff_type, self._buff.buff_reason) def apply_to_interaction_statistic_change_element(self, interaction): if not self._buff.buff_type.commodity: return autonomy_modifier_handlers = None locked_stat = [self._buff.buff_type.commodity] for recipient in interaction.get_participants(self.subject): while recipient.add_buff_from_op(self._buff.buff_type, self._buff.buff_reason): if autonomy_modifier_handlers is None: autonomy_modifier_handlers = {} autonomy_modifier_handlers[recipient] = AutonomyModifier( locked_stats=locked_stat) return autonomy_modifier_handlers
class SituationManager(DistributableObjectManager): DEFAULT_LEAVE_SITUATION = sims4.tuning.tunable.TunableReference( description= '\n The situation type for the background leave situation.\n It collects sims who are not in other situations and\n asks them to leave periodically.\n ', manager=services.get_instance_manager(sims4.resources.Types.SITUATION), class_restrictions=situations.complex.leave_situation.LeaveSituation) DEFAULT_LEAVE_NOW_MUST_RUN_SITUATION = sims4.tuning.tunable.TunableReference( description= '\n The situation type that drives the sim off the lot pronto.\n ', manager=services.get_instance_manager(sims4.resources.Types.SITUATION), class_restrictions=situations.complex.single_sim_leave_situation. SingleSimLeaveSituation) DEFAULT_VISIT_SITUATION = sims4.tuning.tunable.TunableReference( description= '\n The default visit situation used when you ask someone to \n hang out or invite them in.\n ', manager=services.get_instance_manager(sims4.resources.Types.SITUATION)) DEFAULT_TRAVEL_SITUATION = Situation.TunableReference( description= ' \n The default situation for when you \n are simply traveling with a group \n of Sims.\n ' ) LEAVE_INTERACTION_TAGS = TunableSet( description= '\n The tags indicating leave lot interactions, but not \n leave lot must run interactions.\n These are used to determine if a leave lot interaction is running\n or cancel one if it is.\n ', tunable=TunableEnumWithFilter(tunable_type=tag.Tag, default=tag.Tag.INVALID, tuning_filter=FilterTag.EXPERT_MODE, filter_prefixes=tag.INTERACTION_PREFIX)) SUPER_SPEED_THREE_REQUEST_BUFF = TunableBuffReference( description= "\n The buff to apply to the Sim when we're trying to make them run the\n leave situation from a super speed three request.\n ", deferred=True) DEFAULT_PLAYER_PLANNED_DRAMA_NODE = sims4.tuning.tunable.TunableReference( description= '\n The drama node that will be scheduled when a player plans an event for the future.\n ', manager=services.get_instance_manager( sims4.resources.Types.DRAMA_NODE)) _perf_test_cheat_enabled = False def __init__(self, manager_id=0): super().__init__(manager_id=manager_id) self._get_next_session_id = UniqueIdGenerator(1) self._added_to_distributor = set() self._callbacks = defaultdict(lambda: defaultdict(CallableList)) self._departing_situation_seed = None self._arriving_situation_seed = None self._zone_seeds_for_zone_spinup = [] self._open_street_seeds_for_zone_spinup = [] self._debug_sims = set() self._leave_situation_id = 0 self._player_greeted_situation_id = 0 self._player_waiting_to_be_greeted_situation_id = 0 self._sim_being_created = None self._sim_data = {} self._delay_situation_destruction_ref_count = 0 self._situations_for_delayed_destruction = set() self._bouncer = None self._pause_handle = None self._zone_spin_up_greeted_complete = False @classproperty def save_error_code(cls): return persistence_error_types.ErrorCodes.SERVICE_SAVE_FAILED_SITUATION_MANAGER def start(self): self._bouncer = Bouncer() def stop(self): if self._pause_handle is not None: pause_handle = self._pause_handle self._pause_handle = None services.game_clock_service().remove_request( pause_handle, source=GameSpeedChangeSource.SITUATION) def destroy_situations_on_teardown(self): self.destroy_all_situations(include_system=True) self._sim_data.clear() self._bouncer.destroy() self._bouncer = None def reset(self, create_system_situations=True): self.destroy_all_situations(include_system=True) self._added_to_distributor.clear() self._callbacks.clear() self._bouncer.reset() if create_system_situations: self._create_system_situations() def update(self): if self._bouncer is not None: try: self._bouncer._update() except Exception: logger.exception('Exception while updating the Bouncer.') def enable_perf_cheat(self, enable=True): self._perf_test_cheat_enabled = enable self._bouncer.spawning_freeze(enable) self._bouncer.cap_cheat(enable) def get_all(self): return [ obj for obj in self._objects.values() if obj._stage == SituationStage.RUNNING ] def get_new_situation_creation_session(self): return self._get_next_session_id() @property def bouncer(self): return self._bouncer @property def sim_being_created(self): return self._sim_being_created def add_debug_sim_id(self, sim_id): self._debug_sims.add(sim_id) def _determine_player_greeted_status_during_zone_spin_up(self): if not services.current_zone( ).venue_service.active_venue.requires_visitation_rights: return GreetedStatus.NOT_APPLICABLE active_household = services.active_household() if active_household is None: return GreetedStatus.NOT_APPLICABLE if active_household.considers_current_zone_its_residence(): return GreetedStatus.NOT_APPLICABLE cur_status = GreetedStatus.WAITING_TO_BE_GREETED lot_seeds = list(self._zone_seeds_for_zone_spinup) if self._arriving_situation_seed is not None: lot_seeds.append(self._arriving_situation_seed) for seed in lot_seeds: status = seed.get_player_greeted_status() logger.debug('Player:{} :{}', status, seed.situation_type, owner='sscholl') if status == GreetedStatus.GREETED: cur_status = status break return cur_status def get_npc_greeted_status_during_zone_fixup(self, sim_info): if not services.current_zone( ).venue_service.active_venue.requires_visitation_rights: return GreetedStatus.NOT_APPLICABLE if sim_info.lives_here: return GreetedStatus.NOT_APPLICABLE cur_status = GreetedStatus.NOT_APPLICABLE for seed in self._zone_seeds_for_zone_spinup: status = seed.get_npc_greeted_status(sim_info) logger.debug('NPC:{} :{} :{}', sim_info, status, seed.situation_type, owner='sscholl') if status == GreetedStatus.GREETED: cur_status = status break if status == GreetedStatus.WAITING_TO_BE_GREETED: cur_status = status return cur_status def is_player_greeted(self): return self._player_greeted_situation_id != 0 def is_player_waiting_to_be_greeted(self): return self._player_waiting_to_be_greeted_situation_id != 0 and self._player_greeted_situation_id == 0 def create_situation(self, situation_type, guest_list=None, user_facing=True, duration_override=None, custom_init_writer=None, zone_id=0, scoring_enabled=True, spawn_sims_during_zone_spin_up=False, creation_source=None, travel_request_kwargs=frozendict(), linked_sim_id=GLOBAL_SITUATION_LINKED_SIM_ID, scheduled_time=None, **extra_kwargs): zone = services.current_zone() if zone.is_zone_shutting_down: return current_zone_id = services.current_zone_id() situation_type = services.narrative_service( ).get_possible_replacement_situation(situation_type) if services.get_zone_modifier_service().is_situation_prohibited( zone_id if zone_id else current_zone_id, situation_type): return if guest_list is None: guest_list = SituationGuestList() hire_cost = guest_list.get_hire_cost() host_sim_info = guest_list.host_sim_info if host_sim_info is not None and not host_sim_info.household.funds.try_remove( situation_type.cost() + hire_cost, Consts_pb2.TELEMETRY_EVENT_COST, host_sim_info): return situation_id = id_generator.generate_object_id() self._send_create_situation_telemetry(situation_type, situation_id, guest_list, hire_cost, zone_id) if zone_id and zone_id != current_zone_id and scheduled_time is None: return self._create_situation_and_travel( situation_type, situation_id, guest_list, user_facing, duration_override, custom_init_writer, zone_id, scoring_enabled=scoring_enabled, creation_source=creation_source, linked_sim_id=linked_sim_id, travel_request_kwargs=travel_request_kwargs) situation_seed = SituationSeed( situation_type, SeedPurpose.NORMAL, situation_id, guest_list, user_facing=user_facing, duration_override=duration_override, zone_id=zone_id, scoring_enabled=scoring_enabled, spawn_sims_during_zone_spin_up=spawn_sims_during_zone_spin_up, creation_source=creation_source, linked_sim_id=linked_sim_id, **extra_kwargs) if custom_init_writer is not None: situation_seed.setup_for_custom_init_params(custom_init_writer) return_id = None if scheduled_time is not None: uid = services.drama_scheduler_service().schedule_node( self.DEFAULT_PLAYER_PLANNED_DRAMA_NODE, SingleSimResolver(guest_list.host_sim.sim_info), specific_time=scheduled_time, situation_seed=situation_seed) return_id = situation_id if uid is not None else None else: return_id = self.create_situation_from_seed(situation_seed) return return_id def _create_situation_and_travel(self, situation_type, *args, travel_request_kwargs, **kwargs): travel_fn = lambda: self._create_departing_seed_and_travel( situation_type, *args, **kwargs) travel_request_situtaion = None for situation in self.get_user_facing_situations_gen(): if travel_request_situtaion is None: travel_request_situtaion = situation elif situation.travel_request_behavior.restrict > travel_request_situtaion.travel_request_behavior.restrict: travel_request_situtaion = situation if travel_request_situtaion is not None: return travel_request_situtaion.travel_request_behavior( travel_request_situtaion, situation_type, travel_fn, **travel_request_kwargs) return travel_fn() def create_visit_situation_for_unexpected(self, sim): duration_override = None if self._perf_test_cheat_enabled: duration_override = 0 self.create_visit_situation(sim, duration_override=duration_override) def create_visit_situation(self, sim, duration_override=None, visit_type_override=None): situation_id = None visit_type = visit_type_override if visit_type_override is not None else self.DEFAULT_VISIT_SITUATION if visit_type is not None: guest_list = situations.situation_guest_list.SituationGuestList( invite_only=True) guest_info = situations.situation_guest_list.SituationGuestInfo.construct_from_purpose( sim.id, visit_type.default_job(), situations. situation_guest_list.SituationInvitationPurpose.INVITED) guest_list.add_guest_info(guest_info) situation_id = self.create_situation( visit_type, guest_list=guest_list, user_facing=False, duration_override=duration_override) if situation_id is None: logger.error('Failed to create visit situation for sim: {}', sim) self.make_sim_leave(sim) return situation_id def create_situation_from_seed(self, seed): if not seed.allow_creation: return if seed.user_facing: for situation in tuple(self.get_user_facing_situations_gen()): if seed.linked_sim_id == GLOBAL_SITUATION_LINKED_SIM_ID: if situation.linked_sim_id == GLOBAL_SITUATION_LINKED_SIM_ID: self.destroy_situation_by_id(situation.id) if seed.situation_type.is_unique_situation: for situation in self.running_situations(): if type(situation) is seed.situation_type: return try: situation = seed.situation_type(seed) except ValueError: logger.exception('Failed to initialize situation: {}', seed.situation_type) return try: if seed.is_loadable: if not situation.load_situation(): situation._destroy() return else: situation.start_situation() except Exception: logger.exception('Exception thrown while starting situation') situation._destroy() return if situation._stage == SituationStage.DYING: return self.add(situation) if situation.is_user_facing or situation.distribution_override: distributor.system.Distributor.instance().add_object(situation) self._added_to_distributor.add(situation) situation.on_added_to_distributor() return situation.id def travel_existing_situation(self, situation, zone_id): seed = situation.save_situation() seed.zone_id = zone_id self.travel_seed(seed) situation._self_destruct() def _create_departing_seed_and_travel( self, situation_type, situation_id, guest_list=None, user_facing=True, duration_override=None, custom_init_writer=None, zone_id=0, scoring_enabled=True, creation_source=None, linked_sim_id=GLOBAL_SITUATION_LINKED_SIM_ID): current_zone = services.current_zone() if current_zone is not None and not current_zone.is_zone_running: logger.error( 'Unable to travel during spin-up: {}. A travel interaction was save/loaded, which is incorrect. Make it one-shot or non-saveable.', situation_type) return traveling_sim = guest_list.get_traveler() if traveling_sim is None: logger.error( 'No traveling Sim available for creating departing seed for situation: {}.', situation_type) return if traveling_sim.client is None: logger.error( 'No client on traveling Sim: {} for for situation: {}.', traveling_sim, situation_type) return if traveling_sim.household is None: logger.error( 'No household on traveling Sim for for situation: {}.', situation_type) return situation_seed = SituationSeed(situation_type, SeedPurpose.TRAVEL, situation_id, guest_list, user_facing, duration_override, zone_id, scoring_enabled=scoring_enabled, creation_source=creation_source, linked_sim_id=linked_sim_id) if situation_seed is None: logger.error('Failed to create departing seed for situation: {}.', situation_type) return if custom_init_writer is not None: situation_seed.setup_for_custom_init_params(custom_init_writer) return self.travel_seed(situation_seed) def travel_seed(self, seed): self._departing_situation_seed = seed traveling_sim = seed.guest_list.get_traveler() travel_info = protocolbuffers.InteractionOps_pb2.TravelSimsToZone() travel_info.zone_id = seed.zone_id travel_info.sim_ids.append(traveling_sim.id) traveling_sim_ids = seed.guest_list.get_other_travelers(traveling_sim) travel_info.sim_ids.extend(traveling_sim_ids) distributor.system.Distributor.instance().add_event( protocolbuffers.Consts_pb2.MSG_TRAVEL_SIMS_TO_ZONE, travel_info) if self._pause_handle is None: self._pause_handle = services.game_clock_service().push_speed( ClockSpeedMode.PAUSED, reason='Situation Travel', source=GameSpeedChangeSource.SITUATION) logger.debug('Travel seed now time {}', services.time_service().sim_now) logger.debug('Travel seed future time {}', services.time_service().sim_future) return seed.situation_id def _create_system_situations(self): self._leave_situation_id = 0 for situation in self.running_situations(): if type(situation) is self.DEFAULT_LEAVE_SITUATION: self._leave_situation_id = situation.id break if self._leave_situation_id == 0: self._leave_situation_id = self.create_situation( self.DEFAULT_LEAVE_SITUATION, user_facing=False, duration_override=0) @property def auto_manage_distributor(self): return False def call_on_remove(self, situation): super().call_on_remove(situation) self._callbacks.pop(situation.id, None) if situation in self._added_to_distributor: dist = distributor.system.Distributor.instance() dist.remove_object(situation) self._added_to_distributor.remove(situation) situation.on_removed_from_distributor() def is_distributed(self, situation): return situation in self._added_to_distributor def _request_destruction(self, situation): if self._delay_situation_destruction_ref_count == 0: return True self._situations_for_delayed_destruction.add(situation) return False def pre_destroy_situation_by_id(self, situation_id): situation = self.get(situation_id) if situation is not None: situation.pre_destroy() def destroy_situation_by_id(self, situation_id): if situation_id in self: if situation_id == self._leave_situation_id: self._leave_situation_id = 0 if situation_id == self._player_greeted_situation_id: self._player_greeted_situation_id = 0 if situation_id == self._player_waiting_to_be_greeted_situation_id: self._player_waiting_to_be_greeted_situation_id = 0 self.remove_id(situation_id) def destroy_all_situations(self, include_system=False): all_situations = tuple(self.values()) for situation in all_situations: if include_system == False and situation.id == self._leave_situation_id: continue try: self.destroy_situation_by_id(situation.id) except Exception: logger.error( 'Error when destroying situation {}. You are probably screwed.', situation) def register_for_callback(self, situation_id, situation_callback_option, callback_fn): if situation_id not in self: logger.error( "Failed to register situation callback. Situation doesn't exist. {}, {}, {}", situation_id, situation_callback_option, callback_fn, owner='rmccord') return callable_list = self._callbacks[situation_id][ situation_callback_option] if callback_fn not in callable_list: callable_list.append(callback_fn) self._callbacks[situation_id][ situation_callback_option] = callable_list def unregister_callback(self, situation_id, situation_callback_option, callback_fn): if situation_id not in self: return callable_list = self._callbacks[situation_id][ situation_callback_option] if callback_fn in callable_list: callable_list.remove(callback_fn) self._callbacks[situation_id][ situation_callback_option] = callable_list def create_greeted_npc_visiting_npc_situation(self, npc_sim_info): services.current_zone().venue_service.active_venue.summon_npcs( (npc_sim_info, ), venues.venue_constants.NPCSummoningPurpose.PLAYER_BECOMES_GREETED) def _create_greeted_player_visiting_npc_situation(self, sim=None): if sim is None: guest_list = situations.situation_guest_list.SituationGuestList() else: guest_list = situations.situation_guest_list.SituationGuestList( host_sim_id=sim.id) greeted_situation_type = services.current_zone( ).venue_service.active_venue.player_greeted_situation_type if greeted_situation_type is None: return self._player_greeted_situation_id = self.create_situation( greeted_situation_type, user_facing=False, guest_list=guest_list) def _create_player_waiting_to_be_greeted_situation(self): self._player_waiting_to_be_greeted_situation_id = self.create_situation( services.current_zone( ).venue_service.active_venue.player_ungreeted_situation_type, user_facing=False) def make_player_waiting_to_be_greeted_during_zone_spin_up(self): waiting_situation_type = services.current_zone( ).venue_service.active_venue.player_ungreeted_situation_type for situation in self.running_situations(): if type(situation) is waiting_situation_type: self._player_waiting_to_be_greeted_situation_id = situation.id break else: self._create_player_waiting_to_be_greeted_situation() def make_player_greeted_during_zone_spin_up(self): greeted_situation_type = services.current_zone( ).venue_service.active_venue.player_greeted_situation_type for situation in self.running_situations(): if type(situation) is greeted_situation_type: self._player_greeted_situation_id = situation.id break else: self._create_greeted_player_visiting_npc_situation() def destroy_player_waiting_to_be_greeted_situation(self): if self._player_waiting_to_be_greeted_situation_id is 0: return situation = self.get(self._player_waiting_to_be_greeted_situation_id) if situation is None: return situation._self_destruct() self._player_waiting_to_be_greeted_situation_id = 0 def make_waiting_player_greeted(self, door_bell_ringing_sim=None): for situation in self.running_situations(): situation._on_make_waiting_player_greeted(door_bell_ringing_sim) if self._player_greeted_situation_id == 0: self._create_greeted_player_visiting_npc_situation( door_bell_ringing_sim) def get_situation_by_type(self, situation_type): for situation in self.running_situations(): if type(situation) is situation_type: return situation def get_situations_by_type(self, *situation_types): found_situations = [] for situation in self.running_situations(): if isinstance(situation, situation_types): found_situations.append(situation) return found_situations def get_situations_by_tags(self, situation_tags): found_situations = [] for situation in self.running_situations(): if situation.tags & situation_tags: found_situations.append(situation) return found_situations def is_situation_running(self, situation_type): return any( isinstance(situation, situation_type) for situation in self.running_situations()) def disable_save_to_situation_manager(self, situation_id): situation = self.get(situation_id) if situation is not None: situation.save_to_situation_manager = False def save(self, zone_data=None, open_street_data=None, save_slot_data=None, **kwargs): if zone_data is None: return zone = services.current_zone() if zone.venue_service.build_buy_edit_mode: return self._save_for_edit_mode(zone_data=zone_data, open_street_data=open_street_data, save_slot_data=save_slot_data) SituationSeed.serialize_travel_seed_to_slot( save_slot_data, self._departing_situation_seed) zone_seeds = [] street_seeds = [] holiday_seeds = [] for situation in self.running_situations(): if not situation.save_to_situation_manager: continue seed = situation.save_situation() if seed is not None: if situation.situation_serialization_option == SituationSerializationOption.OPEN_STREETS: street_seeds.append(seed) elif situation.situation_serialization_option == SituationSerializationOption.LOT: zone_seeds.append(seed) else: holiday_seeds.append(seed) SituationSeed.serialize_seeds_to_zone(zone_seeds=zone_seeds, zone_data_msg=zone_data, blacklist_data=self._sim_data) SituationSeed.serialize_seeds_to_open_street( open_street_seeds=street_seeds, open_street_data_msg=open_street_data) active_household = services.active_household() if active_household is not None: active_household.holiday_tracker.set_holiday_situation_seeds( holiday_seeds) def _save_for_edit_mode(self, zone_data=None, open_street_data=None, save_slot_data=None): SituationSeed.serialize_travel_seed_to_slot( save_slot_data, self._arriving_situation_seed) SituationSeed.serialize_seeds_to_zone( zone_seeds=self._zone_seeds_for_zone_spinup, zone_data_msg=zone_data, blacklist_data=self._sim_data) SituationSeed.serialize_seeds_to_open_street( open_street_seeds=self._open_street_seeds_for_zone_spinup, open_street_data_msg=open_street_data) def spin_up_for_edit_mode(self): self.create_seeds_during_zone_spin_up() def load(self, zone_data=None): if zone_data is None: return for blacklist_data in zone_data.gameplay_zone_data.situations_data.blacklist_data: sim_id = blacklist_data.sim_id sim_data = self._sim_data.setdefault( sim_id, _SituationManagerSimData(sim_id)) sim_data.load(blacklist_data) def create_seeds_during_zone_spin_up(self): zone = services.current_zone() save_slot_proto = services.get_persistence_service( ).get_save_slot_proto_buff() self._arriving_situation_seed = SituationSeed.deserialize_travel_seed_from_slot( save_slot_proto) zone_proto = services.get_persistence_service().get_zone_proto_buff( zone.id) if zone_proto is not None: self._zone_seeds_for_zone_spinup = SituationSeed.deserialize_seeds_from_zone( zone_proto) open_street_proto = services.get_persistence_service( ).get_open_street_proto_buff(zone.open_street_id) if open_street_proto is not None: self._open_street_seeds_for_zone_spinup = SituationSeed.deserialize_seeds_from_open_street( open_street_proto) def get_arriving_seed_during_zone_spin(self): return self._arriving_situation_seed def get_zone_persisted_seeds_during_zone_spin_up(self): return list(self._zone_seeds_for_zone_spinup) def get_open_street_persisted_seeds_during_zone_spin_up(self): return list(self._open_street_seeds_for_zone_spinup) def create_situations_during_zone_spin_up(self): for seed in self._zone_seeds_for_zone_spinup: self.create_situation_from_seed(seed) for seed in self._open_street_seeds_for_zone_spinup: self.create_situation_from_seed(seed) self._create_system_situations() if self._arriving_situation_seed is not None: arrived_id = self.create_situation_from_seed( self._arriving_situation_seed) situation = self.get(arrived_id) if situation is not None: situation.on_arrived() def on_all_situations_created_during_zone_spin_up(self): self._bouncer.request_all_sims_during_zone_spin_up() def on_all_sims_spawned_during_zone_spin_up(self): self._bouncer.assign_all_sims_during_zone_spin_up() for situation in self.running_situations(): if situation.should_time_jump(): situation.on_time_jump() def on_hit_their_marks_during_zone_spin_up(self): self._bouncer.start_full_operations() def make_situation_seed_zone_director_requests(self): venue_service = services.current_zone().venue_service for seed in itertools.chain((self._arriving_situation_seed, ), self._zone_seeds_for_zone_spinup, self._open_street_seeds_for_zone_spinup): if seed is None: continue (zone_director, request_type) = seed.situation_type.get_zone_director_request() if not zone_director is None: if request_type is None: continue if seed.is_loadable and not seed.situation_type.should_seed_be_loaded( seed): continue preserve_state = seed.is_loadable venue_service.request_zone_director( zone_director, request_type, preserve_state=preserve_state) def get_sim_serialization_option(self, sim): result = sims.sim_info_types.SimSerializationOption.UNDECLARED for situation in self.get_situations_sim_is_in(sim): option = situation.situation_serialization_option if option == situations.situation_types.SituationSerializationOption.LOT: result = sims.sim_info_types.SimSerializationOption.LOT break elif option == situations.situation_types.SituationSerializationOption.OPEN_STREETS: result = sims.sim_info_types.SimSerializationOption.OPEN_STREETS return result def remove_sim_from_situation(self, sim, situation_id): situation = self.get(situation_id) if situation is None: return self._bouncer.remove_sim_from_situation(sim, situation) def on_sim_reset(self, sim): for situation in self.running_situations(): if situation.is_sim_in_situation(sim): situation.on_sim_reset(sim) def on_begin_sim_creation_notification(self, sim): sim_data = self._sim_data.setdefault(sim.id, _SituationManagerSimData(sim.id)) sim_data.set_created_time(services.time_service().sim_now) self._prune_sim_data() self._sim_being_created = sim def on_end_sim_creation_notification(self, sim): if sim.id in self._debug_sims: self._debug_sims.discard(sim.id) if self._perf_test_cheat_enabled: self.create_visit_situation_for_unexpected(sim) else: services.current_zone().venue_service.active_venue.summon_npcs( (sim.sim_info, ), NPCSummoningPurpose.DEFAULT) self._bouncer._on_end_sim_creation_notification(sim) self._sim_being_created = None def get_situations_sim_is_in(self, sim): return [ situation for situation in self.values() if situation.is_sim_in_situation(sim) if situation._stage == SituationStage.RUNNING ] def get_situations_sim_is_in_by_tag(self, sim, tag): return [ situation for situation in self.get_situations_sim_is_in(sim) if tag in situation.tags ] def is_user_facing_situation_running(self, global_user_facing_only=False): for situation in self.values(): if situation.is_user_facing: if not global_user_facing_only: return True if situation.linked_sim_id == GLOBAL_SITUATION_LINKED_SIM_ID: return True return False def get_user_facing_situations_gen(self): for situation in self.values(): if situation.is_user_facing: yield situation def running_situations(self): return [ obj for obj in self._objects.values() if obj._stage == SituationStage.RUNNING ] def is_situation_with_tags_running(self, tags): for situation in self.values(): if situation._stage == SituationStage.RUNNING: if situation.tags & tags: return True return False def user_ask_sim_to_leave_now_must_run(self, sim): if not sim.sim_info.is_npc or sim.sim_info.lives_here: return ask_to_leave = True for situation in self.get_situations_sim_is_in(sim): if not situation.on_ask_sim_to_leave(sim): ask_to_leave = False break if ask_to_leave: self.make_sim_leave_now_must_run(sim) def make_sim_leave_now_must_run(self, sim): if services.current_zone().is_zone_shutting_down: return for situation in self.get_situations_sim_is_in(sim): if type(situation) is self.DEFAULT_LEAVE_NOW_MUST_RUN_SITUATION: return leave_now_type = self.DEFAULT_LEAVE_NOW_MUST_RUN_SITUATION guest_list = situations.situation_guest_list.SituationGuestList( invite_only=True) guest_info = situations.situation_guest_list.SituationGuestInfo( sim.id, leave_now_type.default_job(), RequestSpawningOption.CANNOT_SPAWN, BouncerRequestPriority.EVENT_VIP, expectation_preference=True) guest_list.add_guest_info(guest_info) self.create_situation(leave_now_type, guest_list=guest_list, user_facing=False) def is_sim_ss3_safe(self, sim): for situation in self.get_situations_sim_is_in(sim): if not situation.should_send_on_lot_home_in_super_speed_3: return False return True def ss3_make_all_npcs_leave_now(self): sim_info_manager = services.sim_info_manager() current_zone_id = services.current_zone_id() for sim in sim_info_manager.instanced_sims_gen(): if not sim.is_npc: continue if sim.is_on_active_lot() and not self.is_sim_ss3_safe(sim): continue if sim.sim_info.vacation_or_home_zone_id == current_zone_id: continue sim.add_buff( buff_type=self.SUPER_SPEED_THREE_REQUEST_BUFF.buff_type, buff_reason=self.SUPER_SPEED_THREE_REQUEST_BUFF.buff_reason) self.make_sim_leave_now_must_run(sim) def make_sim_leave(self, sim): leave_situation = self.get(self._leave_situation_id) if leave_situation is None: logger.error( 'The leave situation is missing. Making the sim leave now must run.' ) self.make_sim_leave_now_must_run(sim) return leave_situation.invite_sim_to_leave(sim) def expedite_leaving(self): leave_situation = self.get(self._leave_situation_id) if leave_situation is None: return for sim in leave_situation.all_sims_in_situation_gen(): self.make_sim_leave_now_must_run(sim) def get_time_span_sim_has_been_on_lot(self, sim): sim_data = self._sim_data.get(sim.id) if sim_data is None: return if sim_data.created_time is None: return return services.time_service().sim_now - sim_data.created_time def get_blacklist_info(self, sim_id): sim_data = self._sim_data.get(sim_id) if sim_data is None: return return sim_data.get_blacklist_info() def get_auto_fill_blacklist(self, sim_job=None): blacklist = set() for (sim_id, sim_data) in tuple(self._sim_data.items()): if sim_data.is_blacklisted(sim_job=sim_job): blacklist.add(sim_id) return blacklist def add_sim_to_auto_fill_blacklist(self, sim_id, sim_job=None, blacklist_all_jobs_time=None): sim_data = self._sim_data.setdefault(sim_id, _SituationManagerSimData(sim_id)) sim_data.blacklist(sim_job, blacklist_all_jobs_time=blacklist_all_jobs_time) self._prune_sim_data() def remove_sim_from_auto_fill_blacklist(self, sim_id, sim_job=None): sim_data = self._sim_data.get(sim_id) if sim_data is not None: sim_data.whitelist(sim_job=sim_job) self._prune_sim_data() def send_situation_start_ui(self, actor, target=None, situations_available=None, creation_time=None): msg = Situations_pb2.SituationPrepare() msg.situation_session_id = self.get_new_situation_creation_session() msg.creation_time = creation_time if creation_time is not None else 0 msg.sim_id = actor.id if target is not None: msg.is_targeted = True msg.target_id = target.id if situations_available is not None: for situation in situations_available: msg.situation_resource_id.append(situation.guid64) shared_messages.add_message_if_selectable( actor, Consts_pb2.MSG_SITUATION_PREPARE, msg, True) def _prune_sim_data(self): to_remove_ids = [] for (sim_id, sim_data) in self._sim_data.items(): sim_info = services.sim_info_manager().get(sim_id) if not sim_info is None: if not sim_info.is_instanced( allow_hidden_flags=ALL_HIDDEN_REASONS): if sim_data.is_blacklisted == False: to_remove_ids.append(sim_id) if sim_data.is_blacklisted == False: to_remove_ids.append(sim_id) for sim_id in to_remove_ids: del self._sim_data[sim_id] def _issue_callback(self, situation_id, callback_option, data): self._callbacks[situation_id][callback_option](situation_id, callback_option, data) def _send_create_situation_telemetry(self, situation_type, situation_id, guest_list, hire_cost, zone_id): if hasattr(situation_type, 'guid64'): with telemetry_helper.begin_hook( writer, TELEMETRY_HOOK_CREATE_SITUATION) as hook: hook.write_int('situ', situation_id) hook.write_int('host', guest_list.host_sim_id) hook.write_guid('type', situation_type.guid64) hook.write_bool('invi', guest_list.invite_only) hook.write_bool('hire', hire_cost) hook.write_bool( 'nzon', zone_id != 0 and services.current_zone().id != zone_id) sim_info_manager = services.sim_info_manager() if sim_info_manager is not None: for guest_infos in guest_list._job_type_to_guest_infos.values( ): for guest_info in guest_infos: if guest_info.sim_id == 0: continue guest_sim = sim_info_manager.get(guest_info.sim_id) if guest_sim is None: continue client = services.client_manager( ).get_client_by_household_id(guest_sim.household_id) with telemetry_helper.begin_hook( writer, TELEMETRY_HOOK_GUEST) as hook: hook.write_int('situ', situation_id) hook.write_guid('type', situation_type.guid64) if client is None: hook.write_int('npcg', guest_info.sim_id) else: hook.write_int('pcgu', guest_info.sim_id) hook.write_guid('jobb', guest_info.job_type.guid64)
class StrangePreWelcomeWagon(SituationComplexCommon): INSTANCE_TUNABLES = { 'has_front_door_situation_starting_state': HasFrontDoorStrangeSituationStartingState.TunableFactory( description= '\n The first state of this situation in the case that the lot\n has a front door. If it does not then the Has No Front Door\n Situation Starting State will be started instead.\n ', tuning_group=GroupNames.STATE), 'has_no_front_door_situation_starting_state': HasNoFrontDoorStrangeSituationStartingState.TunableFactory( description= '\n The first state of this situation in the case that the lot has\n no front door. Sims should be routing to the arrival spawn\n point.\n ', tuning_group=GroupNames.STATE), '_door_knocker_situation_job': TunableReference( description= '\n The job for the situation door knocker. This sim will end up\n being the host for the situation.\n ', manager=services.situation_job_manager()), '_fruitcake_bearer_situation_job': TunableReference( description= '\n The job for the bearing of the vile nastiness known as...\n \n \n ...fruitcake...\n ', manager=services.situation_job_manager()), '_other_infected_job': TunableReference( description= '\n The job for all of the other infected in the situation.\n ', manager=services.situation_job_manager()), '_extra_infected': TunableRange( description= '\n The number of additional infected Sims to bring.\n ', tunable_type=int, default=1, minimum=1), '_fruitcake_recipe': TunableReference( description= '\n A recipe for the revolting food product commonly known as...\n \n \n ...fruitcake...\n ', manager=services.get_instance_manager( sims4.resources.Types.RECIPE)), '_welcome_wagon_situation': TunableReference( description= '\n The actual welcome wagon situation that we want to start once\n we have actually gotten the Sims to where we want them to be.\n ', manager=services.get_instance_manager( sims4.resources.Types.SITUATION)), '_possession_source': TunableBuffReference( description= "\n Possession buff that keeps the Sims possessed even after the\n situation's end.\n " ) } REMOVE_INSTANCE_TUNABLES = Situation.NON_USER_FACING_REMOVE_INSTANCE_TUNABLES @classproperty def sets_welcome_wagon_flag(cls): return True @classmethod def _states(cls): return (SituationStateData( 1, HasFrontDoorStrangeSituationStartingState, factory=cls.has_front_door_situation_starting_state), SituationStateData( 2, HasNoFrontDoorStrangeSituationStartingState, factory=cls.has_no_front_door_situation_starting_state)) @classmethod def default_job(cls): pass @classmethod def _get_tuned_job_and_default_role_state_tuples(cls): return list(cls.has_front_door_situation_starting_state._tuned_values. job_and_role_changes.items()) @classmethod def get_predefined_guest_list(cls): active_sim_info = services.active_sim_info() door_knocker_results = services.sim_filter_service( ).submit_matching_filter( sim_filter=cls._door_knocker_situation_job.filter, callback=None, requesting_sim_info=active_sim_info, allow_yielding=False, gsi_source_fn=cls.get_sim_filter_gsi_name) door_knocker = door_knocker_results[0] guest_list = SituationGuestList( invite_only=True, host_sim_id=door_knocker.sim_info.sim_id, filter_requesting_sim_id=active_sim_info.sim_id) guest_list.add_guest_info( SituationGuestInfo(door_knocker.sim_info.sim_id, cls._door_knocker_situation_job, RequestSpawningOption.DONT_CARE, BouncerRequestPriority.EVENT_VIP, expectation_preference=True)) blacklist = set() blacklist.add(door_knocker.sim_info.sim_id) fruitcake_bearer_results = services.sim_filter_service( ).submit_matching_filter( sim_filter=cls._fruitcake_bearer_situation_job.filter, callback=None, requesting_sim_info=active_sim_info, allow_yielding=False, blacklist_sim_ids=blacklist, gsi_source_fn=cls.get_sim_filter_gsi_name) fruitcake_bearer = fruitcake_bearer_results[0] guest_list.add_guest_info( SituationGuestInfo(fruitcake_bearer.sim_info.sim_id, cls._fruitcake_bearer_situation_job, RequestSpawningOption.DONT_CARE, BouncerRequestPriority.EVENT_VIP, expectation_preference=True)) blacklist.add(fruitcake_bearer.sim_info.sim_id) guaranteed_infected_results = services.sim_filter_service( ).submit_matching_filter(sim_filter=cls._other_infected_job.filter, callback=None, requesting_sim_info=active_sim_info, allow_yielding=False, blacklist_sim_ids=blacklist, gsi_source_fn=cls.get_sim_filter_gsi_name) guaranteed_infected = guaranteed_infected_results[0] guest_list.add_guest_info( SituationGuestInfo(guaranteed_infected.sim_info.sim_id, cls._other_infected_job, RequestSpawningOption.DONT_CARE, BouncerRequestPriority.EVENT_VIP, expectation_preference=True)) other_infected = services.sim_filter_service().submit_filter( sim_filter=cls._other_infected_job.filter, callback=None, requesting_sim_info=active_sim_info, allow_yielding=False, blacklist_sim_ids=blacklist, gsi_source_fn=cls.get_sim_filter_gsi_name) if not other_infected: return guest_list if len(other_infected) > cls._extra_infected - 1: infected_to_come = random.sample(other_infected, cls._extra_infected - 1) else: infected_to_come = other_infected for infected in infected_to_come: guest_list.add_guest_info( SituationGuestInfo(infected.sim_info.sim_id, cls._other_infected_job, RequestSpawningOption.DONT_CARE, BouncerRequestPriority.EVENT_VIP, expectation_preference=True)) return guest_list def __init__(self, *arg, **kwargs): super().__init__(*arg, **kwargs) reader = self._seed.custom_init_params_reader if reader is None: self._fruitcake_id = None else: self._fruitcake_id = reader.read_uint64(FRUITCAKE_TOKEN, None) def _save_custom_situation(self, writer): super()._save_custom_situation(writer) if self._fruitcake_id is not None: writer.write_uint64(FRUITCAKE_TOKEN, self._fruitcake_id) @property def _bearer_recipes(self): return (self._fruitcake_recipe, ) def start_situation(self): super().start_situation() if services.get_door_service().has_front_door(): self._change_state(self.has_front_door_situation_starting_state()) else: self._change_state( self.has_no_front_door_situation_starting_state()) def create_welcome_wagon(self): situation_manager = services.get_zone_situation_manager() if not situation_manager.is_user_facing_situation_running(): situation_manager.create_situation( self._welcome_wagon_situation, guest_list=self._guest_list.clone(), user_facing=True, scoring_enabled=False) active_household = services.active_household() active_household.needs_welcome_wagon = False self._self_destruct()
class CardBattleBehavior(HasTunableFactory, AutoFactoryInit): FACTORY_TUNABLES = { 'collectable_type': TunableEnumEntry( description= '\n Id for the card battle collection where the collectible items\n will be read when a new card needs to be created.\n ', tunable_type=CollectionIdentifier, default=CollectionIdentifier.Unindentified, invalid_enums=(CollectionIdentifier.Unindentified, )), 'card_slot_type': TunableReference( description= '\n Slot type where player card should appear.\n ', manager=services.get_instance_manager( sims4.resources.Types.SLOT_TYPE)), 'practice_card': TunableReference( description= '\n Object reference to use as the default definition as the opponent\n card. This is to have the same dummy as the opponent when game is\n only played by one player.\n ', manager=services.definition_manager()), 'challenger_buff': TunableBuffReference( description= '\n The buff to apply to the Sim that started the game. This is used\n to be able to guarantee we maintain the challenger Sim consistent\n since the setup mixers and turns can be run by other Sims\n depending on route time and other aspects.\n ' ), 'card_information': TunableTuple( description= '\n Challenger and defender information that will be used to identify\n specific behavior of the cards depending on their placement.\n ', challenge_state_value=ObjectStateValue.TunableReference( description= '\n The state value cards will have when they are selected for \n a game challenge.\n ' ), default_state_value=ObjectStateValue.TunableReference( description= '\n Default state value of cards after a challenge is done.\n ' ), level_state=ObjectState.TunableReference( description= '\n Level states defining the state values that the card has\n representing its experience level.\n ' ), challenger_prop_override=Tunable( description= '\n Prop override name for the card placed on the challenger slot.\n Name for prop should match prop name on swing. \n ', tunable_type=str, default=''), defender_prop_override=Tunable( description= '\n Prop override name for the card placed on the defender slot.\n Name for prop should match prop name on swing.\n ', tunable_type=str, default='')), 'card_scoring': TunableTuple( description= '\n Scoring tunables to apply to a card when the game ends.\n ', level_statistic=Statistic.TunableReference( description= '\n This statistic is used as the level statistic value to be\n increased when the card has won a game.\n ' ), game_won_statistic_increase=TunableRange( description= '\n Statistic value to increase if the game is won.\n Final score increase is affected by the state to stat\n multiplier.\n ', tunable_type=int, default=1, minimum=0), game_lost_statistic_increase=TunableRange( description= '\n Statistic value to increase if the game is lost.\n Final score increase is affected by the state to stat\n multiplier.\n ', tunable_type=int, default=1, minimum=0), state_to_stat_multiplier=TunableMapping( description= "\n Mapping of card state value to stat multiplier when a game is \n finished.\n This value will be multiplied by the \n game_won_statistic_increase or game_lost_statistic_increase\n depending if it's a win or a loss.\n e.g. If card has LEVEL_TWO state value, experience per win is \n game_won_statistic_increase * multiplier corresponding to the\n LEVEL_TWO state value.\n ", key_type=ObjectStateValue.TunableReference( description= '\n State value the card should have to apply this multiplier\n to the statistic increase.\n ' ), value_type=TunableRange( description= '\n Multiplier that affects the game won statistic increase \n on the card.\n ', tunable_type=float, default=1, minimum=0))), 'placement_state_buff': TunableList( description= '\n List of states and buffs to be applied to the Sim when a card\n with active state value.\n ', tunable=TunableTuple( description= '\n Tuple of state and buff that will be added to the Sim when\n a card with that specific state value is played.\n ', state_value=ObjectStateValue.TunableReference( description= '\n Object state value card needs to have to add the buff\n into the Sim.\n ', pack_safe=True), buff=TunableBuffReference( description= '\n The buff to apply to the Sim when a card with this state\n is played.\n ', pack_safe=True))), 'card_tag': TunableEnumWithFilter( description= '\n Tag to look for when iterating through objects to know if they\n are of the card type.\n ', tunable_type=tag.Tag, filter_prefixes=['object', 'func'], default=tag.Tag.INVALID, invalid_enums=(tag.Tag.INVALID, )) } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._players_cards = {} self.challenger_definition = None self.defender_definition = None self._arena_obj = None def on_player_added(self, sim, target): self._arena_obj = target.part_owner candidate_cards = [] player_card = None sim_inventory = sim.inventory_component from_inventory = True player_slot = self._get_slot_for_sim_position(target, sim.position) slotted_objects = player_slot.children if slotted_objects: player_card = player_slot.children[0] if sim.is_npc: from_inventory = False else: for obj in sim_inventory: if obj.definition.has_build_buy_tag(self.card_tag): if obj.state_value_active( self.card_information.challenge_state_value): player_card = obj player_card.set_state( self.card_information.default_state_value.state, self.card_information.default_state_value) break candidate_cards.append(obj) if player_card is None: if candidate_cards: player_card = random.choice(candidate_cards) else: from_inventory = False card_options = ObjectCollectionData.get_collection_data( self.collectable_type).object_list if not card_options: logger.error('Collection {} is an invalid id', self.collectable_type) return card_definition = random.choice( card_options).collectable_item player_card = create_object(card_definition) card_level_state_value = random.choice( self.card_information.level_state.values) player_card.set_state(card_level_state_value.state, card_level_state_value) player_card.persistence_group = PersistenceGroups.NONE if player_card is None: logger.error( 'Failed to create card for player {} for card candidates {}', sim, candidate_cards) card_definition = player_card.get_game_animation_definition() if card_definition is None: logger.error( 'Card {} has no game animation definition tuned and will not be displayed on the card battle object', player_card) return if self.challenger_definition is None: self.challenger_definition = card_definition sim.add_buff_from_op(buff_type=self.challenger_buff.buff_type, buff_reason=self.challenger_buff.buff_reason) else: self.defender_definition = card_definition self._create_card_on_slot(player_card, player_slot) self._apply_card_placement_bonus(sim, player_card) reservation_handler = player_card.get_reservation_handler(sim) reservation_handler.begin_reservation() self._players_cards[sim] = (player_card, from_inventory, reservation_handler) def on_setup_game(self, game_object): pass def on_game_ended(self, winning_team, game_object): for sim in list(self._players_cards): if winning_team is not None: if sim in winning_team.players: self._update_card_scoring( sim, self.card_scoring.game_won_statistic_increase) else: self._update_card_scoring( sim, self.card_scoring.game_lost_statistic_increase) self.on_player_removed(sim, from_game_ended=True) self.challenger_definition = None self.defender_definition = None self._arena_obj = None def _update_card_scoring(self, sim, win_loss_score): (card, from_inventory, _) = self._players_cards[sim] if card is None: logger.error( 'Game ended but Sim {} was removed earlier, this will cause cards to not be updated', sim) return if not from_inventory: return level_state_value = card.get_state(self.card_information.level_state) if level_state_value is None: logger.error( "Card {} doesn't support the state {} used for card scoring", card, self.card_information.level_state) return score_multiplier = self.card_scoring.state_to_stat_multiplier.get( level_state_value) if score_multiplier is None: logger.error( 'Card scoring tuning error, state value {} is not tuned inside the multiplier range of the game', level_state_value) return level_statistic = card.get_stat_instance( self.card_scoring.level_statistic, add=True) if level_statistic is not None: level_statistic.tracker.add_value( self.card_scoring.level_statistic, win_loss_score * score_multiplier) def _apply_card_placement_bonus(self, sim, card): for placement_modifier in self.placement_state_buff: if card.state_value_active(placement_modifier.state_value): sim.add_buff_from_op( buff_type=placement_modifier.buff.buff_type, buff_reason=placement_modifier.buff.buff_reason) def on_player_removed(self, sim, from_game_ended=False): if sim not in self._players_cards: return if not from_game_ended: self._update_card_scoring( sim, self.card_scoring.game_lost_statistic_increase) (card, from_inventory, reservation_handler) = self._players_cards[sim] reservation_handler.end_reservation() if from_inventory: sim.inventory_component.player_try_add_object(card) else: card.set_parent(None) card.destroy(source=self, cause='GameComponent: Placeholder game card removed.') del self._players_cards[sim] if sim.has_buff(self.challenger_buff.buff_type): sim.remove_buff_by_type(self.challenger_buff.buff_type) def _create_card_on_slot(self, card, slot): if slot is not None and slot.empty: slot.add_child(card) def _get_slot_for_sim_position(self, target, sim_position): max_magnitude = None closest_slot = None for runtime_slot in target.part_owner.get_runtime_slots_gen( slot_types={self.card_slot_type}): difference_vector = runtime_slot.position - sim_position difference_magnitude = difference_vector.magnitude() if not max_magnitude is None: if difference_magnitude < max_magnitude: closest_slot = runtime_slot max_magnitude = difference_magnitude closest_slot = runtime_slot max_magnitude = difference_magnitude return closest_slot def additional_anim_overrides_gen(self): prop_overrides = {} if self.challenger_definition is not None: self._set_prop_override( prop_overrides, self.card_information.challenger_prop_override, self.challenger_definition) if self.defender_definition is None: self._set_prop_override( prop_overrides, self.card_information.defender_prop_override, self.practice_card) if self.defender_definition is not None: self._set_prop_override( prop_overrides, self.card_information.defender_prop_override, self.defender_definition) if self.challenger_definition is None: self._set_prop_override( prop_overrides, self.card_information.challenger_prop_override, self.practice_card) yield AnimationOverrides(props=prop_overrides) def _set_prop_override(self, prop_overrides, override_name, card_definition): prop_overrides[override_name] = sims4.collections.FrozenAttributeDict({ 'states_to_override': (), 'from_actor': None, 'definition': card_definition, 'sharing': None, 'set_as_actor': None })
class GameChallengeLiability(HasTunableFactory, AutoFactoryInit, SharedLiability): LIABILITY_TOKEN = 'GameChallengeLiability' FACTORY_TUNABLES = { 'challenge_buff': TunableBuffReference( description= '\n The buff assigned to challenging Sims for the duration of the\n challenge.\n ' ), 'forfeit_buff': OptionalTunable( description= '\n If enabled, specify a buff awarded to Sims that forfeit the\n challenge.\n ', tunable=TunableBuffReference( description= '\n The buff to award to Sims that forfeit the challenge.\n ' )) } def __init__(self, interaction, *args, game=None, **kwargs): super().__init__(*args, **kwargs) self._interaction = interaction self._game = game @property def _sim(self): return self._interaction.sim def _get_linked_sims(self): return {liability._sim for liability in self._shared_liability_refs} def create_new_liability(self, interaction): liability = super().create_new_liability( interaction, interaction, game=self._game, challenge_buff=self.challenge_buff, forfeit_buff=self.forfeit_buff) self._game = None return liability def on_game_started(self, game): self._sim.add_buff_from_op(self.challenge_buff.buff_type, buff_reason=self.challenge_buff.buff_reason) self._game = game linked_sims = self._get_linked_sims() if len(linked_sims) <= 1: self._interaction.cancel( FinishingType.NATURAL, cancel_reason_msg='Challenge ended due to Sims forfeiting') return for sim in linked_sims: game.add_challenger(sim) def release(self, *args, **kwargs): if self._game is not None: self._game.remove_challenger(self._sim) self._sim.remove_buff_by_type(self.challenge_buff.buff_type) if not self._game.game_has_ended: if self.forfeit_buff is not None: self._sim.add_buff_from_op( self.forfeit_buff.buff_type, buff_reason=self.forfeit_buff.buff_reason) if len(self._game.challenge_sims) <= 1: for liability in self._shared_liability_refs: liability._interaction.cancel( FinishingType.NATURAL, cancel_reason_msg= 'Challenge ended due to Sims forfeiting') return super().release(*args, **kwargs) def shared_release(self): pass
class LifeSkillStatistic(HasTunableReference, LifeSkillDisplayMixin, TunedContinuousStatistic, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager( sims4.resources.Types.STATISTIC)): REMOVE_INSTANCE_TUNABLES = ('initial_value', ) INSTANCE_TUNABLES = { 'min_value_tuning': Tunable(description= '\n The minimum value for this stat.\n ', tunable_type=float, default=-100, export_modes=ExportModes.All), 'max_value_tuning': Tunable(description= '\n The maximum value for this stat.\n ', tunable_type=float, default=100, export_modes=ExportModes.All), 'initial_tuning': TunableLiteralOrRandomValue( description= '\n The initial value of this stat. Can be a single value or range.\n ', tunable_type=float, default=0, minimum=-100), 'initial_test_based_modifiers': TunableList( description= '\n List of tuples containing test and a random value. If the test passes,\n a random value is added to the already random initial value. \n ', tunable=TunableTuple( description= '\n A container for test and the corresponding random value.\n ', initial_value_test=TunableTestSet( description= '\n If test passes, then the random value tuned will be applied\n to the initial value. \n ' ), initial_modified_value=TunableLiteralOrRandomValue( description= '\n The initial value of this stat. Can be a single value or range.\n ', tunable_type=float, default=0, minimum=-100))), 'age_to_remove_stat': TunableEnumEntry( description= '\n When sim reaches this age, this stat will be removed permanently. \n ', tunable_type=Age, default=Age.YOUNGADULT), 'missing_career_decay_rate': Tunable( description= '\n How much this life skill decay by if sim is late for school/work.\n ', tunable_type=float, default=0.0), 'trait_on_age_up_list': TunableList( description= '\n A list of trait that will be applied on age up if this commodity \n falls within the range specified in this tuple.\n It also contains other visual information like VFX and notification.\n ', tunable=TunableTuple( description= '\n A container for the range and corresponding information.\n ', export_class_name='TunableTraitOnAgeUpTuple', life_skill_range=TunableInterval( description= '\n If the commodity is in this range on age up, the trait\n will be applied. \n The vfx and notification will be played every time the \n range is crossed.\n ', tunable_type=float, default_lower=0, default_upper=100, export_modes=ExportModes.All), age_up_info=OptionalTunable( description= "\n If enabled, this trait will be added on age up given the specified age. \n Otherwise, no trait will be added.\n We don't use loot because UI needs this trait exported for display.\n ", enabled_name='enabled_age_up_info', tunable=TunableTuple( export_class_name='TunableAgeUpInfoTuple', age_to_apply_trait=TunableEnumEntry( description= '\n When sim reaches this age, this trait will be added on age up.\n ', tunable_type=Age, default=Age.YOUNGADULT), life_skill_trait=Trait.TunableReference( description= '\n Trait that is added on age up.\n ', pack_safe=True)), export_modes=ExportModes.All), in_range_notification= OptionalTunable(tunable=TunableUiDialogNotificationSnippet( description= '\n Notification that is sent when the commodity reaches this range.\n ' )), out_of_range_notification= OptionalTunable(tunable=TunableUiDialogNotificationSnippet( description= '\n Notification that is sent when the commodity exits this range.\n ' )), vfx_triggered=TunablePlayEffectVariant( description= '\n Vfx to play on the sim when commodity enters this threshold.\n ', tuning_group=GroupNames.ANIMATION), in_range_buff=OptionalTunable(tunable=TunableBuffReference( description= '\n Buff that is added when sim enters this threshold.\n ' )))), 'headline': TunableReference( description= '\n The headline that we want to send down when this life skill updates.\n ', manager=services.get_instance_manager( sims4.resources.Types.HEADLINE), tuning_group=GroupNames.UI) } def __init__(self, tracker): self._vfx = None super().__init__(tracker, self.get_initial_value()) self._last_update_value = None if not tracker.load_in_progress: self._apply_initial_value_modifier() @classproperty def persists_across_gallery_for_state(cls): if cls.gallery_load_behavior == GalleryLoadBehavior.LOAD_FOR_ALL or cls.gallery_load_behavior == GalleryLoadBehavior.LOAD_ONLY_FOR_OBJECT: return True return False @classmethod def get_initial_value(cls): return cls.initial_tuning.random_int() def _apply_initial_value_modifier(self): initial_value = self._value resolver = SingleSimResolver(self.tracker.owner) for initial_modifier in self.initial_test_based_modifiers: if initial_modifier.initial_value_test.run_tests(resolver): initial_value += initial_modifier.initial_modified_value.random_float( ) self.set_value(initial_value, from_add=True) def _update_value(self): old_value = self._value super()._update_value() new_value = self._value self._evaluate_threshold(old_value=old_value, new_value=new_value) def _evaluate_threshold(self, old_value=0, new_value=0, from_load=False): old_infos = [] new_infos = [] for range_info in self.trait_on_age_up_list: if old_value in range_info.life_skill_range: old_infos.append(range_info) if new_value in range_info.life_skill_range: new_infos.append(range_info) old_infos_set = set(old_infos) new_infos_set = set(new_infos) out_ranges = old_infos_set - new_infos_set in_ranges = new_infos_set - old_infos_set owner = self.tracker.owner is_household_sim = owner.is_selectable and owner.valid_for_distribution if not from_load: for out_range in out_ranges: if out_range.out_of_range_notification is not None and is_household_sim: dialog = out_range.out_of_range_notification( owner, resolver=SingleSimResolver(owner)) dialog.show_dialog(additional_tokens=(owner, )) if out_range.in_range_buff is not None: owner.Buffs.remove_buff_by_type( out_range.in_range_buff.buff_type) for in_range in in_ranges: if in_range.in_range_notification is not None and not from_load and is_household_sim: dialog = in_range.in_range_notification( owner, resolver=SingleSimResolver(owner)) dialog.show_dialog(additional_tokens=(owner, )) if in_range.vfx_triggered is not None and not from_load and is_household_sim: if self._vfx is not None: self._vfx.stop(immediate=True) self._vfx = None sim = owner.get_sim_instance( allow_hidden_flags=ALL_HIDDEN_REASONS) if sim is not None: self._vfx = in_range.vfx_triggered(sim) self._vfx.start() if in_range.in_range_buff is not None: owner.Buffs.add_buff( in_range.in_range_buff.buff_type, buff_reason=in_range.in_range_buff.buff_reason) def _on_statistic_modifier_changed(self, notify_watcher=True): super()._on_statistic_modifier_changed(notify_watcher=notify_watcher) self.create_and_send_commodity_update_msg(is_rate_change=False) @constproperty def remove_on_convergence(): return False def set_value(self, value, *args, from_load=False, interaction=None, **kwargs): old_value = self._value super().set_value(value, *args, from_load=from_load, interaction=interaction, **kwargs) new_value = self._value self._evaluate_threshold(old_value=old_value, new_value=new_value, from_load=from_load) if from_load: return self.create_and_send_commodity_update_msg(is_rate_change=False, from_add=kwargs.get( 'from_add', False)) def on_remove(self, on_destroy=False): super().on_remove(on_destroy=on_destroy) if self._vfx is not None: self._vfx.stop(immediate=True) self._vfx = None def save_statistic(self, commodities, skills, ranked_statistics, tracker): message = protocols.Commodity() message.name_hash = self.guid64 message.value = self.get_saved_value() if self._time_of_last_value_change: message.time_of_last_value_change = self._time_of_last_value_change.absolute_ticks( ) commodities.append(message) def create_and_send_commodity_update_msg(self, is_rate_change=True, allow_npc=False, from_add=False): current_value = self.get_value() change_rate = self.get_change_rate() life_skill_msg = Commodities_pb2.LifeSkillUpdate() life_skill_msg.sim_id = self.tracker.owner.id life_skill_msg.life_skill_id = self.guid64 life_skill_msg.curr_value = current_value life_skill_msg.rate_of_change = change_rate life_skill_msg.is_from_add = from_add send_sim_life_skill_update_message(self.tracker.owner, life_skill_msg) if self._last_update_value is None: value_to_send = change_rate else: value_to_send = current_value - self._last_update_value self._last_update_value = current_value if value_to_send != 0 and not from_add: self.headline.send_headline_message(self.tracker.owner, value_to_send) def create_and_send_life_skill_delete_msg(self): life_skill_msg = Commodities_pb2.LifeSkillDelete() life_skill_msg.sim_id = self.tracker.owner.id life_skill_msg.life_skill_id = self.guid64 send_sim_life_skill_delete_message(self.tracker.owner, life_skill_msg)
class ClubGatheringSituation(SituationComplexCommon): INSTANCE_TUNABLES = {'_default_job': SituationJob.TunableReference(description='\n The default job for all members of this situation.\n '), '_default_role_state': RoleState.TunableReference(description='\n The Role State for Sims in the default job of this situation.\n '), '_default_gathering_vibe': TunableEnumEntry(description='\n The default Club vibe to use for the gathering.\n ', tunable_type=ClubGatheringVibe, default=ClubGatheringVibe.NO_VIBE), '_vibe_buffs': TunableMapping(description=" \n A Mapping of ClubGatheringVibe to List of buffs.\n \n When setting the vibe for the gathering the type is found in the\n mapping and then each buff is processed in order until one of them\n can be added. Then evaluation stops.\n \n Example: The club vibe is getting set to ClubGatheringVibe.Angry.\n That entry has 3 buffs associated with it in the mapping. Angry3,\n Angry2, Angry1 in that order. Angry3 doesn't pass evaluation so it\n is passed. Next Angry2 does pass evaluation and so we add Angry2\n Vibe Buff to the gathering. Angry1 is never evaluated in this\n situation. Angry1 is only ever evaluated if Angry3 and Angry2 both\n fail.\n ", key_type=ClubGatheringVibe, value_type=TunableList(description='\n A List of buff to attempt to use on the gathering. Order is\n important as we do not try to give any buffs after one is given\n to the gathering.\n ', tunable=Buff.TunableReference(), minlength=1)), '_gathering_buff_reason': TunableLocalizedString(description='\n The reason the gathering buff was added. Displayed on the buff\n tooltip.\n '), '_initial_disband_timer': TunableSimMinute(description='\n The number of Sim minutes after a Gathering is created before it\n can disband due to lack of members.\n ', default=30, minimum=1), '_initial_notification': TunableUiDialogNotificationSnippet(description='\n A notification that shows up once the gathering starts.\n '), '_minimum_number_of_sims': TunableRange(description='\n The minimum number of Sims that must be present in a Gathering to\n keep it from disbanding.\n ', tunable_type=int, default=3, minimum=2), 'time_between_bucks_rewards': TunableSimMinute(description='\n The time in Sim Minutes to wait before awarding\n the first club bucks for being in a gathering.\n ', default=10), 'reward_bucks_per_interval': Tunable(description='\n The amount of Club Bucks to award to the associated club at each \n tuned interval.\n ', tunable_type=int, default=1), 'rule_breaking_buff': TunableBuffReference(description='\n Award this buff whenever a Sim breaks the rules.\n ')} REMOVE_INSTANCE_TUNABLES = Situation.NON_USER_FACING_REMOVE_INSTANCE_TUNABLES def __init__(self, seed): super().__init__(seed) self.associated_club = None self._current_gathering_buff = None self._current_gathering_vibe = None self._sim_gathering_time_checks = {} self._can_disband = False self._initial_disband_timer_handle = None self._rewards_timer = None self._time_tracker_timer = None self._validity_household_id_override = None reader = self._seed.custom_init_params_reader if reader is not None: start_source = reader.read_uint64(ClubGatheringKeys.START_SOURCE, None) disband_ticks = reader.read_uint64(ClubGatheringKeys.DISBAND_TICKS, 0) self._validity_household_id_override = reader.read_uint64(ClubGatheringKeys.HOUSEHOLD_ID_OVERRIDE, None) associated_club_id = reader.read_uint64(ClubGatheringKeys.ASSOCIATED_CLUB_ID, None) if associated_club_id is not None: club_service = services.get_club_service() associated_club = club_service.get_club_by_id(associated_club_id) self.initialize_gathering(associated_club, disband_ticks=disband_ticks, start_source=start_source) current_gathering_buff_guid = reader.read_uint64(ClubGatheringKeys.GATHERING_BUFF, 0) self._current_gathering_buff = services.get_instance_manager(sims4.resources.Types.BUFF).get(current_gathering_buff_guid) vibe = reader.read_uint64(ClubGatheringKeys.GATHERING_VIBE, self._default_gathering_vibe) self.set_club_vibe(vibe) @classmethod def default_job(cls): return cls._default_job @classmethod def _get_tuned_job_and_default_role_state_tuples(cls): return [(cls.default_job(), cls._default_role_state)] @classmethod def _states(cls): return (SituationStateData(1, ClubGatheringSituationState),) def _destroy(self): if self._initial_disband_timer_handle is not None: self._initial_disband_timer_handle.cancel() self._initial_disband_timer_handle = None if self._rewards_timer is not None: self._rewards_timer.cancel() self._rewards_timer = None if self._time_tracker_timer is not None: self._time_tracker_timer.cancel() self._time_tracker_timer = None super()._destroy() def _disband_timer_callback(self, _): self._can_disband = True if self._initial_disband_timer_handle is not None: alarms.cancel_alarm(self._initial_disband_timer_handle) self._initial_disband_timer_handle = None self._disband_if_neccessary() def _disband_if_neccessary(self): if not self._can_disband: return if len(list(self.all_sims_in_situation_gen())) < self._minimum_number_of_sims: self._self_destruct() def on_remove(self): self._can_disband = False super().on_remove() self._cleanup_gathering() def _cleanup_gathering(self): club_service = services.get_club_service() if club_service is None: logger.error("Attempting to end a Gathering but the ClubService doesn't exist.") return op = EndClubGathering(self.associated_club.club_id) Distributor.instance().add_op_with_no_owner(op) club_service.on_gathering_ended(self) def start_situation(self): super().start_situation() self._change_state(ClubGatheringSituationState()) def load_situation(self): result = super().load_situation() if result and (self.associated_club is None or not (not self.is_validity_overridden() and not self.associated_club.is_zone_valid_for_gathering(services.current_zone_id()))): self._cleanup_gathering() return False return result def is_validity_overridden(self): return self._validity_household_id_override == services.active_household_id() and services.active_household_lot_id() == services.active_lot_id() def initialize_gathering(self, associated_club, disband_ticks=None, start_source=None): club_service = services.get_club_service() if club_service is None: logger.error("Attempting to start a Gathering but the ClubService doesn't exist.") return self.associated_club = associated_club if start_source is not None: if start_source == ClubGatheringStartSource.APPLY_FOR_INVITE: invited_sim = services.sim_info_manager().get(self._guest_list.host_sim_id) self.associated_club.show_club_notification(invited_sim, ClubTunables.CLUB_GATHERING_START_DIALOG) elif any(sim_info.is_selectable for sim_info in self._guest_list.invited_sim_infos_gen()): initial_notification = self._initial_notification(services.active_sim_info()) initial_notification.show_dialog(icon_override=IconInfoData(icon_resource=associated_club.icon), additional_tokens=(associated_club.name,)) self._initial_disband_timer_handle = alarms.add_alarm(self.associated_club, interval_in_sim_minutes(self._initial_disband_timer), self._disband_timer_callback) elif disband_ticks > 0: self._initial_disband_timer_handle = alarms.add_alarm(self.associated_club, clock.TimeSpan(disband_ticks), self._disband_timer_callback) time_between_rewards = create_time_span(minutes=self.time_between_bucks_rewards) self._rewards_timer = alarms.add_alarm(self, time_between_rewards, self._award_club_bucks, True) time_between_gathering_checks = create_time_span(minutes=club_tuning.ClubTunables.MINUTES_BETWEEN_CLUB_GATHERING_PULSES) self._time_tracker_timer = alarms.add_alarm(self, time_between_gathering_checks, self._add_time_in_gathering, True) op = StartClubGathering(self.associated_club.club_id) Distributor.instance().add_op_with_no_owner(op) club_service.on_gathering_started(self) def _on_minimum_number_of_members_reached(self): self._can_disband = True if self._initial_disband_timer_handle is not None: alarms.cancel_alarm(self._initial_disband_timer_handle) self._initial_disband_timer_handle = None def _on_add_sim_to_situation(self, sim, *args, **kwargs): super()._on_add_sim_to_situation(sim, *args, **kwargs) club_service = services.get_club_service() if club_service is None: logger.error("Attempting to add a Sim to a Gathering but the ClubService doesn't exist.") return club_service.on_sim_added_to_gathering(sim, self) self.add_club_vibe_buff_to_sim(sim) relationship_tracker = sim.relationship_tracker relationship_tracker.add_create_relationship_listener(self._relationship_added_callback) sim.sim_info.register_for_outfit_changed_callback(self._on_outfit_changed) if not self._can_disband and len(list(self.all_sims_in_situation_gen())) >= self._minimum_number_of_sims: self._on_minimum_number_of_members_reached() op = UpdateClubGathering(GatheringUpdateType.ADD_MEMBER, self.associated_club.club_id, sim.id) Distributor.instance().add_op_with_no_owner(op) if self.associated_club.member_should_spin_into_club_outfit(sim): self._push_spin_into_current_outfit_interaction(sim) self._sim_gathering_time_checks[sim] = services.time_service().sim_timeline.now def _on_remove_sim_from_situation(self, sim): super()._on_remove_sim_from_situation(sim) sim.remove_buff_by_type(self._current_gathering_buff) self._disband_if_neccessary() club_service = services.get_club_service() if club_service is None: logger.error("Attempting to add a Sim to a Gathering but the ClubService doesn't exist.") return club_service.on_sim_removed_from_gathering(sim, self) relationship_tracker = sim.relationship_tracker relationship_tracker.remove_create_relationship_listener(self._relationship_added_callback) sim.sim_info.unregister_for_outfit_changed_callback(self._on_outfit_changed) if self.associated_club.member_should_spin_into_club_outfit(sim): sim.sim_info.register_for_outfit_changed_callback(self._on_outfit_removed) self._push_spin_into_current_outfit_interaction(sim) else: self._remove_apprearance_modifiers(sim.sim_info) if self.associated_club in club_service.clubs_to_gatherings_map: op = UpdateClubGathering(GatheringUpdateType.REMOVE_MEMBER, self.associated_club.club_id, sim.id) Distributor.instance().add_op_with_no_owner(op) if sim in self._sim_gathering_time_checks: self._process_time_in_gathering_event(sim) del self._sim_gathering_time_checks[sim] def set_club_vibe(self, vibe): self._current_gathering_vibe = vibe vibe_buffs = self._vibe_buffs.get(vibe, ()) member = self.associated_club.members[0] for buff in vibe_buffs: if buff.can_add(member): if buff is not self._current_gathering_buff: for sim in self.all_sims_in_situation_gen(): sim.remove_buff_by_type(self._current_gathering_buff) self.add_club_vibe_buff_to_sim(sim, buff) self._current_gathering_buff = buff return def add_club_vibe_buff_to_sim(self, sim, buff=None): buff = self._current_gathering_buff if buff is None else buff if buff is None: return if sim.has_buff(buff): return sim.add_buff(buff, self._gathering_buff_reason) def _relationship_added_callback(self, relationship): resolver = DoubleSimResolver(relationship.find_sim_info_a(), relationship.find_sim_info_b(), additional_participants={ParticipantType.AssociatedClub: (self.associated_club,)}) for (perk, benefit) in ClubTunables.NEW_RELATIONSHIP_MODS.items(): if self.associated_club.bucks_tracker.is_perk_unlocked(perk): if not benefit.test_set.run_tests(resolver=resolver): continue benefit.loot.apply_to_resolver(resolver=resolver) def _award_club_bucks(self, handle): qualified_sims = [sim for sim in self._situation_sims if self._sim_satisfies_requirement_for_bucks(sim)] if not qualified_sims: return if any(sim for sim in self._situation_sims if club_tuning.ClubTunables.CLUB_BUCKS_REWARDS_MULTIPLIER.trait in sim.sim_info.trait_tracker.equipped_traits): multiplier = club_tuning.ClubTunables.CLUB_BUCKS_REWARDS_MULTIPLIER.multiplier else: multiplier = 1 bucks_tracker = self.associated_club.bucks_tracker if bucks_tracker is None: return bucks_tracker.try_modify_bucks(ClubTunables.CLUB_BUCKS_TYPE, int(self.reward_bucks_per_interval*multiplier), reason='Time in club gathering') def _sim_satisfies_requirement_for_bucks(self, sim): if not sim.is_selectable: return False elif not sim.sim_info.is_instanced(): return False return True def _on_outfit_changed(self, sim_info, outfit_category_and_index): club = self.associated_club (cas_parts_add, cas_parts_remove) = club.get_club_outfit_parts(sim_info, outfit_category_and_index) appearance_tracker = sim_info.appearance_tracker appearance_tracker.remove_appearance_modifiers(self.guid, source=self) modifiers = [] for cas_part in cas_parts_add: modifier = AppearanceModifier.SetCASPart(cas_part=cas_part, should_toggle=False, replace_with_random=False, update_genetics=False, _is_combinable_with_same_type=True, remove_conflicting=False, outfit_type_compatibility=None) modifiers.append(modifier) for cas_part in cas_parts_remove: modifier = AppearanceModifier.SetCASPart(cas_part=cas_part, should_toggle=True, replace_with_random=False, update_genetics=False, _is_combinable_with_same_type=True, remove_conflicting=False, outfit_type_compatibility=None) modifiers.append(modifier) for modifier in modifiers: appearance_tracker.add_appearance_modifier(modifier, self.guid, 1, False, source=self) appearance_tracker.evaluate_appearance_modifiers() if sim_info.appearance_tracker.appearance_override_sim_info is not None: sim = sim_info.get_sim_instance() if sim is not None: sim.apply_outfit_buffs_for_sim_info(sim_info.appearance_tracker.appearance_override_sim_info, outfit_category_and_index) def _on_outfit_removed(self, sim_info, outfit_category_and_index): self._remove_apprearance_modifiers(sim_info) def _remove_apprearance_modifiers(self, sim_info): sim_info.appearance_tracker.remove_appearance_modifiers(self.guid, source=self) sim_info.unregister_for_outfit_changed_callback(self._on_outfit_removed) def _push_spin_into_current_outfit_interaction(self, sim): sim.sim_info.set_outfit_dirty(sim.get_current_outfit()[0]) change_outfit_context = InteractionContext(sim, InteractionContext.SOURCE_SCRIPT, priority.Priority.High) return sim.push_super_affordance(ForceChangeToCurrentOutfit, None, change_outfit_context) def remove_all_club_outfits(self): for sim in self.all_sims_in_situation_gen(): self._push_spin_into_current_outfit_interaction(sim) def _add_time_in_gathering(self, handle): qualified_sims = [sim for sim in self._situation_sims if self._sim_satisfies_requirement_for_bucks(sim)] if not qualified_sims: return now = services.time_service().sim_timeline.now for sim in qualified_sims: self._process_time_in_gathering_event(sim, now) self._sim_gathering_time_checks[sim] = now def _process_time_in_gathering_event(self, sim, now=None): if now is None: now = services.time_service().sim_timeline.now elapsed_time = now - self._sim_gathering_time_checks[sim] services.get_event_manager().process_event(test_events.TestEvent.TimeInClubGathering, sim_info=sim.sim_info, amount=int(elapsed_time.in_minutes())) def _save_custom_situation(self, writer): super()._save_custom_situation(writer) writer.write_uint64(ClubGatheringKeys.ASSOCIATED_CLUB_ID, self.associated_club.club_id) if self._initial_disband_timer_handle is not None: current_time = services.time_service().sim_now disband_ticks = max((self._initial_disband_timer_handle.finishing_time - current_time).in_ticks(), 0) else: disband_ticks = 0 writer.write_uint64(ClubGatheringKeys.DISBAND_TICKS, disband_ticks) if self._current_gathering_buff is not None: writer.write_uint64(ClubGatheringKeys.GATHERING_BUFF, self._current_gathering_buff.guid64) writer.write_uint64(ClubGatheringKeys.GATHERING_VIBE, self._current_gathering_vibe) if self._validity_household_id_override is not None: writer.write_uint64(ClubGatheringKeys.HOUSEHOLD_ID_OVERRIDE, self._validity_household_id_override) def _issue_requests(self): super()._issue_requests() request = AssociatedClubRequestFactory(self, callback_data=_RequestUserData(), job_type=self._default_job, request_priority=BouncerRequestPriority.EVENT_DEFAULT_JOB, user_facing=False, exclusivity=self.exclusivity) self.manager.bouncer.submit_request(request)
class SituationManager(DistributableObjectManager): __qualname__ = 'SituationManager' DEFAULT_LEAVE_SITUATION = sims4.tuning.tunable.TunableReference( description= '\n The situation type for the background leave situation.\n It collects sims who are not in other situations and\n asks them to leave periodically.\n ', manager=services.get_instance_manager(sims4.resources.Types.SITUATION), class_restrictions=situations.complex.leave_situation.LeaveSituation) DEFAULT_LEAVE_NOW_MUST_RUN_SITUATION = sims4.tuning.tunable.TunableReference( description= '\n The situation type that drives the sim off the lot pronto.\n ', manager=services.get_instance_manager(sims4.resources.Types.SITUATION), class_restrictions=situations.complex.single_sim_leave_situation. SingleSimLeaveSituation) DEFAULT_VISIT_SITUATION = sims4.tuning.tunable.TunableReference( description= '\n The default visit situation used when you ask someone to \n hang out or invite them in.\n ', manager=services.get_instance_manager(sims4.resources.Types.SITUATION)) DEFAULT_TRAVEL_SITUATION = Situation.TunableReference( description= ' \n The default situation for when you \n are simply traveling with a group \n of Sims.\n ' ) NPC_SOFT_CAP = sims4.tuning.tunable.Tunable( description= '\n The base value for calculating the soft cap on the maximum \n number of NPCs instantiated.\n \n The actual value of the NPC soft cap will be\n this tuning value minus the number of sims in the active household.\n \n There is no hard cap because certain types of NPCs must always\n spawn or the game will be broken. The prime example of a \n game breaker is the Grim Reaper.\n \n If the number of NPCs is:\n \n 1) At or above the soft cap only game breaker NPCs will be spawned.\n \n 2) Above the soft cap then low priority NPCs will be driven from the lot.\n \n 3) Equal to the soft cap and there are pending requests for higher priority\n NPCs, then lower priority NPCs will be driven from the lot.\n \n ', tunable_type=int, default=20, tuning_filter=sims4.tuning.tunable_base.FilterTag.EXPERT_MODE) LEAVE_INTERACTION_TAGS = TunableSet( description= '\n The tags indicating leave lot interactions, but not \n leave lot must run interactions.\n These are used to determine if a leave lot interaction is running\n or cancel one if it is.\n ', tunable=TunableEnumWithFilter(tunable_type=tag.Tag, default=tag.Tag.INVALID, tuning_filter=FilterTag.EXPERT_MODE, filter_prefixes=tag.INTERACTION_PREFIX)) SUPER_SPEED_THREE_REQUEST_BUFF = TunableBuffReference( description= "\n The buff to apply to the Sim when we're trying to make them run the\n leave situation from a super speed three request.\n ", deferred=True) _npc_soft_cap_override = None _perf_test_cheat_enabled = False def __init__(self, manager_id=0): super().__init__(manager_id=manager_id) self._get_next_session_id = UniqueIdGenerator(1) self._added_to_distributor = set() self._callbacks = {} self._departing_situation_seed = None self._arriving_situation_seed = None self._zone_seeds_for_zone_spinup = [] self._open_street_seeds_for_zone_spinup = [] self._debug_sims = set() self._leave_situation_id = 0 self._player_greeted_situation_id = 0 self._player_waiting_to_be_greeted_situation_id = 0 self._sim_being_created = None self._sim_data = {} self._delay_situation_destruction_ref_count = 0 self._situations_for_delayed_destruction = set() self._bouncer = None self._zone_spin_up_greeted_complete = False self._pre_bouncer_update = [] def start(self): self._bouncer = Bouncer() def destroy_situations_on_teardown(self): self.destroy_all_situations(include_system=True) self._sim_data.clear() self._bouncer.destroy() self._bouncer = None def reset(self, create_system_situations=True): self.destroy_all_situations(include_system=True) self._added_to_distributor.clear() self._callbacks.clear() self._bouncer.reset() if create_system_situations: self._create_system_situations() def add_pre_bouncer_update(self, situation): self._pre_bouncer_update.append(situation) def update(self): if self._bouncer is not None: try: situations = tuple(self._pre_bouncer_update) if situations: self._pre_bouncer_update = [] for situation in situations: situation.on_pre_bouncer_update() self._bouncer._update() except Exception: logger.exception('Exception while updating the Bouncer.') @property def npc_soft_cap(self): cap = self.NPC_SOFT_CAP if self._npc_soft_cap_override is None else self._npc_soft_cap_override if services.active_household() is None: return 0 return cap - services.active_household().size_of_household def set_npc_soft_cap_override(self, override): self._npc_soft_cap_override = override def enable_perf_cheat(self, enable=True): self._perf_test_cheat_enabled = enable self._bouncer.spawning_freeze(enable) self._bouncer.cap_cheat(enable) def get_all(self): return [ obj for obj in self._objects.values() if obj._stage == SituationStage.RUNNING ] def get_new_situation_creation_session(self): return self._get_next_session_id() @property def bouncer(self): return self._bouncer @property def sim_being_created(self): return self._sim_being_created def add_debug_sim_id(self, sim_id): self._debug_sims.add(sim_id) def _determine_player_greeted_status_during_zone_spin_up(self): if not services.current_zone( ).venue_service.venue.requires_visitation_rights: return GreetedStatus.NOT_APPLICABLE active_household = services.active_household() if active_household is None: return GreetedStatus.NOT_APPLICABLE if active_household.home_zone_id == services.current_zone().id: return GreetedStatus.NOT_APPLICABLE cur_status = GreetedStatus.WAITING_TO_BE_GREETED lot_seeds = list(self._zone_seeds_for_zone_spinup) if self._arriving_situation_seed is not None: lot_seeds.append(self._arriving_situation_seed) for seed in lot_seeds: status = seed.get_player_greeted_status() logger.debug('Player:{} :{}', status, seed.situation_type, owner='sscholl') while status == GreetedStatus.GREETED: cur_status = status break return cur_status def get_npc_greeted_status_during_zone_fixup(self, sim_info): if not services.current_zone( ).venue_service.venue.requires_visitation_rights: return GreetedStatus.NOT_APPLICABLE if sim_info.lives_here: return GreetedStatus.NOT_APPLICABLE cur_status = GreetedStatus.NOT_APPLICABLE for seed in self._zone_seeds_for_zone_spinup: status = seed.get_npc_greeted_status(sim_info) logger.debug('NPC:{} :{} :{}', sim_info, status, seed.situation_type, owner='sscholl') if status == GreetedStatus.GREETED: cur_status = status break while status == GreetedStatus.WAITING_TO_BE_GREETED: cur_status = status return cur_status def is_player_greeted(self): return self._player_greeted_situation_id != 0 def is_player_waiting_to_be_greeted(self): return self._player_waiting_to_be_greeted_situation_id != 0 and self._player_greeted_situation_id == 0 @property def is_zone_spin_up_greeted_complete(self): return self._zone_spin_up_greeted_complete def create_situation(self, situation_type, guest_list=None, user_facing=True, duration_override=None, custom_init_writer=None, zone_id=0, scoring_enabled=True, spawn_sims_during_zone_spin_up=False): if guest_list is None: guest_list = SituationGuestList() hire_cost = guest_list.get_hire_cost() reserved_funds = None if guest_list.host_sim is not None: reserved_funds = guest_list.host_sim.family_funds.try_remove( situation_type.cost() + hire_cost, Consts_pb2.TELEMETRY_EVENT_COST, guest_list.host_sim) if reserved_funds is None: return reserved_funds.apply() situation_id = id_generator.generate_object_id() self._send_create_situation_telemetry(situation_type, situation_id, guest_list, hire_cost, zone_id) if zone_id != 0 and services.current_zone().id != zone_id: return self._create_departing_seed_and_travel( situation_type, situation_id, guest_list, user_facing, duration_override, custom_init_writer, zone_id, scoring_enabled=scoring_enabled) situation_seed = SituationSeed( situation_type, SeedPurpose.NORMAL, situation_id, guest_list, user_facing=user_facing, duration_override=duration_override, scoring_enabled=scoring_enabled, spawn_sims_during_zone_spin_up=spawn_sims_during_zone_spin_up) if custom_init_writer is not None: situation_seed.setup_for_custom_init_params(custom_init_writer) return self.create_situation_from_seed(situation_seed) def create_visit_situation_for_unexpected(self, sim): duration_override = None if self._perf_test_cheat_enabled: duration_override = 0 self.create_visit_situation(sim, duration_override=duration_override) def create_visit_situation(self, sim, duration_override=None, visit_type_override=None): situation_id = None visit_type = visit_type_override if visit_type_override is not None else self.DEFAULT_VISIT_SITUATION if visit_type is not None: guest_list = situations.situation_guest_list.SituationGuestList( invite_only=True) guest_info = situations.situation_guest_list.SituationGuestInfo.construct_from_purpose( sim.id, visit_type.default_job(), situations. situation_guest_list.SituationInvitationPurpose.INVITED) guest_list.add_guest_info(guest_info) situation_id = self.create_situation( visit_type, guest_list=guest_list, user_facing=False, duration_override=duration_override) if situation_id is None: logger.error('Failed to create visit situation for sim: {}', sim) self.make_sim_leave(sim) return situation_id def create_situation_from_seed(self, seed): if not seed.allow_creation: return if seed.user_facing: situation = self.get_user_facing_situation() if situation is not None: self.destroy_situation_by_id(situation.id) if seed.situation_type.is_unique_situation: for situation in self.running_situations(): while type(situation) is seed.situation_type: return situation = seed.situation_type(seed) try: if seed.is_loadable: situation._destroy() return else: situation.start_situation() except Exception: logger.exception('Exception thrown while starting situation') situation._destroy() return self.add(situation) if situation.is_user_facing or situation.distribution_override: distributor.system.Distributor.instance().add_object(situation) self._added_to_distributor.add(situation) situation.on_added_to_distributor() return situation.id def _create_departing_seed_and_travel(self, situation_type, situation_id, guest_list=None, user_facing=True, duration_override=None, custom_init_writer=None, zone_id=0, scoring_enabled=True): traveling_sim = guest_list.get_traveler() if traveling_sim is None: logger.error( 'No traveling sim available for creating departing seed for situation: {}.', situation_type) return if traveling_sim.client is None: logger.error( 'No client on traveling sim: {} for for situation: {}.', traveling_sim, situation_type) return if traveling_sim.household is None: logger.error( 'No household on traveling sim for for situation: {}.', situation_type) return situation_seed = SituationSeed(situation_type, SeedPurpose.TRAVEL, situation_id, guest_list, user_facing, duration_override, zone_id, scoring_enabled=scoring_enabled) if situation_seed is None: logger.error('Failed to create departing seed.for situation: {}.', situation_type) return if custom_init_writer is not None: situation_seed.setup_for_custom_init_params(custom_init_writer) self._departing_situation_seed = situation_seed travel_info = protocolbuffers.InteractionOps_pb2.TravelSimsToZone() travel_info.zone_id = zone_id travel_info.sim_ids.append(traveling_sim.id) traveling_sim_ids = guest_list.get_other_travelers(traveling_sim) travel_info.sim_ids.extend(traveling_sim_ids) distributor.system.Distributor.instance().add_event( protocolbuffers.Consts_pb2.MSG_TRAVEL_SIMS_TO_ZONE, travel_info) services.game_clock_service().request_pause('Situation Travel') logger.debug('Travel seed creation time {}', services.time_service().sim_now) logger.debug('Travel seed future time {}', services.time_service().sim_future) return situation_id def _create_system_situations(self): self._leave_situation_id = 0 for situation in self.running_situations(): while type(situation) is self.DEFAULT_LEAVE_SITUATION: self._leave_situation_id = situation.id break if self._leave_situation_id == 0: self._leave_situation_id = self.create_situation( self.DEFAULT_LEAVE_SITUATION, user_facing=False, duration_override=0) @property def auto_manage_distributor(self): return False def call_on_remove(self, situation): super().call_on_remove(situation) self._callbacks.pop(situation.id, None) if situation in self._added_to_distributor: dist = distributor.system.Distributor.instance() dist.remove_object(situation) self._added_to_distributor.remove(situation) situation.on_removed_from_distributor() def is_distributed(self, situation): return situation in self._added_to_distributor def _request_destruction(self, situation): if self._delay_situation_destruction_ref_count == 0: return True self._situations_for_delayed_destruction.add(situation) return False def destroy_situation_by_id(self, situation_id): if situation_id in self: if situation_id == self._leave_situation_id: self._leave_situation_id = 0 if situation_id == self._player_greeted_situation_id: self._player_greeted_situation_id = 0 if situation_id == self._player_waiting_to_be_greeted_situation_id: self._player_waiting_to_be_greeted_situation_id = 0 self.remove_id(situation_id) def destroy_all_situations(self, include_system=False): all_situations = tuple(self.values()) for situation in all_situations: if include_system == False and situation.id == self._leave_situation_id: pass try: self.destroy_situation_by_id(situation.id) except Exception: logger.error( 'Error when destroying situation {}. You are probably screwed.', situation) def register_for_callback(self, situation_id, situation_callback_option, callback_fn): registrant = _CallbackRegistration(situation_callback_option, callback_fn) registrant_list = self._callbacks.setdefault(situation_id, []) registrant_list.append(registrant) def create_greeted_npc_visiting_npc_situation(self, npc_sim_info): services.current_zone().venue_service.venue.summon_npcs( (npc_sim_info, ), venues.venue_constants.NPCSummoningPurpose.PLAYER_BECOMES_GREETED) def create_greeted_player_visiting_npc_situation(self, sim=None): if sim is None: guest_list = situations.situation_guest_list.SituationGuestList() else: guest_list = situations.situation_guest_list.SituationGuestList( host_sim_id=sim.id) greeted_situation_type = services.current_zone( ).venue_service.venue.player_greeted_situation_type if greeted_situation_type is None: return self._player_greeted_situation_id = self.create_situation( greeted_situation_type, user_facing=False, guest_list=guest_list) def create_player_waiting_to_be_greeted_situation(self): self._player_waiting_to_be_greeted_situation_id = self.create_situation( services.current_zone( ).venue_service.venue.player_ungreeted_situation_type, user_facing=False) def _handle_player_greeting_situations_during_zone_spin_up(self): if self._zone_spin_up_player_greeted_status == GreetedStatus.NOT_APPLICABLE: return if self._zone_spin_up_player_greeted_status == GreetedStatus.GREETED: greeted_situation_type = services.current_zone( ).venue_service.venue.player_greeted_situation_type for situation in self.running_situations(): while type(situation) is greeted_situation_type: break self.create_greeted_player_visiting_npc_situation() return waiting_situation_type = services.current_zone( ).venue_service.venue.player_ungreeted_situation_type for situation in self.running_situations(): while type(situation) is waiting_situation_type: break self.create_player_waiting_to_be_greeted_situation() def handle_npcs_during_zone_fixup(self): if services.game_clock_service( ).time_has_passed_in_world_since_zone_save() or services.current_zone( ).active_household_changed_between_save_and_load(): sim_infos_to_fix_up = [] for sim_info in services.sim_info_manager( ).get_sim_infos_saved_in_zone(): while sim_info.is_npc and not sim_info.lives_here and sim_info.get_sim_instance( ) is not None: sim_infos_to_fix_up.append(sim_info) if sim_infos_to_fix_up: logger.debug('Fixing up {} npcs during zone fixup', len(sim_infos_to_fix_up), owner='sscholl') services.current_zone().venue_service.venue.zone_fixup( sim_infos_to_fix_up) def make_waiting_player_greeted(self, door_bell_ringing_sim=None): for situation in self.running_situations(): situation._on_make_waiting_player_greeted(door_bell_ringing_sim) if self._player_greeted_situation_id == 0: self.create_greeted_player_visiting_npc_situation( door_bell_ringing_sim) def save(self, zone_data=None, open_street_data=None, save_slot_data=None, **kwargs): if zone_data is None: return SituationSeed.serialize_travel_seed_to_slot( save_slot_data, self._departing_situation_seed) zone_seeds = [] street_seeds = [] for situation in self.running_situations(): seed = situation.save_situation() while seed is not None: if situation.situation_serialization_option == SituationSerializationOption.OPEN_STREETS: street_seeds.append(seed) else: zone_seeds.append(seed) SituationSeed.serialize_seeds_to_zone(zone_seeds, zone_data) SituationSeed.serialize_seeds_to_open_street(street_seeds, open_street_data) def on_pre_spawning_sims(self): zone = services.current_zone() save_slot_proto = services.get_persistence_service( ).get_save_slot_proto_buff() seed = SituationSeed.deserialize_travel_seed_from_slot(save_slot_proto) if seed is not None: if zone.id != seed.zone_id: logger.debug( 'Travel situation :{} not loaded. Expected zone :{} but is on zone:{}', seed.situation_type, seed.zone_id, services.current_zone().id) seed.allow_creation = False else: time_since_travel_seed_created = services.time_service( ).sim_now - seed.create_time if time_since_travel_seed_created > date_and_time.TimeSpan.ZERO: logger.debug( 'Not loading traveled situation :{} because time has passed {}', seed.situation_type, time_since_travel_seed_created) seed.allow_creation = False self._arriving_situation_seed = seed zone_proto = services.get_persistence_service().get_zone_proto_buff( zone.id) if zone_proto is not None: self._zone_seeds_for_zone_spinup = SituationSeed.deserialize_seeds_from_zone( zone_proto) for seed in self._zone_seeds_for_zone_spinup: while not seed.situation_type._should_seed_be_loaded(seed): seed.allow_creation = False open_street_proto = services.get_persistence_service( ).get_open_street_proto_buff(zone.open_street_id) if open_street_proto is not None: self._open_street_seeds_for_zone_spinup = SituationSeed.deserialize_seeds_from_open_street( open_street_proto) for seed in self._open_street_seeds_for_zone_spinup: while not seed.situation_type._should_seed_be_loaded(seed): seed.allow_creation = False self._zone_spin_up_player_greeted_status = self._determine_player_greeted_status_during_zone_spin_up( ) def create_situations_during_zone_spin_up(self): for seed in self._zone_seeds_for_zone_spinup: self.create_situation_from_seed(seed) for seed in self._open_street_seeds_for_zone_spinup: self.create_situation_from_seed(seed) self._create_system_situations() if self._arriving_situation_seed is not None: self.create_situation_from_seed(self._arriving_situation_seed) self._handle_player_greeting_situations_during_zone_spin_up() self.handle_npcs_during_zone_fixup() def on_all_situations_created_during_zone_spin_up(self): self._bouncer.start() def get_sim_serialization_option(self, sim): result = sims.sim_info_types.SimSerializationOption.UNDECLARED for situation in self.get_situations_sim_is_in(sim): option = situation.situation_serialization_option if option == situations.situation_types.SituationSerializationOption.LOT: result = sims.sim_info_types.SimSerializationOption.LOT break else: while option == situations.situation_types.SituationSerializationOption.OPEN_STREETS: result = sims.sim_info_types.SimSerializationOption.OPEN_STREETS return result def remove_sim_from_situation(self, sim, situation_id): situation = self.get(situation_id) if situation is None: return self._bouncer.remove_sim_from_situation(sim, situation) def on_reset(self, sim_ref): pass def on_sim_creation(self, sim): sim_data = self._sim_data.setdefault(sim.id, _SituationManagerSimData(sim.id)) sim_data.set_created_time(services.time_service().sim_now) self._prune_sim_data() self._sim_being_created = sim if sim.id in self._debug_sims: self._debug_sims.discard(sim.id) if self._perf_test_cheat_enabled: self.create_visit_situation_for_unexpected(sim) else: services.current_zone().venue_service.venue.summon_npcs( (sim.sim_info, ), NPCSummoningPurpose.DEFAULT) self._bouncer.on_sim_creation(sim) self._sim_being_created = None def get_situations_sim_is_in(self, sim): return [ situation for situation in self.values() if situation._stage == SituationStage.RUNNING ] def is_user_facing_situation_running(self): for situation in self.values(): while situation.is_user_facing: return True return False def get_user_facing_situation(self): for situation in self.values(): while situation.is_user_facing: return situation def running_situations(self): return [ obj for obj in self._objects.values() if obj._stage == SituationStage.RUNNING ] def is_situation_with_tags_running(self, tags): for situation in self.values(): while situation._stage == SituationStage.RUNNING and situation.tags & tags: return True return False def user_ask_sim_to_leave_now_must_run(self, sim): if not sim.sim_info.is_npc or sim.sim_info.lives_here: return ask_to_leave = True for situation in self.get_situations_sim_is_in(sim): while not situation.on_ask_sim_to_leave(sim): ask_to_leave = False break if ask_to_leave: self.make_sim_leave_now_must_run(sim) def make_sim_leave_now_must_run(self, sim, super_speed_three_request=False): if services.current_zone().is_zone_shutting_down: return for situation in self.get_situations_sim_is_in(sim): while type(situation) is self.DEFAULT_LEAVE_NOW_MUST_RUN_SITUATION: return if super_speed_three_request: sim.add_buff( buff_type=self.SUPER_SPEED_THREE_REQUEST_BUFF.buff_type, buff_reason=self.SUPER_SPEED_THREE_REQUEST_BUFF.buff_reason) leave_now_type = self.DEFAULT_LEAVE_NOW_MUST_RUN_SITUATION guest_list = situations.situation_guest_list.SituationGuestList( invite_only=True) guest_info = situations.situation_guest_list.SituationGuestInfo( sim.id, leave_now_type.default_job(), RequestSpawningOption.CANNOT_SPAWN, BouncerRequestPriority.VIP, expectation_preference=True) guest_list.add_guest_info(guest_info) self.create_situation(leave_now_type, guest_list=guest_list, user_facing=False) def make_sim_leave(self, sim): leave_situation = self.get(self._leave_situation_id) if leave_situation is None: logger.error( 'The leave situation is missing. Making the sim leave now must run.' ) self.make_sim_leave_now_must_run(sim) return leave_situation.invite_sim_to_leave(sim) def expedite_leaving(self): leave_situation = self.get(self._leave_situation_id) if leave_situation is None: return for sim in leave_situation.all_sims_in_situation_gen(): self.make_sim_leave_now_must_run(sim) def get_time_span_sim_has_been_on_lot(self, sim): sim_data = self._sim_data.get(sim.id) if sim_data is None: return if sim_data.created_time is None: return return services.time_service().sim_now - sim_data.created_time def get_remaining_blacklist_time_span(self, sim_id): sim_data = self._sim_data.get(sim_id) if sim_data is None: return date_and_time.TimeSpan.ZERO return sim_data.get_remaining_blacklisted_time_span() def get_auto_fill_blacklist(self): blacklist = set() for (sim_id, sim_data) in tuple(self._sim_data.items()): while sim_data.is_blacklisted: blacklist.add(sim_id) return blacklist def add_sim_to_auto_fill_blacklist(self, sim_id, blacklist_until=None): sim_data = self._sim_data.setdefault(sim_id, _SituationManagerSimData(sim_id)) sim_data.blacklist(blacklist_until=blacklist_until) self._prune_sim_data() def _prune_sim_data(self): to_remove_ids = [] for (sim_id, sim_data) in self._sim_data.items(): while services.object_manager().get( sim_id) is None and sim_data.is_blacklisted == False: to_remove_ids.append(sim_id) for sim_id in to_remove_ids: del self._sim_data[sim_id] def _get_callback_registrants(self, situation_id): return list(self._callbacks.get(situation_id, [])) def _send_create_situation_telemetry(self, situation_type, situation_id, guest_list, hire_cost, zone_id): if hasattr(situation_type, 'guid64'): with telemetry_helper.begin_hook( writer, TELEMETRY_HOOK_CREATE_SITUATION) as hook: hook.write_int('situ', situation_id) hook.write_int('host', guest_list.host_sim_id) hook.write_guid('type', situation_type.guid64) hook.write_bool('invi', guest_list.invite_only) hook.write_bool('hire', hire_cost) hook.write_bool( 'nzon', zone_id != 0 and services.current_zone().id != zone_id) sim_info_manager = services.sim_info_manager() if sim_info_manager is not None: while True: for guest_infos in guest_list._job_type_to_guest_infos.values( ): for guest_info in guest_infos: if guest_info.sim_id == 0: pass guest_sim = sim_info_manager.get(guest_info.sim_id) if guest_sim is None: pass client = services.client_manager( ).get_client_by_household_id( guest_sim.household_id) with telemetry_helper.begin_hook( writer, TELEMETRY_HOOK_GUEST) as hook: hook.write_int('situ', situation_id) if client is None: hook.write_int('npcg', guest_info.sim_id) else: hook.write_int('pcgu', guest_info.sim_id) hook.write_guid('jobb', guest_info.job_type.guid64)
def __init__(self, *args, **kwargs): super().__init__( description= "\n The rules to apply for how autonomy handle on-lot and off-lot\n targets.\n \n DEFAULT:\n Off-lot sims who are outside the lot's tolerance will not autonomously perform\n interactions on the lot. Sims will only autonomously perform off-lot\n interactions within their off-lot radius.\n ON_LOT_ONLY:\n Sims will only consider targets on the active lot.\n OFF_LOT_ONLY:\n Sims will only consider targets that are off the active lot.\n UNLIMITED:\n Sims will consider all objects regardless of on/off lot status.\n FESTIVAL:\n Sims will consider all objects within the festival area.\n ANCHORED:\n Sims will only consider objects within a tuned radius of\n autonomy anchor objects. Anchor objects can be objects that\n match a tag, sims that match a buff, or set by external\n systems.\n ", default_behavior=TunableTuple( description= "\n Off-lot sims who are outside the lot's tolerance will not autonomously perform\n interactions on the lot. Sims will only autonomously perform off-lot\n interactions within their off-lot radius.\n ", locked_args={ 'rule': OffLotAutonomyRules.DEFAULT, 'anchor_tag': None, 'anchor_buff': None }, tolerance=Tunable( description= '\n This is how many meters the Sim can be off of the lot while still being \n considered on the lot for the purposes of autonomy. For example, if \n this is set to 5, the sim can be 5 meters from the edge of the lot and \n still consider all the objects on the lot for autonomy. If the sim were \n to step 6 meters from the lot, the sim would be considered off the lot \n and would only score off-lot objects that are within the off lot radius.\n ', tunable_type=float, default=7.5), radius=TunableRange( description= '\n The radius around the sim in which he will consider off-lot objects. If it is \n 0, the Sim will not consider off-lot objects at all. This is not recommended \n since it will keep them from running any interactions unless they are already \n within the tolerance for that lot (set with Off Lot Tolerance).\n ', tunable_type=float, default=25, minimum=0)), on_lot_only=TunableTuple( description= '\n Sims will only consider targets on the active lot.\n ', locked_args={ 'rule': OffLotAutonomyRules.ON_LOT_ONLY, 'tolerance': 0, 'radius': 0, 'anchor_tag': None, 'anchor_buff': None }), off_lot_only=TunableTuple( description= '\n Sims will only consider targets that are off the active lot. \n ', locked_args={ 'rule': OffLotAutonomyRules.OFF_LOT_ONLY, 'tolerance': 0, 'anchor_tag': None, 'anchor_buff': None }, radius=TunableRange( description= '\n The radius around the sim in which he will consider off-lot objects. If it is \n 0, the Sim will not consider off-lot objects at all. This is not recommended \n since it will keep them from running any interactions unless they are already \n within the tolerance for that lot (set with Off Lot Tolerance).\n ', tunable_type=float, default=1000, minimum=0)), unlimited=TunableTuple( description= '\n Sims will consider all objects regardless of on/off lot\n status.\n ', locked_args={ 'rule': OffLotAutonomyRules.UNLIMITED, 'tolerance': 0, 'radius': 1000, 'anchor_tag': None, 'anchor_buff': None }), restricted=TunableTuple( description= '\n Sims will consider all objects in the restricted open\n street autonomy area. This is defined by points in world\n builder so please make sure that world builder has setup\n the objects before trying to use this option.\n ', locked_args={ 'rule': OffLotAutonomyRules.RESTRICTED, 'tolerance': 0, 'radius': 0, 'anchor_tag': None, 'anchor_buff': None }), anchored=TunableTuple( description= '\n Sims will only consider targets that are off the active lot. \n ', locked_args={ 'rule': OffLotAutonomyRules.ANCHORED, 'tolerance': 0 }, radius=TunableRange( description= '\n The radius around the anchoring point in which the sim will consider objects.\n This point must be set on the autonomy component.\n \n Designers: Please make sure this autonomy modifier is attached to a role or \n other other gameplay system that will correctly set the anchoring point before\n you set this. Or set the anchor tag on this tunable.\n ', tunable_type=float, default=50, minimum=0), anchor_tag=OptionalTunable( description= '\n If enabled, this will set the autonomy anchor to all\n objects that match the tuned tag.\n ', tunable=TunableEnumEntry( description= '\n The tag used to find an object to be an anchor.\n ', tunable_type=tag.Tag, default=tag.Tag.INVALID)), anchor_buff=OptionalTunable( description= '\n If enabled, this will set the autonomy anchor to all\n sims that match the tuned buff.\n ', tunable=TunableBuffReference( description= '\n The buff in question.\n ' ))), default='default_behavior')
class Trait(HasTunableReference, SuperAffordanceProviderMixin, TargetSuperAffordanceProviderMixin, HasTunableLodMixin, MixerActorMixin, MixerProviderMixin, metaclass=HashedTunedInstanceMetaclass, manager=services.trait_manager()): EQUIP_SLOT_NUMBER_MAP = TunableMapping( description= '\n The number of personality traits available to Sims of specific ages.\n ', key_type=TunableEnumEntry( description="\n The Sim's age.\n ", tunable_type=sim_info_types.Age, default=sim_info_types.Age.YOUNGADULT), value_type=Tunable( description= '\n The number of personality traits available to a Sim of the specified\n age.\n ', tunable_type=int, default=3), key_name='Age', value_name='Slot Number') PERSONALITY_TRAIT_TAG = TunableEnumEntry( description= '\n The tag that marks a trait as a personality trait.\n ', tunable_type=tag.Tag, default=tag.Tag.INVALID, invalid_enums=(tag.Tag.INVALID, )) DAY_NIGHT_TRACKING_BUFF_TAG = TunableEnumWithFilter( description= '\n The tag that marks buffs as opting in to Day Night Tracking on traits..\n ', tunable_type=tag.Tag, filter_prefixes=['buff'], default=tag.Tag.INVALID, invalid_enums=(tag.Tag.INVALID, )) INSTANCE_TUNABLES = { 'trait_type': TunableEnumEntry( description='\n The type of the trait.\n ', tunable_type=TraitType, default=TraitType.PERSONALITY, export_modes=ExportModes.All, tuning_group=GroupNames.APPEARANCE), 'display_name': TunableLocalizedStringFactory( description= "\n The trait's display name. This string is provided with the owning\n Sim as its only token.\n ", allow_none=True, export_modes=ExportModes.All, tuning_group=GroupNames.APPEARANCE), 'display_name_gender_neutral': TunableLocalizedString( description= "\n The trait's gender-neutral display name. This string is not provided\n any tokens, and thus can't rely on context to properly form\n masculine and feminine forms.\n ", allow_none=True, tuning_group=GroupNames.APPEARANCE), 'trait_description': TunableLocalizedStringFactory( description="\n The trait's description.\n ", allow_none=True, export_modes=ExportModes.All, tuning_group=GroupNames.APPEARANCE), 'trait_origin_description': TunableLocalizedString( description= "\n A description of how the Sim obtained this trait. Can be overloaded\n for other uses in certain cases:\n - When the trait type is AGENT this string is the name of the \n agency's Trade type and will be provided with the owning sim \n as its token.\n - When the trait type is HIDDEN and the trait is used by the CAS\n STORIES flow, this can be used as a secondary description in \n the CAS Stories UI. If this trait is tagged as a CAREER CAS \n stories trait, this description will be used to explain which \n skills are also granted with this career.\n ", allow_none=True, export_modes=ExportModes.All, tuning_group=GroupNames.APPEARANCE), 'icon': TunableResourceKey( description="\n The trait's icon.\n ", allow_none=True, resource_types=CompoundTypes.IMAGE, export_modes=ExportModes.All, tuning_group=GroupNames.APPEARANCE), 'pie_menu_icon': TunableResourceKey( description= "\n The trait's pie menu icon.\n ", resource_types=CompoundTypes.IMAGE, default=None, allow_none=True, tuning_group=GroupNames.APPEARANCE), 'trait_asm_overrides': TunableTuple( description= '\n Tunables that will specify if a Trait will add any parameters\n to the Sim and how it will affect their boundary conditions.\n ', param_type=OptionalTunable( description= '\n Define if this trait is parameterized as an on/off value or as\n part of an enumeration.\n ', tunable=Tunable( description= '\n The name of the parameter enumeration. For example, if this\n value is tailType, then the tailType actor parameter is set\n to the value specified in param_value, for this Sim.\n ', tunable_type=str, default=None), disabled_name='boolean', enabled_name='enum'), trait_asm_param=Tunable( description= "\n The ASM parameter for this trait. If unset, it will be auto-\n generated depending on the instance name (e.g. 'trait_Clumsy').\n ", tunable_type=str, default=None), consider_for_boundary_conditions=Tunable( description= '\n If enabled the trait_asm_param will be considered when a Sim\n is building the goals and validating against its boundary\n conditions.\n This should ONLY be enabled, if we need this parameter for\n cases like a posture transition, or boundary specific cases. \n On regular cases like an animation outcome, this is not needed.\n i.e. Vampire trait has an isVampire parameter set to True, so\n when animatin out of the coffin it does different get in/out \n animations. When this is enabled, isVampire will be set to \n False for every other Sim.\n ', tunable_type=bool, default=False), tuning_group=GroupNames.ANIMATION), 'ages': TunableSet( description= '\n The allowed ages for this trait. If no ages are specified, then all\n ages are considered valid.\n ', tunable=TunableEnumEntry(tunable_type=Age, default=None, export_modes=ExportModes.All), tuning_group=GroupNames.AVAILABILITY), 'genders': TunableSet( description= '\n The allowed genders for this trait. If no genders are specified,\n then all genders are considered valid.\n ', tunable=TunableEnumEntry(tunable_type=Gender, default=None, export_modes=ExportModes.All), tuning_group=GroupNames.AVAILABILITY), 'species': TunableSet( description= '\n The allowed species for this trait. If not species are specified,\n then all species are considered valid.\n ', tunable=TunableEnumEntry(tunable_type=Species, default=Species.HUMAN, invalid_enums=(Species.INVALID, ), export_modes=ExportModes.All), tuning_group=GroupNames.AVAILABILITY), 'conflicting_traits': TunableList( description= '\n Conflicting traits for this trait. If the Sim has any of the\n specified traits, then they are not allowed to be equipped with this\n one.\n \n e.g.\n Family Oriented conflicts with Hates Children, and vice-versa.\n ', tunable=TunableReference(manager=services.trait_manager(), pack_safe=True), export_modes=ExportModes.All, tuning_group=GroupNames.AVAILABILITY), 'is_npc_only': Tunable( description= '\n If checked, this trait will get removed from Sims that have a home\n when the zone is loaded or whenever they switch to a household that\n has a home zone.\n ', tunable_type=bool, default=False, tuning_group=GroupNames.AVAILABILITY), 'cas_selected_icon': TunableResourceKey( description= '\n Icon to be displayed in CAS when this trait has already been applied\n to a Sim.\n ', resource_types=CompoundTypes.IMAGE, default=None, allow_none=True, export_modes=(ExportModes.ClientBinary, ), tuning_group=GroupNames.CAS), 'cas_idle_asm_key': TunableInteractionAsmResourceKey( description= '\n The ASM to use for the CAS idle.\n ', default=None, allow_none=True, category='asm', export_modes=ExportModes.All, tuning_group=GroupNames.CAS), 'cas_idle_asm_state': Tunable( description= '\n The state to play for the CAS idle.\n ', tunable_type=str, default=None, source_location='cas_idle_asm_key', source_query=SourceQueries.ASMState, export_modes=ExportModes.All, tuning_group=GroupNames.CAS), 'cas_trait_asm_param': Tunable( description= '\n The ASM parameter for this trait for use with CAS ASM state machine,\n driven by selection of this Trait, i.e. when a player selects the a\n romantic trait, the Flirty ASM is given to the state machine to\n play. The name tuned here must match the animation state name\n parameter expected in Swing.\n ', tunable_type=str, default=None, export_modes=ExportModes.All, tuning_group=GroupNames.CAS), 'tags': TunableList( description= "\n The associated categories of the trait. Need to distinguish among\n 'Personality Traits', 'Achievement Traits' and 'Walkstyle\n Traits'.\n ", tunable=TunableEnumEntry(tunable_type=tag.Tag, default=tag.Tag.INVALID), export_modes=ExportModes.All, tuning_group=GroupNames.CAS), 'sim_info_fixup_actions': TunableList( description= '\n A list of fixup actions which will be performed on a sim_info with\n this trait when it is loaded.\n ', tunable=TunableVariant( career_fixup_action=_SimInfoCareerFixupAction.TunableFactory( description= '\n A fix up action to set a career with a specific level.\n ' ), skill_fixup_action=_SimInfoSkillFixupAction.TunableFactory( description= '\n A fix up action to set a skill with a specific level.\n ' ), unlock_fixup_action=_SimInfoUnlockFixupAction.TunableFactory( description= '\n A fix up action to unlock certain things for a Sim\n ' ), perk_fixup_action=_SimInfoPerkFixupAction.TunableFactory( description= '\n A fix up action to grant perks to a Sim. It checks perk required\n unlock tuning and unlocks prerequisite perks first.\n ' ), default='career_fixup_action'), tuning_group=GroupNames.CAS), 'sim_info_fixup_actions_timing': TunableEnumEntry( description= "\n This is DEPRECATED, don't tune this field. We usually don't do trait-based\n fixup unless it's related to CAS stories. We keep this field only for legacy\n support reason.\n \n This is mostly to optimize performance when applying fix-ups to\n a Sim. We ideally would not like to spend time scanning every Sim \n on every load to see if they need fixups. Please be sure you \n consult a GPE whenever you are creating fixup tuning.\n ", tunable_type=SimInfoFixupActionTiming, default=SimInfoFixupActionTiming.ON_FIRST_SIMINFO_LOAD, tuning_group=GroupNames.DEPRECATED, deprecated=True), 'teleport_style_interaction_to_inject': TunableReference( description= '\n When this trait is added to a Sim, if a teleport style interaction\n is specified, any time another interaction runs, we may run this\n teleport style interaction to shorten or replace the route to the \n target.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION), class_restrictions=('TeleportStyleSuperInteraction', ), allow_none=True, tuning_group=GroupNames.SPECIAL_CASES), 'interactions': OptionalTunable( description= '\n Mixer interactions that are available to Sims equipped with this\n trait.\n ', tunable=ContentSet.TunableFactory(locked_args={ 'phase_affordances': frozendict(), 'phase_tuning': None })), 'buffs_add_on_spawn_only': Tunable( description= '\n If unchecked, buffs are added to the Sim as soon as this trait is\n added. If checked, buffs will be added only when the Sim is\n instantiated and removed when the Sim uninstantiates.\n \n General guidelines: If the buffs only matter to Sims, for example\n buffs that alter autonomy behavior or walkstyle, this should be\n checked.\n ', tunable_type=bool, default=True), 'buffs': TunableList( description= '\n Buffs that should be added to the Sim whenever this trait is\n equipped.\n ', tunable=TunableBuffReference(pack_safe=True), unique_entries=True), 'buffs_proximity': TunableList( description= '\n Proximity buffs that are active when this trait is equipped.\n ', tunable=TunableReference(manager=services.buff_manager())), 'buff_replacements': TunableMapping( description= '\n A mapping of buff replacement. If Sim has this trait on, whenever he\n get the buff tuned in the key of the mapping, it will get replaced\n by the value of the mapping.\n ', key_type=TunableReference( description= '\n Buff that will get replaced to apply on Sim by this trait.\n ', manager=services.buff_manager(), reload_dependent=True, pack_safe=True), value_type=TunableTuple( description= '\n Data specific to this buff replacement.\n ', buff_type=TunableReference( description= '\n Buff used to replace the buff tuned as key.\n ', manager=services.buff_manager(), reload_dependent=True, pack_safe=True), buff_reason=OptionalTunable( description= '\n If enabled, override the buff reason.\n ', tunable=TunableLocalizedString( description= '\n The overridden buff reason.\n ' )), buff_replacement_priority=TunableEnumEntry( description= "\n The priority of this buff replacement, relative to other\n replacements. Tune this to be a higher value if you want\n this replacement to take precedence.\n \n e.g.\n (NORMAL) trait_HatesChildren (buff_FirstTrimester -> \n buff_FirstTrimester_HatesChildren)\n (HIGH) trait_Male (buff_FirstTrimester -> \n buff_FirstTrimester_Male)\n \n In this case, both traits have overrides on the pregnancy\n buffs. However, we don't want males impregnated by aliens\n that happen to hate children to lose their alien-specific\n buffs. Therefore we tune the male replacement at a higher\n priority.\n ", tunable_type=TraitBuffReplacementPriority, default=TraitBuffReplacementPriority.NORMAL))), 'excluded_mood_types': TunableList( TunableReference( description= '\n List of moods that are prevented by having this trait.\n ', manager=services.mood_manager())), 'outfit_replacements': TunableMapping( description= "\n A mapping of outfit replacements. If the Sim has this trait, outfit\n change requests are intercepted to produce the tuned result. If\n multiple traits with outfit replacements exist, the behavior is\n undefined.\n \n Tuning 'Invalid' as a key acts as a fallback and applies to all\n reasons.\n \n Tuning 'Invalid' as a value keeps a Sim in their current outfit.\n ", key_type=TunableEnumEntry(tunable_type=OutfitChangeReason, default=OutfitChangeReason.Invalid), value_type=TunableEnumEntry(tunable_type=OutfitChangeReason, default=OutfitChangeReason.Invalid)), 'disable_aging': OptionalTunable( description= '\n If enabled, aging out of specific ages can be disabled.\n ', tunable=TunableTuple( description= '\n The tuning that disables aging out of specific age groups.\n ', allowed_ages=TunableSet( description= '\n A list of ages that the Sim CAN age out of. If an age is in\n this list then the Sim is allowed to age out of it. If an\n age is not in this list than a Sim is not allowed to age out\n of it. For example, if the list only contains Child and\n Teen, then a Child Sim would be able to age up to Teen and\n a Teen Sim would be able to age up to Young Adult. But, a\n Young Adult, Adult, or Elder Sim would not be able to age\n up.\n ', tunable=TunableEnumEntry(Age, default=Age.ADULT)), tooltip=OptionalTunable( description= '\n When enabled, this tooltip will be displayed in the aging\n progress bar when aging is disabled because of the trait.\n ', tunable=TunableLocalizedStringFactory( description= '\n The string that displays in the aging UI when aging up\n is disabled due to the trait.\n ' ))), tuning_group=GroupNames.SPECIAL_CASES), 'can_die': Tunable( description= '\n When set, Sims with this trait are allowed to die. When unset, Sims\n are prevented from dying.\n ', tunable_type=bool, default=True, tuning_group=GroupNames.SPECIAL_CASES), 'culling_behavior': TunableVariant( description= '\n The culling behavior of a Sim with this trait.\n ', default_behavior=CullingBehaviorDefault.TunableFactory(), immune_to_culling=CullingBehaviorImmune.TunableFactory(), importance_as_npc_score=CullingBehaviorImportanceAsNpc. TunableFactory(), default='default_behavior', tuning_group=GroupNames.SPECIAL_CASES), 'always_send_test_event_on_add': Tunable( description= '\n If checked, will send out a test event when added to a trait\n tracker even if the receiving sim is hidden or not instanced.\n ', tunable_type=bool, default=False, tuning_group=GroupNames.SPECIAL_CASES), 'voice_effect': OptionalTunable( description= '\n The voice effect of a Sim with this trait. This is prioritized\n against other traits with voice effects.\n \n The Sim may only have one voice effect at a time.\n ', tunable=VoiceEffectRequest.TunableFactory()), 'plumbbob_override': OptionalTunable( description= '\n If enabled, allows a new plumbbob model to be used when a Sim has\n this occult type.\n ', tunable=PlumbbobOverrideRequest.TunableFactory()), 'vfx_mask': OptionalTunable( description= '\n If enabled when this trait is added the masks will be applied to\n the Sim affecting the visibility of specific VFX.\n Example: TRAIT_CHILDREN will provide a mask MASK_CHILDREN which \n the monster battle object will only display VFX for any Sim \n using that mask.\n ', tunable=TunableEnumFlags( description= "\n Mask that will be added to the Sim's mask when the trait is\n added.\n ", enum_type=VFXMask), enabled_name='apply_vfx_mask', disabled_name='no_vfx_mask'), 'day_night_tracking': OptionalTunable( description= "\n If enabled, allows this trait to track various aspects of day and\n night via buffs on the owning Sim.\n \n For example, if this is enabled and the Sunlight Buff is tuned with\n buffs, the Sim will get the buffs added every time they're in\n sunlight and removed when they're no longer in sunlight.\n ", tunable=DayNightTracking.TunableFactory()), 'persistable': Tunable( description= '\n If checked then this trait will be saved onto the sim. If\n unchecked then the trait will not be saved.\n Example unchecking:\n Traits that are applied for the sim being in the region.\n ', tunable_type=bool, default=True), 'initial_commodities': TunableSet( description= '\n A list of commodities that will be added to a sim on load, if the\n sim has this trait.\n \n If a given commodity is also blacklisted by another trait that the\n sim also has, it will NOT be added.\n \n Example:\n Adult Age Trait adds Hunger.\n Vampire Trait blacklists Hunger.\n Hunger will not be added.\n ', tunable=Commodity.TunableReference(pack_safe=True)), 'initial_commodities_blacklist': TunableSet( description= "\n A list of commodities that will be prevented from being\n added to a sim that has this trait.\n \n This always takes priority over any commodities listed in any\n trait's initial_commodities.\n \n Example:\n Adult Age Trait adds Hunger.\n Vampire Trait blacklists Hunger.\n Hunger will not be added.\n ", tunable=Commodity.TunableReference(pack_safe=True)), 'ui_commodity_sort_override': OptionalTunable( description= '\n Optional list of commodities to override the default UI sort order.\n ', tunable=TunableList( description= '\n The position of the commodity in this list represents the sort order.\n Add all possible combination of traits in the list.\n If we have two traits which have sort override, we will implement\n a priority system to determine which determines which trait sort\n order to use.\n ', tunable=Commodity.TunableReference())), 'ui_category': OptionalTunable( description= '\n If enabled then this trait will be displayed in a specific category\n within the relationship panel if this trait would be displayed\n within that panel.\n ', tunable=TunableEnumEntry( description= '\n The UI trait category that we use to categorize this trait\n within the relationship panel.\n ', tunable_type=TraitUICategory, default=TraitUICategory.PERSONALITY), export_modes=ExportModes.All, enabled_name='ui_trait_category_tag'), 'loot_on_trait_add': OptionalTunable( description= '\n If tuned, this list of loots will be applied when trait is added in game.\n ', tunable=TunableList( description= '\n List of loot to apply on the sim when this trait is added not\n through CAS.\n ', tunable=TunableReference( description= '\n Loot to apply.\n ', manager=services.get_instance_manager( sims4.resources.Types.ACTION), pack_safe=True))), 'npc_leave_lot_interactions': OptionalTunable( description= '\n If enabled, allows tuning a set of Leave Lot and Leave Lot Must Run\n interactions that this trait provides. NPC Sims with this trait will\n use these interactions to leave the lot instead of the defaults.\n ', tunable=TunableTuple( description= '\n Leave Lot Now and Leave Lot Now Must Run interactions.\n ', leave_lot_now_interactions=TunableSet( TunableReference( description= '\n If tuned, the Sim will consider these interaction when trying to run\n any "leave lot" situation.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION), allow_none=False, pack_safe=True)), leave_lot_now_must_run_interactions=TunableSet( TunableReference( description= '\n If tuned, the Sim will consider these interaction when trying to run\n any "leave lot must run" situation.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION), allow_none=False, pack_safe=True)))), 'hide_relationships': Tunable( description= '\n If checked, then any relationships with a Sim who has this trait\n will not be displayed in the UI. This is done by keeping the\n relationship from having any tracks to actually track which keeps\n it out of the UI.\n ', tunable_type=bool, default=False, tuning_group=GroupNames.RELATIONSHIP), 'whim_set': OptionalTunable( description= '\n If enabled then this trait will offer a whim set to the Sim when it\n is active.\n ', tunable=TunableReference( description= '\n A whim set that is active when this trait is active.\n ', manager=services.get_instance_manager( sims4.resources.Types.ASPIRATION), class_restrictions=('ObjectivelessWhimSet', ))), 'allow_from_gallery': Tunable( description= '\n If checked, then this trait is allowed to be transferred over from\n Sims downloaded from the gallery.\n ', tunable_type=bool, default=True, tuning_group=GroupNames.SPECIAL_CASES), 'remove_on_death': Tunable( description= '\n If checked, when a Sim dies this trait will be removed.\n ', tunable_type=bool, default=False, tuning_group=GroupNames.SPECIAL_CASES), 'build_buy_purchase_tracking': OptionalTunable( description= '\n If enabled, allows this trait to track various build-buy purchases\n via event listening in the trait tracker.\n ', tunable=TunableList( description= '\n Loots to apply to the hamper when clothing pile is being put.\n ', tunable=TunableReference(manager=services.get_instance_manager( sims4.resources.Types.ACTION), class_restrictions=('LootActions', ), pack_safe=True))) } _asm_param_name = None default_trait_params = set() def __repr__(self): return '<Trait:({})>'.format(self.__name__) def __str__(self): return '{}'.format(self.__name__) @classmethod def _tuning_loaded_callback(cls): cls._asm_param_name = cls.trait_asm_overrides.trait_asm_param if cls._asm_param_name is None: cls._asm_param_name = cls.__name__ if cls.trait_asm_overrides.trait_asm_param is not None and cls.trait_asm_overrides.consider_for_boundary_conditions: cls.default_trait_params.add( cls.trait_asm_overrides.trait_asm_param) for (buff, replacement_buff) in cls.buff_replacements.items(): if buff.trait_replacement_buffs is None: buff.trait_replacement_buffs = {} buff.trait_replacement_buffs[cls] = replacement_buff for mood in cls.excluded_mood_types: if mood.excluding_traits is None: mood.excluding_traits = [] mood.excluding_traits.append(cls) @classmethod def _verify_tuning_callback(cls): if cls.display_name: if not cls.display_name_gender_neutral.hash: logger.error( 'Trait {} specifies a display name. It must also specify a gender-neutral display name. These must use different string keys.', cls, owner='BadTuning') if cls.display_name._string_id == cls.display_name_gender_neutral.hash: logger.error( 'Trait {} has the same string tuned for its display name and its gender-neutral display name. These must be different strings for localization.', cls, owner='BadTuning') if cls.day_night_tracking is not None: if not cls.day_night_tracking.sunlight_buffs and not ( not cls.day_night_tracking.shade_buffs and not (not cls.day_night_tracking.day_buffs and not cls.day_night_tracking.night_buffs)): logger.error( 'Trait {} has Day Night Tracking enabled but no buffs are tuned. Either tune buffs or disable the tracking.', cls, owner='BadTuning') else: tracking_buff_tag = Trait.DAY_NIGHT_TRACKING_BUFF_TAG if any( buff for buff in cls.day_night_tracking.sunlight_buffs if not buff.buff_type.has_tag(tracking_buff_tag) ) or (any(buff for buff in cls.day_night_tracking.shade_buffs if not buff.buff_type.has_tag(tracking_buff_tag)) or any(buff for buff in cls.day_night_tracking.day_buffs if not buff.buff_type.has_tag(tracking_buff_tag)) ) or any( buff for buff in cls.day_night_tracking.night_buffs if not buff.buff_type.has_tag(tracking_buff_tag)): logger.error( 'Trait {} has Day Night tracking with an invalid\n buff. All buffs must be tagged with {} in order to be\n used as part of Day Night Tracking. Add these buffs with the\n understanding that, regardless of what system added them, they\n will always be on the Sim when the condition is met (i.e.\n Sunlight Buffs always added with sunlight is out) and they will\n always be removed when the condition is not met. Even if another\n system adds the buff, they will be removed if this trait is\n tuned to do that.\n ', cls, tracking_buff_tag) for buff_reference in cls.buffs: if buff_reference.buff_type.broadcaster is not None: logger.error( 'Trait {} has a buff {} with a broadcaster tuned that will never be removed. This is a potential performance hit, and a GPE should decide whether this is the best place for such.', cls, buff_reference, owner='rmccord') for commodity in cls.initial_commodities: if not commodity.persisted_tuning: logger.error( 'Trait {} has an initial commodity {} that does not have persisted tuning.', cls, commodity) @classproperty def is_personality_trait(cls): return cls.trait_type == TraitType.PERSONALITY @classproperty def is_aspiration_trait(cls): return cls.trait_type == TraitType.ASPIRATION @classproperty def is_gender_option_trait(cls): return cls.trait_type == TraitType.GENDER_OPTIONS @classproperty def is_ghost_trait(cls): return cls.trait_type == TraitType.GHOST @classproperty def is_robot_trait(cls): return cls.trait_type == TraitType.ROBOT @classmethod def is_valid_trait(cls, sim_info_data): if cls.ages and sim_info_data.age not in cls.ages: return False if cls.genders and sim_info_data.gender not in cls.genders: return False elif cls.species and sim_info_data.species not in cls.species: return False return True @classmethod def should_apply_fixup_actions(cls, fixup_source): if cls.sim_info_fixup_actions and cls.sim_info_fixup_actions_timing == fixup_source: if fixup_source != SimInfoFixupActionTiming.ON_FIRST_SIMINFO_LOAD: logger.warn( 'Trait {} has fixup actions not from CAS flow.This should only happen to old saves before EP08', cls, owner='yozhang') return True return False @classmethod def apply_fixup_actions(cls, sim_info): for fixup_action in cls.sim_info_fixup_actions: fixup_action(sim_info) @classmethod def can_age_up(cls, current_age): if not cls.disable_aging: return True return current_age in cls.disable_aging.allowed_ages @classmethod def is_conflicting(cls, trait): if trait is None: return False if cls.conflicting_traits and trait in cls.conflicting_traits: return True elif trait.conflicting_traits and cls in trait.conflicting_traits: return True return False @classmethod def get_outfit_change_reason(cls, outfit_change_reason): replaced_reason = cls.outfit_replacements.get( outfit_change_reason if outfit_change_reason is not None else OutfitChangeReason.Invalid) if replaced_reason is not None: return replaced_reason elif outfit_change_reason is not None: replaced_reason = cls.outfit_replacements.get( OutfitChangeReason.Invalid) if replaced_reason is not None: return replaced_reason return outfit_change_reason @classmethod def get_teleport_style_interaction_to_inject(cls): return cls.teleport_style_interaction_to_inject
class MotherPlantBattleSituation(SituationComplexCommon): MOTHER_PLANT_METER_ID = 1 PLAYER_HEALTH_METER_ID = 2 INSTANCE_TUNABLES = { 'player_job': TunableReference( description= '\n Job for the main player sim that fights the plant.\n ', manager=services.get_instance_manager( sims4.resources.Types.SITUATION_JOB)), 'player_sim_role_state': TunableReference( description= '\n Role state for the main player sim Role.\n ', manager=services.get_instance_manager( sims4.resources.Types.ROLE_STATE)), 'other_player_jobs': TunableReference( description= '\n Job for the other player Sims that are not the main Sim and are not\n participating as helpers.\n ', manager=services.get_instance_manager( sims4.resources.Types.SITUATION_JOB)), 'other_player_sims_role_state': TunableReference( description= '\n Role state for the other player Sims.\n ', manager=services.get_instance_manager( sims4.resources.Types.ROLE_STATE)), 'helper_1_job': TunableReference( description= '\n Job for one of the helper Sims for the fight.\n ', manager=services.get_instance_manager( sims4.resources.Types.SITUATION_JOB)), 'helper_2_job': TunableReference( description= '\n Job for one of the helper Sims for the fight.\n ', manager=services.get_instance_manager( sims4.resources.Types.SITUATION_JOB)), 'helper_3_job': TunableReference( description= '\n Job for one of the helper Sims for the fight.\n ', manager=services.get_instance_manager( sims4.resources.Types.SITUATION_JOB)), 'helper_sim_prepare_role_state_1': TunableReference( description= '\n Role state for helper Sim 1 when preparing for battle.\n ', manager=services.get_instance_manager( sims4.resources.Types.ROLE_STATE)), 'helper_sim_prepare_role_state_2': TunableReference( description= '\n Role state for helper Sim 2 when preparing for battle.\n ', manager=services.get_instance_manager( sims4.resources.Types.ROLE_STATE)), 'helper_sim_prepare_role_state_3': TunableReference( description= '\n Role state for helper Sim 3 when preparing for battle.\n ', manager=services.get_instance_manager( sims4.resources.Types.ROLE_STATE)), 'zombie_job': TunableReference( description= '\n Job for the Zombies for the fight.\n ', manager=services.get_instance_manager( sims4.resources.Types.SITUATION_JOB)), 'zombie_prepare_role_state': TunableReference( description= '\n Role state for the zombie Sims when preparing for battle.\n ', manager=services.get_instance_manager( sims4.resources.Types.ROLE_STATE)), 'zombie_fight_interaction': TunableReference( description= '\n Interaction pushed on zombies to get them to fight a Sim.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION)), 'zombie_fight_interaction_timer': TunableSimMinute( description= '\n Timer for the amount of time between zombie attacks.\n ', minimum=1, default=30), 'player_health_statistic': TunableReference( description= "\n The statistic that we will use in order to determine the Sim's\n health for the motherplant.\n ", manager=services.get_instance_manager( sims4.resources.Types.STATISTIC)), 'motherplant_health_statisic': TunableReference( description= "\n The statistic that we will use in order to determine the Sim's\n health for the motherplant.\n ", manager=services.get_instance_manager( sims4.resources.Types.STATISTIC)), 'victory_interaction_of_interest': TunableInteractionOfInterest( description= '\n The interaction of interest that we are looking for to determine\n victory.\n ' ), 'retreat_interaction_of_interest': TunableInteractionOfInterest( description= '\n The interaction of interest that we are looking for to determine\n retreat.\n ' ), 'loss_interaction_mixer': TunableReference( description= '\n The affordance that will be pushed on the primary Sims if they\n lose.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION)), 'fight_affordance': TunableReference( description= '\n The primary fight interaction that we will use to run the defeat\n mixer the player Sim.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION)), 'helper_victory_affordance': TunableReference( description= '\n The affordance that will be pushed on the helper Sims if they\n achieve victory.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION)), 'helper_lose_affordance': TunableReference( description= '\n The affordance that will be pushed on the helper Sims if they\n lose.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION)), 'mother_plant_definition': TunableReference( description= '\n The actual mother plant itself.\n ', manager=services.definition_manager()), 'base_battle_situation_state': BattleThePlantSituationState.TunableFactory( locked_args={ 'allow_join_situation': True, 'time_out': None }, tuning_group=GroupNames.STATE), 'attack_battle_situation_state': AttackBattleThePlantSituationState.TunableFactory( locked_args={'allow_join_situation': True}, tuning_group=GroupNames.STATE), 'inspire_battle_situation_state': InspireBattleThePlantSituationState.TunableFactory( locked_args={'allow_join_situation': True}, tuning_group=GroupNames.STATE), 'rally_battle_sitaution_state': RallyBattleThePlantSituationState.TunableFactory( locked_args={'allow_join_situation': True}, tuning_group=GroupNames.STATE), 'warbling_warcry_battle_situation_state': WarblingWarcryBattleThePlantSituationState.TunableFactory( locked_args={'allow_join_situation': True}, tuning_group=GroupNames.STATE), 'save_lock_tooltip': TunableLocalizedString( description= '\n The tooltip/message to show when the player tries to save the game\n while this situation is running. Save is locked when situation starts.\n ', tuning_group=GroupNames.UI), 'mother_plant_meter_settings': StatBasedSituationMeterData.TunableFactory( description= '\n The meter used to track the health of the mother plant.\n ', tuning_group=GroupNames.SITUATION, locked_args={'_meter_id': MOTHER_PLANT_METER_ID}), 'player_health_meter_settings': StatBasedSituationMeterData.TunableFactory( description= '\n The meter used to track the health of the player team.\n ', tuning_group=GroupNames.SITUATION, locked_args={'_meter_id': PLAYER_HEALTH_METER_ID}), 'mother_plant_icon': TunableResourceKey( description= '\n Icon to be displayed in the situation UI beside the mother plant\n health bar.\n ', resource_types=sims4.resources.CompoundTypes.IMAGE, default=None, allow_none=True, tuning_group=GroupNames.SITUATION), 'states_to_set_on_start': TunableList( description= '\n A list of states to set on the motherplant on start.\n ', tunable=TunableStateValueReference( description= '\n The state to set.\n ')), 'states_to_set_on_end': TunableList( description= '\n A list of states to set on the motherplant on end.\n ', tunable=TunableStateValueReference( description= '\n The state to set.\n ')), 'victory_reward': TunableReference( description= '\n The Reward received when the Sim wins the situation.\n ', manager=services.get_instance_manager( sims4.resources.Types.REWARD)), 'victory_audio_sting': TunableResourceKey( description= '\n The sound to play when the Sim wins the battle.\n ', resource_types=(sims4.resources.Types.PROPX, ), default=None, tuning_group=GroupNames.AUDIO), 'defeat_audio_sting': TunableResourceKey( description= '\n The sound to play when the Sim loses the battle.\n ', resource_types=(sims4.resources.Types.PROPX, ), default=None, tuning_group=GroupNames.AUDIO), 'possessed_buff': TunableBuffReference( description= '\n Possessed Buff for zombie Sims. \n ') } @property def user_facing_type(self): return SituationUserFacingType.MOTHER_PLANT_EVENT @property def situation_display_type(self): return SituationDisplayType.VET @property def situation_display_priority(self): return SituationDisplayPriority.VET @classmethod def _states(cls): return (SituationStateData(1, PrepareForBattleSituationState), SituationStateData.from_auto_factory( 2, cls.base_battle_situation_state), SituationStateData.from_auto_factory( 3, cls.attack_battle_situation_state), SituationStateData.from_auto_factory( 4, cls.inspire_battle_situation_state), SituationStateData.from_auto_factory( 5, cls.rally_battle_sitaution_state), SituationStateData.from_auto_factory( 6, cls.warbling_warcry_battle_situation_state)) @classmethod def default_job(cls): pass @classmethod def _get_tuned_job_and_default_role_state_tuples(cls): return ((cls.player_job, cls.player_sim_role_state), (cls.other_player_jobs, cls.other_player_sims_role_state), (cls.helper_1_job, cls.helper_sim_prepare_role_state_1), (cls.helper_2_job, cls.helper_sim_prepare_role_state_2), (cls.helper_3_job, cls.helper_sim_prepare_role_state_3), (cls.zombie_job, cls.zombie_prepare_role_state)) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._zombie_attack_alarm_handle = None self._registered_test_events = set() self._player_health_tracking_situation_goal = None self._statistic_watcher_handle = None self._victory = False @property def end_audio_sting(self): if self._victory: return self.victory_audio_sting return self.defeat_audio_sting def _get_reward(self): if self._victory: return self.victory_reward def _get_motherplant(self): return next( iter(services.object_manager().get_objects_of_type_gen( self.mother_plant_definition))) def _push_loss_on_player(self): motherplant = self._get_motherplant() for (sim, situation_sim) in self._situation_sims.items(): if situation_sim.current_job_type is self.player_job: parent_si = sim.si_state.get_si_by_affordance( self.fight_affordance) if parent_si is not None: interaction_context = InteractionContext( sim, InteractionSource.PIE_MENU, Priority.Critical) aop = AffordanceObjectPair(self.loss_interaction_mixer, motherplant, self.fight_affordance, parent_si) if not aop.test_and_execute(interaction_context): logger.error( 'Attempting to push Motherplant Battle Ending Interaction, but failed.' ) self._push_interaction_on_all_helpers(self.helper_lose_affordance) def on_goal_completed(self, goal): super().on_goal_completed(goal) self._push_loss_on_player() self._self_destruct() def _on_set_sim_job(self, sim, job_type): super()._on_set_sim_job(sim, job_type) if job_type is self.zombie_job: sim.add_buff_from_op(self.possessed_buff.buff_type, buff_reason=self.possessed_buff.buff_reason) def _on_statistic_updated(self, stat_type, old_value, new_value): if stat_type is self.player_health_statistic: self._player_health_tracking_situation_goal.set_count(new_value) self._player_health_meter.send_update_if_dirty() elif stat_type is self.motherplant_health_statisic: self._mother_plant_meter.send_update_if_dirty() def _zombie_attack(self, _): if not self._cur_state.zombie_attack_valid: return zombies = [] for (sim, situation_sim) in self._situation_sims.items(): if situation_sim.current_job_type is self.zombie_job: zombies.append(sim) zombie_to_attack = random.choice(zombies) context = InteractionContext( sim, InteractionContext.SOURCE_SCRIPT, interactions.priority.Priority.High, insert_strategy=QueueInsertStrategy.NEXT, bucket=interactions.context.InteractionBucketType.DEFAULT) zombie_to_attack.push_super_affordance(self.zombie_fight_interaction, None, context) def _push_interaction_on_all_helpers(self, interaction_to_push): for (sim, situation_sim) in self._situation_sims.items(): if not situation_sim.current_job_type is self.helper_1_job: if not situation_sim.current_job_type is self.helper_2_job: if situation_sim.current_job_type is self.helper_3_job: context = InteractionContext( sim, InteractionContext.SOURCE_SCRIPT, interactions.priority.Priority.High, insert_strategy=QueueInsertStrategy.NEXT, bucket=interactions.context.InteractionBucketType. DEFAULT) sim.push_super_affordance(interaction_to_push, None, context) context = InteractionContext( sim, InteractionContext.SOURCE_SCRIPT, interactions.priority.Priority.High, insert_strategy=QueueInsertStrategy.NEXT, bucket=interactions.context.InteractionBucketType.DEFAULT) sim.push_super_affordance(interaction_to_push, None, context) def handle_event(self, sim_info, event, resolver): super().handle_event(sim_info, event, resolver) if event != TestEvent.InteractionComplete: return if resolver(self.victory_interaction_of_interest): self._push_interaction_on_all_helpers( self.helper_victory_affordance) self._victory = True self._self_destruct() elif resolver(self.retreat_interaction_of_interest): self._push_loss_on_player() self._self_destruct() def start_situation(self): services.get_persistence_service().lock_save(self) super().start_situation() self._change_state(PrepareForBattleSituationState()) motherplant = self._get_motherplant() motherplant.set_stat_value(self.player_health_statistic, 0, add=True) motherplant.set_stat_value(self.motherplant_health_statisic, self.motherplant_health_statisic.max_value, add=True) for state_value in self.states_to_set_on_start: motherplant.set_state(state_value.state, state_value) statistic_tracker = motherplant.statistic_tracker self._statistic_watcher_handle = statistic_tracker.add_watcher( self._on_statistic_updated) self._setup_situation_meters() self._zombie_attack_alarm_handle = alarms.add_alarm( self, create_time_span(minutes=self.zombie_fight_interaction_timer), self._zombie_attack, repeating=True) for custom_key in itertools.chain( self.victory_interaction_of_interest.custom_keys_gen(), self.retreat_interaction_of_interest.custom_keys_gen()): custom_key_tuple = (TestEvent.InteractionComplete, custom_key) self._registered_test_events.add(custom_key_tuple) services.get_event_manager().register_with_custom_key( self, TestEvent.InteractionComplete, custom_key) def _setup_situation_meters(self): motherplant = self._get_motherplant() self._mother_plant_meter = self.mother_plant_meter_settings.create_meter_with_sim_info( self, motherplant) self._player_health_meter = self.player_health_meter_settings.create_meter_with_sim_info( self, motherplant) def build_situation_start_message(self): msg = super().build_situation_start_message() with ProtocolBufferRollback(msg.meter_data) as meter_data_msg: self.mother_plant_meter_settings.build_data_message(meter_data_msg) with ProtocolBufferRollback(msg.meter_data) as meter_data_msg: self.player_health_meter_settings.build_data_message( meter_data_msg) build_icon_info_msg(IconInfoData(icon_resource=self.mother_plant_icon), None, msg.icon_info) return msg def _destroy(self): super()._destroy() services.get_persistence_service().unlock_save(self) for (event_type, custom_key) in self._registered_test_events: services.get_event_manager().unregister_with_custom_key( self, event_type, custom_key) motherplant = self._get_motherplant() statistic_tracker = motherplant.statistic_tracker statistic_tracker.remove_watcher(self._statistic_watcher_handle) for state_value in self.states_to_set_on_end: motherplant.set_state(state_value.state, state_value) self._registered_test_events.clear() if self._mother_plant_meter is not None: self._mother_plant_meter.destroy() if self._player_health_meter is not None: self._player_health_meter.destroy() def get_lock_save_reason(self): return self.save_lock_tooltip def set_motherplant_situation_state(self, motherplant_battle_state): if motherplant_battle_state == MotherplantBattleStates.ATTACK: self._change_state(self.attack_battle_situation_state()) elif motherplant_battle_state == MotherplantBattleStates.INSPIRE: self._change_state(self.inspire_battle_situation_state()) elif motherplant_battle_state == MotherplantBattleStates.RALLY: self._change_state(self.rally_battle_sitaution_state()) elif motherplant_battle_state == MotherplantBattleStates.WARBLING_WARCRY: self._change_state(self.warbling_warcry_battle_situation_state()) def _on_proxy_situation_goal_added(self, goal): self._player_health_tracking_situation_goal = goal def _issue_requests(self): super()._issue_requests() request = SelectableSimRequestFactory( self, _RequestUserData(), self.other_player_jobs, self.exclusivity, request_priority=BouncerRequestPriority.EVENT_DEFAULT_JOB) self.manager.bouncer.submit_request(request)
class DeathTracker(SimInfoTracker): DEATH_ZONE_ID = 0 DEATH_TYPE_GHOST_TRAIT_MAP = TunableMapping( description= '\n The ghost trait to be applied to a Sim when they die with a given death\n type.\n ', key_type=TunableEnumEntry( description= '\n The death type to map to a ghost trait.\n ', tunable_type=DeathType, default=DeathType.NONE), key_name='Death Type', value_type=TunableReference( description= '\n The ghost trait to apply to a Sim when they die from the specified\n death type.\n ', manager=services.trait_manager()), value_name='Ghost Trait') DEATH_BUFFS = TunableList( description= '\n A list of buffs to apply to Sims when another Sim dies. For example, use\n this tuning to tune a "Death of a Good Friend" buff.\n ', tunable=TunableTuple( test_set=TunableReference( description= "\n The test that must pass between the dying Sim (TargetSim) and\n the Sim we're considering (Actor). If this test passes, no\n further test is executed.\n ", manager=services.get_instance_manager(sims4.resources.Types. SNIPPET), class_restrictions=('TestSetInstance', ), pack_safe=True), buff=TunableBuffReference( description= '\n The buff to apply to the Sim.\n ', pack_safe=True), notification=OptionalTunable( description= '\n If enabled, an off-lot death generates a notification for the\n target Sim. This is limited to one per death instance.\n ', tunable=TunableUiDialogNotificationReference( description= '\n The notification to show.\n ', pack_safe=True)))) IS_DYING_BUFF = TunableReference( description= '\n A reference to the buff a Sim is given when they are dying.\n ', manager=services.buff_manager()) DEATH_RELATIONSHIP_BIT_FIXUP_LOOT = TunableReference( description= '\n A reference to the loot to apply to a Sim upon death.\n \n This is where the relationship bit fixup loots will be tuned. This\n used to be on the interactions themselves but if the interaction was\n reset then the bits would stay as they were. If we add more relationship\n bits we want to clean up on death, the references Loot is the place to \n do it.\n ', manager=services.get_instance_manager(sims4.resources.Types.ACTION)) def __init__(self, sim_info): self._sim_info = sim_info self._death_type = None self._death_time = None @property def death_type(self): return self._death_type @property def death_time(self): return self._death_time @property def is_ghost(self): return self._sim_info.trait_tracker.has_any_trait( self.DEATH_TYPE_GHOST_TRAIT_MAP.values()) def get_ghost_trait(self): return self.DEATH_TYPE_GHOST_TRAIT_MAP.get(self._death_type) def set_death_type(self, death_type, is_off_lot_death=False): is_npc = self._sim_info.is_npc household = self._sim_info.household self._sim_info.inject_into_inactive_zone(self.DEATH_ZONE_ID, start_away_actions=False, skip_instanced_check=True, skip_daycare=True) household.remove_sim_info(self._sim_info, destroy_if_empty_household=True) if is_off_lot_death: household.pending_urnstone_ids.append(self._sim_info.sim_id) self._sim_info.transfer_to_hidden_household() clubs.on_sim_killed_or_culled(self._sim_info) if death_type is None: return relationship_service = services.relationship_service() for target_sim_info in relationship_service.get_target_sim_infos( self._sim_info.sim_id): resolver = DoubleSimResolver(target_sim_info, self._sim_info) for death_data in self.DEATH_BUFFS: if not death_data.test_set(resolver): continue target_sim_info.add_buff_from_op( death_data.buff.buff_type, buff_reason=death_data.buff.buff_reason) if is_npc and not target_sim_info.is_npc: notification = death_data.notification(target_sim_info, resolver=resolver) notification.show_dialog() break ghost_trait = DeathTracker.DEATH_TYPE_GHOST_TRAIT_MAP.get(death_type) if ghost_trait is not None: self._sim_info.add_trait(ghost_trait) traits = list(self._sim_info.trait_tracker.equipped_traits) for trait in traits: if trait.remove_on_death: self._sim_info.remove_trait(trait) self._death_type = death_type self._death_time = services.time_service().sim_now.absolute_ticks() self._sim_info.reset_age_progress() self._sim_info.resend_death_type() self._handle_remove_rel_bits_on_death() services.get_event_manager().process_event( test_events.TestEvent.SimDeathTypeSet, sim_info=self._sim_info) def _handle_remove_rel_bits_on_death(self): resolver = SingleSimResolver(self._sim_info) if self.DEATH_RELATIONSHIP_BIT_FIXUP_LOOT is not None: for (loot, _) in self.DEATH_RELATIONSHIP_BIT_FIXUP_LOOT.get_loot_ops_gen( ): result = loot.test_resolver(resolver) if result: loot.apply_to_resolver(resolver) def clear_death_type(self): self._death_type = None self._death_time = None self._sim_info.resend_death_type() def save(self): if self._death_type is not None: data = protocols.PersistableDeathTracker() data.death_type = self._death_type data.death_time = self._death_time return data def load(self, data): try: self._death_type = DeathType(data.death_type) except: self._death_type = DeathType.NONE self._death_time = data.death_time @classproperty def _tracker_lod_threshold(cls): return SimInfoLODLevel.MINIMUM
class TutorialTip( metaclass=sims4.tuning.instances.HashedTunedInstanceMetaclass, manager=services.get_instance_manager( sims4.resources.Types.TUTORIAL_TIP)): INSTANCE_TUNABLES = { 'required_tip_groups': TunableList( description= '\n The Tip Groups that must be complete for this tip to be valid.\n ', tunable=TunableReference(manager=services.get_instance_manager( sims4.resources.Types.TUTORIAL_TIP), class_restrictions='TutorialTipGroup'), tuning_group=GROUP_NAME_DISPLAY_CRITERIA, export_modes=ExportModes.ClientBinary), 'required_ui_list': TunableList( description= '\n The UI elements that are required to be present in order for this\n tutorial tip to be valid.\n ', tunable=TunableEnumEntry(tunable_type=TutorialTipUiElement, default=TutorialTipUiElement.UI_INVALID), tuning_group=GROUP_NAME_DISPLAY_CRITERIA, export_modes=ExportModes.ClientBinary), 'required_ui_hidden_list': TunableList( description= '\n The UI elements that are required to NOT be present in order for this\n tutorial tip to be valid.\n ', tunable=TunableEnumEntry(tunable_type=TutorialTipUiElement, default=TutorialTipUiElement.UI_INVALID), tuning_group=GROUP_NAME_DISPLAY_CRITERIA, export_modes=ExportModes.ClientBinary), 'required_game_state': TunableEnumEntry( description= '\n The state the game must be in for this tutorial tip to be valid.\n ', tunable_type=TutorialTipGameState, default=TutorialTipGameState.GAMESTATE_NONE, tuning_group=GROUP_NAME_DISPLAY_CRITERIA, export_modes=ExportModes.ClientBinary), 'required_tips_not_satisfied': TunableList( description= '\n This is a list of tips that must be un-satisfied in order for this\n tip to activate. If any tip in this list is satisfied, this tip will\n not activate.\n ', tunable=TunableReference(manager=services.get_instance_manager( sims4.resources.Types.TUTORIAL_TIP), class_restrictions='TutorialTip'), tuning_group=GROUP_NAME_DISPLAY_CRITERIA, export_modes=ExportModes.ClientBinary), 'platform_filter': TunableEnumEntry( description= '\n The platforms on which this tutorial tip is shown.\n ', tunable_type=tutorials.tutorial.TutorialPlatformFilter, default=tutorials.tutorial.TutorialPlatformFilter.ALL_PLATFORMS, tuning_group=GROUP_NAME_DISPLAY_CRITERIA, export_modes=ExportModes.ClientBinary), 'required_tutorial_mode': TunableEnumEntry( description= '\n What mode this tutorial tip should be restricted to.\n STANDARD allows this tip to be in the original / standard tutorial mode.\n FTUE allows this tip to be in the FTUE tutorial mode.\n DISABLED means this tip is valid in any mode.\n ', tunable_type=TutorialMode, default=TutorialMode.STANDARD, tuning_group=GROUP_NAME_DISPLAY_CRITERIA, export_modes=ExportModes.ClientBinary), 'display': TunableTutorialTipDisplay( description= '\n This display information for this tutorial tip.\n ', tuning_group=GROUP_NAME_ACTIONS, export_modes=ExportModes.ClientBinary), 'display_narration': OptionalTunable( description= '\n Optionally play narration voice-over and display subtitles.\n ', tunable=TunableTuple( voiceover_audio=TunableResourceKey( description= '\n Narration audio to play.\n ', default=None, allow_none=True, resource_types=(sims4.resources.Types.PROPX, )), voiceover_audio_ps4=TunableResourceKey( description= '\n Narration audio to play specific to PS4.\n ', default=None, allow_none=True, resource_types=(sims4.resources.Types.PROPX, )), voiceover_audio_xb1=TunableResourceKey( description= '\n Narration audio to play specific to XB1.\n ', default=None, allow_none=True, resource_types=(sims4.resources.Types.PROPX, )), subtitle_text=TunableLocalizedString( description= '\n Subtitles to display while audio narration is playing.\n ' ), subtitle_display_location=TunableVariant( description= '\n What area on the screen the subtitles should appear.\n Top - Use the generic top-of-screen position.\n Bottom - Use the generic bottom-of-screen position.\n Custom - Specify a custom position in terms of % vertically.\n ', location=TunableEnumEntry( description= '\n Semantic location (UX-defined) for where the subtitles should appear.\n ', tunable_type=TutorialTipSubtitleDisplayLocation, default=TutorialTipSubtitleDisplayLocation.BOTTOM), custom=TunablePercent( description= '\n Vertical position for the subtitles, expressed as a\n percentage of the height of the screen.\n ', default=90), default='location'), satisfy_when_voiceover_finished=Tunable( description= '\n If set, the tutorial tip will be marked as satisfied when the\n voiceover completes or is interrupted.\n ', tunable_type=bool, default=False), delay_satisfaction_until_voiceover_finished=Tunable( description= '\n If set, the tutorial tip will not be marked satisfied until after\n the voiceover completes, preventing the voiceover from being\n interrupted by external satisfaction.\n ', tunable_type=bool, default=False), keep_subtitle_visible_until_satisfaction=Tunable( description= '\n If set, the subtitle will remain visible until the tutorial tip is\n marked as satisfied, even though the voiceover may have finished.\n ', tunable_type=bool, default=False), export_class_name='TutorialTipNarrationDisplay'), tuning_group=GROUP_NAME_ACTIONS, export_modes=ExportModes.ClientBinary), 'activation_ui_message': TunableTutorialTipUiMessage( description= '\n Sends a message to the UI when this tip is activated.\n ', tuning_group=GROUP_NAME_ACTIONS, export_modes=ExportModes.ClientBinary), 'deactivation_ui_message': TunableTutorialTipUiMessage( description= '\n Sends a message to the UI when this tip is deactivated.\n ', tuning_group=GROUP_NAME_ACTIONS, export_modes=ExportModes.ClientBinary), 'buffs': TunableList( description= '\n Buffs that will be applied at the start of this tutorial tip.\n ', tunable=TunableBuffReference(), tuning_group=GROUP_NAME_ACTIONS), 'buffs_removed_on_deactivate': Tunable( description= '\n If enabled, this tip will remove those buffs on deactivate.\n ', tunable_type=bool, default=False, tuning_group=GROUP_NAME_ACTIONS), 'commodities_to_solve': TunableSet( description= "\n A set of commodities we will attempt to solve. This will result in\n the Sim's interaction queue being filled with various interactions.\n ", tunable=TunableReference(services.statistic_manager()), tuning_group=GROUP_NAME_ACTIONS), 'gameplay_loots': OptionalTunable( description= '\n Loots that will be given at the start of this tip.\n Actor is is the sim specified by Sim Actor.\n Target is the sim specified by Sim Target.\n ', tunable=TunableList( tunable=TunableReference(manager=services.get_instance_manager( sims4.resources.Types.ACTION), class_restrictions=('LootActions', ), pack_safe=True)), tuning_group=GROUP_NAME_ACTIONS), 'restricted_affordances': OptionalTunable( description= '\n If enabled, use the filter to determine which affordances are allowed.\n ', tunable=TunableTuple( visible_affordances=TunableAffordanceFilterSnippet( description= '\n The filter of affordances that are visible.\n ' ), tooltip=OptionalTunable( description= '\n Tooltip when interaction is disabled by tutorial restrictions\n If not specified, will use the default in the tutorial service\n tuning.\n ', tunable=sims4.localization.TunableLocalizedStringFactory( )), enabled_affordances=TunableAffordanceFilterSnippet( description= '\n The filter of visible affordances that are enabled.\n ' )), tuning_group=GROUP_NAME_ACTIONS), 'call_to_actions': OptionalTunable( description= '\n Call to actions that should persist for the duration of this tip.\n ', tunable=TunableList( tunable=TunableReference(manager=services.get_instance_manager( sims4.resources.Types.CALL_TO_ACTION), pack_safe=True)), tuning_group=GROUP_NAME_ACTIONS), 'end_drama_node': Tunable( description= '\n If enabled, this tip will end the tutorial drama node.\n ', tunable_type=bool, default=False, tuning_group=GROUP_NAME_ACTIONS), 'sim_actor': TunableEnumEntry( description= "\n The entity who will be the actor sim for loot, and will\n receive the items that aren't specified via loots.\n \n If there is no Tutorial Drama Node active, actor will be active\n sim\n ", tunable_type=TutorialTipActorOption, default=TutorialTipActorOption.ACTIVE_SIM, tuning_group=GROUP_NAME_ACTIONS), 'sim_target': TunableEnumEntry( description= '\n The entity who will be the target sim for loot\n \n If there is no Tutorial Drama Node active, target sim will be active\n sim.\n ', tunable_type=TutorialTipActorOption, default=TutorialTipActorOption.ACTIVE_SIM, tuning_group=GROUP_NAME_ACTIONS), 'add_target_to_actor_household': Tunable( description= '\n If enabled, target sim will be added to active sim household.\n ', tunable_type=bool, default=False, tuning_group=GROUP_NAME_ACTIONS), 'make_housemate_unselectable': Tunable( description= '\n If enabled, housemate will be unselectable for the duration of the\n tooltip.\n ', tunable_type=bool, default=False, tuning_group=GROUP_NAME_ACTIONS), 'timeout_satisfies': Tunable( description= '\n If enabled, this tip is satisfied when the timeout is reached.\n If disabled, this tip will not satisfy when the timeout is reached.\n ', tunable_type=bool, default=False, tuning_group=GROUP_NAME_SATISFY, export_modes=ExportModes.ClientBinary), 'gameplay_test': OptionalTunable( description= '\n Tests that, if passed, will satisfy this tutorial tip.\n Only one test needs to pass to satisfy. These are intended for tips\n where the satisfy message should be tested and sent at a later time.\n ', tunable=tutorials.tutorial.TunableTutorialTestVariant(), tuning_group=GROUP_NAME_SATISFY, export_modes=ExportModes.All), 'sim_tested': TunableEnumEntry( description= '\n The entity who must fulfill the test events.\n \n If there is no Tutorial Drama Node, player sim and housemate sim will be active\n sim.\n ', tunable_type=TutorialTipTestSpecificityOption, default=TutorialTipTestSpecificityOption.UNSPECIFIED, tuning_group=GROUP_NAME_SATISFY), 'time_of_day': OptionalTunable( description= '\n If specified, tutorialtip will be satisfied once the time passes \n the specified time.\n ', tunable=TunableTimeOfDay(), tuning_group=GROUP_NAME_SATISFY), 'gameplay_immediate_test': OptionalTunable( description= '\n Tests that, if passed, will satisfy this tutorial tip.\n Only one test needs to pass to satisfy. These are intended for tips\n where the satisfy message should be tested and sent back immediately.\n ', tunable=tutorials.tutorial.TunableTutorialTestVariant(), tuning_group=GROUP_NAME_SATISFY, export_modes=ExportModes.All), 'satisfy_on_active_sim_change': Tunable( description= '\n If enabled, this tip is satisfied when the active sim changes\n ', tunable_type=bool, default=False, tuning_group=GROUP_NAME_SATISFY, export_modes=ExportModes.All), 'satisfy_on_activate': Tunable( description= "\n If enabled, this tip is satisfied immediately when all of it's\n preconditions have been met.\n ", tunable_type=bool, default=False, tuning_group=GROUP_NAME_SATISFY, export_modes=ExportModes.ClientBinary), 'tutorial_group_to_complete_on_skip': TunableReference( description= '\n The tutorial group who will have all tutorial tips within it\n completed when the button to skip all is pressed from this tip.\n ', manager=services.get_instance_manager( sims4.resources.Types.TUTORIAL_TIP), class_restrictions='TutorialTipGroup', export_modes=ExportModes.ClientBinary) } def __init__(self): raise NotImplementedError @classmethod def activate(cls): tutorial_service = services.get_tutorial_service() client = services.client_manager().get_first_client() actor_sim_info = client.active_sim.sim_info target_sim_info = actor_sim_info housemate_sim_info = None tutorial_drama_node = None drama_scheduler = services.drama_scheduler_service() if drama_scheduler is not None: drama_nodes = drama_scheduler.get_running_nodes_by_drama_node_type( DramaNodeType.TUTORIAL) if drama_nodes: tutorial_drama_node = drama_nodes[0] housemate_sim_info = tutorial_drama_node.get_housemate_sim_info( ) player_sim_info = tutorial_drama_node.get_player_sim_info() if cls.sim_actor == TutorialTipActorOption.PLAYER_SIM: actor_sim_info = player_sim_info elif cls.sim_actor == TutorialTipActorOption.HOUSEMATE_SIM: actor_sim_info = housemate_sim_info if cls.sim_target == TutorialTipActorOption.PLAYER_SIM: target_sim_info = player_sim_info elif cls.sim_target == TutorialTipActorOption.HOUSEMATE_SIM: target_sim_info = housemate_sim_info if cls.gameplay_immediate_test is not None: resolver = event_testing.resolver.SingleSimResolver(actor_sim_info) if resolver(cls.gameplay_immediate_test): cls.satisfy() else: return for buff_ref in cls.buffs: actor_sim_info.add_buff_from_op(buff_ref.buff_type, buff_reason=buff_ref.buff_reason) if cls.gameplay_test is not None: services.get_event_manager().register_tests( cls, [cls.gameplay_test]) if cls.satisfy_on_active_sim_change: client = services.client_manager().get_first_client() if client is not None: client.register_active_sim_changed(cls._on_active_sim_change) if cls.commodities_to_solve: actor_sim = actor_sim_info.get_sim_instance() if actor_sim is not None: context = InteractionContext( actor_sim, InteractionContext.SOURCE_SCRIPT_WITH_USER_INTENT, priority.Priority.High, bucket=InteractionBucketType.DEFAULT) for commodity in cls.commodities_to_solve: if not actor_sim.queue.can_queue_visible_interaction(): break autonomy_request = autonomy.autonomy_request.AutonomyRequest( actor_sim, autonomy_mode=autonomy.autonomy_modes.FullAutonomy, commodity_list=(commodity, ), context=context, consider_scores_of_zero=True, posture_behavior=AutonomyPostureBehavior. IGNORE_SI_STATE, distance_estimation_behavior= AutonomyDistanceEstimationBehavior. ALLOW_UNREACHABLE_LOCATIONS, allow_opportunity_cost=False, autonomy_mode_label_override='Tutorial') selected_interaction = services.autonomy_service( ).find_best_action(autonomy_request) AffordanceObjectPair.execute_interaction( selected_interaction) if cls.gameplay_loots: resolver = DoubleSimResolver(actor_sim_info, target_sim_info) for loot_action in cls.gameplay_loots: loot_action.apply_to_resolver(resolver) if cls.restricted_affordances is not None and tutorial_service is not None: tutorial_service.set_restricted_affordances( cls.restricted_affordances.visible_affordances, cls.restricted_affordances.tooltip, cls.restricted_affordances.enabled_affordances) if cls.call_to_actions is not None: call_to_action_service = services.call_to_action_service() for call_to_action_fact in cls.call_to_actions: call_to_action_service.begin(call_to_action_fact, None) if cls.add_target_to_actor_household: household_manager = services.household_manager() household_manager.switch_sim_household(target_sim_info) if cls.make_housemate_unselectable and tutorial_service is not None: tutorial_service.set_unselectable_sim(housemate_sim_info) if cls.end_drama_node and tutorial_drama_node is not None: tutorial_drama_node.end() if cls.time_of_day is not None and tutorial_service is not None: tutorial_service.add_tutorial_alarm(cls, lambda _: cls.satisfy(), cls.time_of_day) @classmethod def _on_active_sim_change(cls, old_sim, new_sim): cls.satisfy() @classmethod def handle_event(cls, sim_info, event, resolver): if cls.gameplay_test is not None and resolver(cls.gameplay_test): if cls.sim_tested != TutorialTipTestSpecificityOption.UNSPECIFIED: client = services.client_manager().get_first_client() test_sim_info = client.active_sim.sim_info drama_scheduler = services.drama_scheduler_service() if drama_scheduler is not None: drama_nodes = drama_scheduler.get_running_nodes_by_drama_node_type( DramaNodeType.TUTORIAL) if drama_nodes: drama_node = drama_nodes[0] if cls.sim_tested == TutorialTipTestSpecificityOption.PLAYER_SIM: test_sim_info = drama_node.get_player_sim_info() elif cls.sim_tested == TutorialTipTestSpecificityOption.HOUSEMATE_SIM: test_sim_info = drama_node.get_housemate_sim_info() if test_sim_info is not sim_info: return cls.satisfy() @classmethod def satisfy(cls): op = distributor.ops.SetTutorialTipSatisfy(cls.guid64) distributor_instance = Distributor.instance() distributor_instance.add_op_with_no_owner(op) @classmethod def deactivate(cls): tutorial_service = services.get_tutorial_service() client = services.client_manager().get_first_client() if cls.gameplay_test is not None: services.get_event_manager().unregister_tests( cls, (cls.gameplay_test, )) if cls.satisfy_on_active_sim_change and client is not None: client.unregister_active_sim_changed(cls._on_active_sim_change) if cls.restricted_affordances is not None and tutorial_service is not None: tutorial_service.clear_restricted_affordances() if cls.call_to_actions is not None: call_to_action_service = services.call_to_action_service() for call_to_action_fact in cls.call_to_actions: call_to_action_service.end(call_to_action_fact) if cls.buffs_removed_on_deactivate: actor_sim_info = None if client is not None: actor_sim_info = client.active_sim.sim_info drama_scheduler = services.drama_scheduler_service() if drama_scheduler is not None: drama_nodes = drama_scheduler.get_running_nodes_by_drama_node_type( DramaNodeType.TUTORIAL) if drama_nodes: tutorial_drama_node = drama_nodes[0] if cls.sim_actor == TutorialTipActorOption.PLAYER_SIM: actor_sim_info = tutorial_drama_node.get_player_sim_info( ) elif cls.sim_actor == TutorialTipActorOption.HOUSEMATE_SIM: actor_sim_info = tutorial_drama_node.get_housemate_sim_info( ) if actor_sim_info is not None: for buff_ref in cls.buffs: actor_sim_info.remove_buff_by_type(buff_ref.buff_type) if cls.time_of_day is not None and tutorial_service is not None: tutorial_service.remove_tutorial_alarm(cls) if cls.make_housemate_unselectable and tutorial_service is not None: tutorial_service.set_unselectable_sim(None)
class TutorialTip( metaclass=sims4.tuning.instances.HashedTunedInstanceMetaclass, manager=services.get_instance_manager( sims4.resources.Types.TUTORIAL_TIP)): __qualname__ = 'TutorialTip' INSTANCE_TUNABLES = { 'required_tip_groups': TunableList( description= '\n The Tip Groups that must be complete for this tip to be valid.\n ', tunable=TunableReference(manager=services.get_instance_manager( sims4.resources.Types.TUTORIAL_TIP), class_restrictions='TutorialTipGroup'), tuning_group=GROUP_NAME_DISPLAY_CRITERIA, export_modes=ExportModes.ClientBinary), 'required_ui_list': TunableList( description= '\n The UI elements that are required to be present in order for this\n tutorial tip to be valid.\n ', tunable=TunableEnumEntry(tunable_type=TutorialTipUiElement, default=TutorialTipUiElement.UI_INVALID), tuning_group=GROUP_NAME_DISPLAY_CRITERIA, export_modes=ExportModes.ClientBinary), 'required_game_state': TunableEnumEntry( description= '\n The state the game must be in for this tutorial tip to be valid.\n ', tunable_type=TutorialTipGameState, default=TutorialTipGameState.GAMESTATE_NONE, tuning_group=GROUP_NAME_DISPLAY_CRITERIA, export_modes=ExportModes.ClientBinary), 'required_tips_not_satisfied': TunableList( description= '\n This is a list of tips that must be un-satisfied in order for this\n tip to activate. If any tip in this list is satisfied, this tip will\n not activate.\n ', tunable=TunableReference(manager=services.get_instance_manager( sims4.resources.Types.TUTORIAL_TIP), class_restrictions='TutorialTip'), tuning_group=GROUP_NAME_DISPLAY_CRITERIA, export_modes=ExportModes.ClientBinary), 'display': TunableTutorialTipDisplay( description= '\n This display information for this tutorial tip.\n ', tuning_group=GROUP_NAME_ACTIONS, export_modes=ExportModes.ClientBinary), 'buffs': TunableList( description= '\n Buffs that will be applied at the start of this tutorial tip.\n ', tunable=TunableBuffReference(), tuning_group=GROUP_NAME_ACTIONS), 'commodities_to_solve': TunableSet( description= "\n A set of commodities we will attempt to solve. This will result in\n the Sim's interaction queue being filled with various interactions.\n ", tunable=TunableReference(services.statistic_manager()), tuning_group=GROUP_NAME_ACTIONS), 'timeout_satisfies': Tunable( description= '\n If enabled, this tip is satisfied when the timeout is reached.\n If disabled, this tip will not satisfy when the timeout is reached.\n ', tunable_type=bool, default=True, tuning_group=GROUP_NAME_SATISFY, export_modes=ExportModes.ClientBinary), 'gameplay_test': OptionalTunable( description= '\n Tests that, if passed, will satisfy this tutorial tip.\n Only one test needs to pass to satisfy. These are intended for tips\n where the satisfy message should be tested and sent at a later time.\n ', tunable=tutorials.tutorial.TunableTutorialTestVariant(), tuning_group=GROUP_NAME_SATISFY), 'gameplay_immediate_test': OptionalTunable( description= '\n Tests that, if passed, will satisfy this tutorial tip.\n Only one test needs to pass to satisfy. These are intended for tips\n where the satisfy message should be tested and sent back immediately.\n ', tunable=tutorials.tutorial.TunableTutorialTestVariant(), tuning_group=GROUP_NAME_SATISFY), 'satisfy_on_activate': Tunable( description= "\n If enabled, this tip is satisfied immediately when all of it's\n preconditions have been met.\n ", tunable_type=bool, default=False, tuning_group=GROUP_NAME_SATISFY, export_modes=ExportModes.ClientBinary) } def __init__(self): raise NotImplementedError @classmethod def activate(cls): client = services.client_manager().get_first_client() active_sim = client.active_sim if cls.gameplay_immediate_test is not None: resolver = event_testing.resolver.SingleSimResolver( active_sim.sim_info) if resolver(cls.gameplay_immediate_test): cls.satisfy() else: return for buff_ref in cls.buffs: active_sim.add_buff_from_op(buff_ref.buff_type, buff_reason=buff_ref.buff_reason) if cls.gameplay_test is not None: services.get_event_manager().register_tests( cls, [cls.gameplay_test]) if cls.commodities_to_solve: context = InteractionContext( active_sim, InteractionContext.SOURCE_SCRIPT_WITH_USER_INTENT, priority.Priority.High, bucket=InteractionBucketType.DEFAULT) for commodity in cls.commodities_to_solve: if not active_sim.queue.can_queue_visible_interaction(): break autonomy_request = autonomy.autonomy_request.AutonomyRequest( active_sim, autonomy_mode=autonomy.autonomy_modes.FullAutonomy, commodity_list=(commodity, ), context=context, consider_scores_of_zero=True, posture_behavior=AutonomyPostureBehavior.IGNORE_SI_STATE, distance_estimation_behavior= AutonomyDistanceEstimationBehavior. ALLOW_UNREACHABLE_LOCATIONS, allow_opportunity_cost=False, autonomy_mode_label_override='Tutorial') selected_interaction = services.autonomy_service( ).find_best_action(autonomy_request) AffordanceObjectPair.execute_interaction(selected_interaction) @classmethod def handle_event(cls, sim_info, event, resolver): if cls.gameplay_test is not None and resolver(cls.gameplay_test): cls.satisfy() @classmethod def satisfy(cls): op = distributor.ops.SetTutorialTipSatisfy(cls.guid64) distributor_instance = Distributor.instance() distributor_instance.add_op_with_no_owner(op) @classmethod def deactivate(cls): if cls.gameplay_test is not None: services.get_event_manager().unregister_tests( cls, (cls.gameplay_test, ))
class VampireNighttimeSituation(WalkbyLimitingTagsMixin, SituationComplexCommon): INSTANCE_TUNABLES = { 'vampire_job': sims4.tuning.tunable.TunableTuple( situation_job=SituationJob.TunableReference( description= '\n A reference to the SituationJob used for the Sim performing the\n vampire nighttime situation.\n ' ), arrival_state=_VampireArrivalState.TunableFactory( description= '\n The state for telling a Sim to go and to the active lot from\n the walkby spawn point.\n ' ), break_in_state=_BreakInState.TunableFactory( description= '\n The state for pushing the Sim into the house depending on the\n active powers that it has.\n ' ), bite_state=_BiteState.TunableFactory( description= '\n The state for forcing the bite interaction on the chosen\n target Sim.\n ' ), leave_startled_state=_LeaveStartledState.TunableFactory( description= '\n The state for forcing the the vampire to leave.\n ' ), tuning_group=GroupNames.SITUATION), 'vampire_discovered_buffs': TunableList( description= "\n Buff's that will push the vampire to leave the situation since \n it's been discovered by the household owners or by an anti vampire\n object.\n ", tunable=TunableBuffReference( description= '\n Buff to make the vampire enter its discovered state.\n ', pack_safe=True)), 'sleep_category_tag': TunableEnumWithFilter( description= '\n These tag values are used for testing interactions.\n ', tunable_type=Tag, default=Tag.INVALID, invalid_enums=(tag.Tag.INVALID, ), filter_prefixes=('Interaction', )), 'power_buck_type': TunableEnumEntry( description= '\n Type of buck type for the vampire powers to be enabled for the \n vampire trying to enter into the visiting household.\n ', tunable_type=BucksType, default=BucksType.INVALID), 'active_powers': TunableList( description= '\n A list of Perks and buff to add if the perk is unlocked whenever\n the vampire decides to enter the household.\n ', tunable=TunableTuple( description= '\n Tuple of perk and buff powers.\n ', power=TunableReference(manager=services.get_instance_manager( sims4.resources.Types.BUCKS_PERK)), buff_to_add=TunableBuffReference( description= '\n Temporary buff to add for the specified power to the Sim \n while doing the break in.\n ' ))), 'save_lock_tooltip': TunableLocalizedString( description= '\n The tooltip/message to show when the player tries to save the game\n while this situation is running. Save is locked when situation starts.\n ', tuning_group=GroupNames.UI), 'household_sim_tests': TunableTestSet( description= '\n Tests to verify the Sims on the household can be valid targets\n for the nightime visit.\n ' ) } REMOVE_INSTANCE_TUNABLES = Situation.NON_USER_FACING_REMOVE_INSTANCE_TUNABLES def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.selected_sim = None @classmethod def _states(cls): return (SituationStateData(1, _VampireArrivalState, factory=cls.vampire_job.arrival_state), SituationStateData(2, _BreakInState, factory=cls.vampire_job.break_in_state), SituationStateData(3, _BiteState, factory=cls.vampire_job.bite_state), SituationStateData(4, _LeaveState), SituationStateData( 5, _LeaveStartledState, factory=cls.vampire_job.leave_startled_state)) @classmethod def _get_tuned_job_and_default_role_state_tuples(cls): return [(cls.vampire_job.situation_job, cls.vampire_job.arrival_state)] @classmethod def default_job(cls): return cls.vampire_job.situation_job @classmethod def get_sims_expected_to_be_in_situation(cls): return 1 def vampire_sim(self): sim = next(self.all_sims_in_job_gen(self.default_job()), None) return sim def _destroy(self): for sim in services.active_household().instanced_sims_gen(): for si in sim.si_state: if self.sleep_category_tag in si.affordance.interaction_category_tags: si.remove_liability(UNCANCELABLE_LIABILITY) super()._destroy() services.get_persistence_service().unlock_save(self) def register_selected_sim(self, selected_sim): self.selected_sim = selected_sim def start_situation(self): super().start_situation() self._change_state(self.vampire_job.arrival_state()) def lock_save(self): services.get_persistence_service().lock_save(self) def get_lock_save_reason(self): return self.save_lock_tooltip
class InfectedSituation(SubSituationOwnerMixin, SituationComplexCommon): INSTANCE_TUNABLES = {'infected_state': _InfectedState.TunableFactory(display_name='Infected State', tuning_group=SituationComplexCommon.SITUATION_STATE_GROUP), 'possessed_start_time': TunableTimeOfDay(description='\n Time of day Sims become possessed.\n ', tuning_group=GroupNames.SITUATION), 'possessed_duration_hours': TestedSum.TunableFactory(description='\n How long the possession lasts.\n ', tuning_group=GroupNames.SITUATION), 'possessed_situation': Situation.TunableReference(description='\n Possessed situation to place Sim in.\n ', class_restrictions=('PossessedSituation',), tuning_group=GroupNames.SITUATION), 'default_job_and_role': TunableSituationJobAndRoleState(description='\n The job/role the infected Sim will be in.\n ', tuning_group=GroupNames.SITUATION), 'possessed_buff_tag': TunableTag(description='\n Tag for buffs that can add the Possessed Mood through the Infection\n System. Possessed status is refreshed when these buffs are added\n or removed.\n ', filter_prefixes=('Buff',)), 'possessed_buff_no_animate_tag': TunableTag(description='\n Possession buffs with this tag will not play the start possession\n animation.\n ', filter_prefixes=('Buff',)), 'possession_time_buff': TunableBuffReference(description='\n The buff to add to the Sim when it is the possessed start time.\n ')} REMOVE_INSTANCE_TUNABLES = Situation.NON_USER_FACING_REMOVE_INSTANCE_TUNABLES def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._start_possession_alarm = None self._end_possession_alarm = None self._possession_sources = [] @classmethod def _states(cls): return (SituationStateData.from_auto_factory(0, cls.infected_state),) @classmethod def _get_tuned_job_and_default_role_state_tuples(cls): return ((cls.default_job_and_role.job, cls.default_job_and_role.role_state),) @classmethod def default_job(cls): pass @classproperty def situation_serialization_option(cls): return SituationSerializationOption.DONT def start_situation(self): super().start_situation() self._change_state(self.infected_state()) sim_info = self._get_sim_info() for buff in sim_info.Buffs.get_all_buffs_with_tag(self.possessed_buff_tag): self._request_possession(buff.buff_type, animate_possession_override=False) now = services.time_service().sim_now time_span_day = create_time_span(days=1) start_day_time = self._get_possessed_start_day_time() time_span = now.time_till_next_day_time(start_day_time) self._start_possession_alarm = alarms.add_alarm(self, time_span, self._on_possession_start, repeating_time_span=time_span_day, repeating=True) end_day_time = self._get_possessed_end_day_time() sim_info.Buffs.on_buff_added.register(self._on_buff_added) sim_info.Buffs.on_buff_removed.register(self._on_buff_removed) in_possession_window = now.time_between_day_times(start_day_time, end_day_time) if in_possession_window: elapsed_time = create_time_span(days=1) - now.time_till_next_day_time(start_day_time) self._trigger_possession_time(elapsed_time=elapsed_time) def on_remove(self): sim_info = self._get_sim_info() sim_info.Buffs.on_buff_added.unregister(self._on_buff_added) sim_info.Buffs.on_buff_removed.unregister(self._on_buff_removed) super().on_remove() @property def sim_id(self): return self._guest_list.host_sim_id def _get_sim_info(self): sim_info = services.sim_info_manager().get(self.sim_id) return sim_info def _get_possessed_start_day_time(self): return self.possessed_start_time def _get_possessed_end_day_time(self): sim_info = self._get_sim_info() if sim_info is None: logger.error('Missing SimInfo for infected sim') return start_day_time = self._get_possessed_start_day_time() resolver = SingleSimResolver(sim_info) hours = self.possessed_duration_hours.get_modified_value(resolver) end_day_time = start_day_time + create_time_span(hours=hours) return end_day_time def _on_possession_start(self, _): self._trigger_possession_time() def _trigger_possession_time(self, elapsed_time=None): sim_info = self._get_sim_info() if sim_info is None: logger.error('Missing SimInfo for infected sim') return possession_buff = self.possession_time_buff sim_info.add_buff_from_op(possession_buff.buff_type, possession_buff.buff_reason) buff_commodity = sim_info.get_statistic(possession_buff.buff_type.commodity, add=False) if buff_commodity: resolver = SingleSimResolver(sim_info) hours = self.possessed_duration_hours.get_modified_value(resolver) buff_time = hours*60 if elapsed_time is not None: buff_time -= elapsed_time.in_minutes() buff_commodity.set_value(buff_time) def _on_sub_situation_end(self, sub_situation_id): if services.current_zone().is_zone_shutting_down: return if self._possession_sources: sim_info = self._get_sim_info() for source in tuple(self._possession_sources): sim_info.remove_buff_by_type(source) def _start_possession_situation(self, animate_possession_override=None): guest_list = SituationGuestList(invite_only=True, host_sim_id=self.sim_id) animate_possession = services.current_zone().is_zone_running if animate_possession: if animate_possession_override is not None: animate_possession = animate_possession_override self._create_sub_situation(self.possessed_situation, guest_list=guest_list, user_facing=False, animate_possession=animate_possession) def _on_possession_sources_changed(self): sub_situations = self._get_sub_situations() if sub_situations: sub_situations[0].on_possession_sources_changed() def _request_possession(self, source, animate_possession_override=None): if source in self._possession_sources: logger.error('Redundant source: {}', source) return self._possession_sources.append(source) if not self._sub_situation_ids: self._start_possession_situation(animate_possession_override=animate_possession_override) self._on_possession_sources_changed() def _remove_possession_request(self, source): if source not in self._possession_sources: logger.error('Missing source: {}', source) return self._possession_sources.remove(source) self._on_possession_sources_changed() def _on_buff_added(self, buff_type, owner_sim_id): if self.possessed_buff_tag not in buff_type.tags: return animate = None if self.possessed_buff_no_animate_tag in buff_type.tags: animate = False self._request_possession(buff_type, animate_possession_override=animate) def _on_buff_removed(self, buff_type, owner_sim_id): if self.possessed_buff_tag not in buff_type.tags: return self._remove_possession_request(buff_type) def get_possession_source(self): sim_info = self._get_sim_info() if sim_info is None: return (None, None) buff_component = sim_info.Buffs longest_source = None buff_duration = None for source in self._possession_sources: buff = buff_component.get_buff_by_type(source) if buff is None: continue buff_commodity = buff.get_commodity_instance() if buff_commodity is None: longest_source = buff buff_duration = None break buff_value = buff_commodity.get_value() if not buff_duration is None: if buff_value > buff_duration: buff_duration = buff_value longest_source = buff buff_duration = buff_value longest_source = buff return (longest_source, buff_duration)
class Situation(BaseSituation, HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(sims4.resources.Types.SITUATION)): @staticmethod def _verify_situation_level_tuning(instance_class, tunable_name, source, bronze, gold, silver, tin): gold_reward = gold.reward if gold_reward is not None and gold_reward.reward_description is None: logger.error('Situation "{}" has a Gold tier reward that has no Reward Description tuned. Bronze and Silver are optional, but Gold requires a description.', source, owner='asantos') INSTANCE_SUBCLASSES_ONLY = True NPC_HOSTED_SITUATION_AGE_WEIGHTING = TunableMapping(description='\n A map of ages to weights when determining which sim in the household\n will be selected to receive an invitation.\n ', key_name='age', key_type=TunableEnumEntry(description='\n The age of a possible invitee that will be mapped to a weight.\n ', tunable_type=Age, default=Age.ADULT), value_name='weight', value_type=TunableRange(description='\n The weight a sim of this age will be chosen to have an event run\n on them.\n ', tunable_type=int, default=1, minimum=1)) INSTANCE_TUNABLES = {'category': TunableEnumEntry(description='\n The Category that the Situation belongs to.\n ', tunable_type=SituationCategoryUid, default=SituationCategoryUid.DEFAULT, tuning_group=GroupNames.UI), 'load_open_street_situation_with_selectable_sim': Tunable(description='\n If the situation has selectable sims, set to True to ensure the\n situation can load from the open street, False otherwise.\n \n Note: The Serialization Option also determines save/load strategy.\n Check with GPE to verify the situation save/load behavior.\n ', tunable_type=bool, default=False), '_display_name': TunableLocalizedString(description='\n Display name for situation\n ', allow_none=True, tuning_group=GroupNames.UI), 'situation_description': TunableLocalizedString(description='\n Situation Description\n ', allow_none=True, tuning_group=GroupNames.UI), 'entitlement': OptionalTunable(description='\n If enabled, this situation is locked by an entitlement. Otherwise,\n this situation is available to all players.\n ', tunable=TunableEntitlement(description='\n Entitlement required to plan this event.\n ', tuning_group=GroupNames.UI)), '_default_job': TunableReference(description='\n The default job for Sims in this situation\n ', manager=services.situation_job_manager(), allow_none=True), '_resident_job': SituationJob.TunableReference(description='\n The job to assign to members of the host sims household.\n Make sure to use the in_family filter term in the filter\n of the job you reference here.\n It is okay if this tunable is None.\n ', allow_none=True), '_icon': TunableResourceKey(description='\n Icon to be displayed in the situation UI.\n ', resource_types=sims4.resources.CompoundTypes.IMAGE, default=None, allow_none=True, tuning_group=GroupNames.UI), 'calendar_icon': TunableIconAllPacks(description='\n Icon to be displayed in the calendar UI.\n ', allow_none=True, tuning_group=GroupNames.UI), 'calendar_alert_description': OptionalTunable(description='\n If tuned, there will be a calendar alert description.\n ', tunable=TunableLocalizedString(description='\n Description that shows up in the calendar alert.\n ')), '_level_data': TunableTuple(tin=TunableSituationLevel(description='\n Tuning for the Tin level of this situation. This level has\n a score delta of 0 as it is considered the default level\n of any situation.\n ', locked_args={'medal': SituationMedal.TIN, 'score_delta': 0}), bronze=TunableSituationLevel(description='\n Tuning for the Bronze level of this situation.\n ', locked_args={'medal': SituationMedal.BRONZE}), silver=TunableSituationLevel(description='\n Tuning for the Silver level of this situation.\n ', locked_args={'medal': SituationMedal.SILVER}), gold=TunableSituationLevel(description='\n Tuning for the Gold level of this situation.\n ', locked_args={'medal': SituationMedal.GOLD}), description='\n Tuning for the different situation levels and rewards that\n are associated with them.\n ', verify_tunable_callback=_verify_situation_level_tuning), 'job_display_ordering': OptionalTunable(description='\n An optional list of the jobs in the order you want them displayed\n in the Plan an Event UI.\n ', tunable=TunableList(tunable=TunableReference(manager=services.situation_job_manager())), tuning_group=GroupNames.UI), 'recommended_job_object_notification': ui.ui_dialog_notification.UiDialogNotification.TunableFactory(description='\n The notification that is displayed when one or more recommended objects\n for a job are missing.\n ', locked_args={'text': None}), 'recommended_job_object_text': TunableLocalizedStringFactory(description='\n The text of the notification that is displayed when one or more recommended\n objects for a job are missing.\n \n The localization tokens for the Text field are:\n {0.String} = bulleted list of strings for the missing objects\n ', allow_none=True), '_buff': TunableBuffReference(description='\n Buff that will get added to sim when commodity is at this\n current state.\n ', allow_none=True), '_cost': Tunable(description='\n The cost of this situation\n ', tunable_type=int, default=0), 'exclusivity': TunableEnumEntry(description='\n Defines the exclusivity category for the situation which is used to prevent sims assigned\n to this situation from being assigned to situations from categories excluded by this\n category and vice versa.\n ', tunable_type=situations.bouncer.bouncer_types.BouncerExclusivityCategory, default=situations.bouncer.bouncer_types.BouncerExclusivityCategory.NORMAL), 'main_goal': TunableReference(description='The main goal of the situation. e.g. Get Married.', manager=services.get_instance_manager(sims4.resources.Types.SITUATION_GOAL), allow_none=True, tuning_group=GroupNames.GOALS), 'main_goal_audio_sting': TunableResourceKey(description='\n The sound to play when the main goal of this situation\n completes.\n ', default=None, resource_types=(sims4.resources.Types.PROPX,), tuning_group=GroupNames.AUDIO), '_main_goal_visibility_test': OptionalTunable(description='\n If enabled then the main goal of the situation will not be\n visible until this test passes. If the state of this test no\n longer becomes true then the main gaol will not become\n invisible again.\n \n Ex. A hospital emergency starting triggers the visiblity of the\n main goal within the active career event situation.\n \n IMPORTANT: The nature of this test can cause performance\n problems.\n ', tunable=TunableMainGoalVisibilityTestVariant(), tuning_group=GroupNames.GOALS), 'minor_goal_chains': TunableList(description='\n A list of goal sets, each one starting a chain of goal sets, for selecting minor goals.\n The list is in priority order, first being the most important.\n At most one goal will be selected from each chain.\n ', tunable=situations.situation_goal_set.SituationGoalSet.TunableReference(), tuning_group=GroupNames.GOALS), 'force_invite_only': Tunable(description='\n If True, the situation is invite only. Otherwise, it is not.\n For a date situation, this would be set to True.\n ', tunable_type=bool, default=False), 'creation_ui_option': TunableEnumEntry(description='\n Determines if the situation can be created from the Plan Event\n UI triggered from the phone.\n \n NOT_AVAILABLE - situation is not available in the creation UI.\n \n AVAILABLE - situation is available in the creation UI.\n \n DEBUG_AVAILABLE - situation is only available in the UI if\n you have used the |situations.allow_debug_situations command\n \n SPECIFIED_ONLY - situation is available in the creation UI if\n that UI is tuned to only look at a subset of situations.\n ', tunable_type=SituationCreationUIOption, default=SituationCreationUIOption.AVAILABLE, tuning_group=GroupNames.UI), 'audio_sting_on_start': TunableResourceKey(description='\n The sound to play when the Situation starts.\n ', default=None, resource_types=(sims4.resources.Types.PROPX,), tuning_group=GroupNames.AUDIO), 'background_audio': OptionalTunable(description='\n If enabled then we will play audio in the background while this\n user facing situation is running.\n ', tunable=TunableResourceKey(description='\n Audio that will play throughout the situation in the background\n and will end at the end of the situation.\n ', default=None, resource_types=(sims4.resources.Types.PROPX,)), tuning_group=GroupNames.AUDIO), 'duration': TunableSimMinute(description='\n How long the situation will last in sim minutes. 0 means forever.\n ', default=60), 'duration_randomizer': TunableSimMinute(description="\n A random time between 0 and this tuned time will be added to the\n situation's duration.\n ", default=0, minimum=0), 'max_participants': Tunable(description='\n Maximum number of Sims the player is allowed to invite to this Situation.\n ', tunable_type=int, default=16, tuning_group=GroupNames.UI), '_initiating_sim_tests': TunableSituationInitiationSet('\n A set of tests that will be run on a sim attempting to initiate a\n situation. If these tests do not pass than this situation will not\n be able to be chosen from the UI.\n '), 'targeted_situation': OptionalTunable(description='\n If enabled, the situation can be used as a targeted situation,\n such as a Date.\n ', tunable=TargetedSituationSpecific.TunableFactory()), 'compatible_venues': TunableList(description='\n In the Plan an Event UI, lots that are these venues will be\n added to the list of lots on which the player can throw the\n event. The player can always choose their own lot and lots of\n their guests.\n ', tunable=TunableReference(manager=services.get_instance_manager(sims4.resources.Types.VENUE), pack_safe=True, tuning_group=GroupNames.VENUES)), 'venue_region_must_be_compatible': Tunable(description='\n If enabled, venues will only be considered if they are in a\n region that is compatible with the current region (regions with\n at least one shared tag).\n ', tunable_type=bool, default=False), 'venue_invitation_message': OptionalTunable(description='\n If enabled, show a dialog when the situation tries to start on a\n venue.\n ', tunable=UiDialogOkCancel.TunableFactory(description="\n The message that will be displayed when this situation tries to\n start for the venue.\n \n Two additional tokens are passed in: the situation's name and\n the job's name.\n "), tuning_group=GroupNames.VENUES), 'venue_situation_player_job': TunableReference(description="\n The job that the player will be put into when they join in a\n user_facing special situation at a venue.\n \n Note: This must be tuned to allow this situation to be in a\n venue's special event schedule. The job also must be a part of\n the Situation.\n ", manager=services.get_instance_manager(sims4.resources.Types.SITUATION_JOB), allow_none=True, tuning_group=GroupNames.VENUES), 'tags': TunableSet(description='\n Tags for arbitrary groupings of situation types.\n ', tunable=TunableEnumWithFilter(tunable_type=Tag, filter_prefixes=['situation'], default=Tag.INVALID, pack_safe=True)), '_relationship_between_job_members': TunableList(description="\n Whenever a sim joins either job_x or job_y, the sim is granted \n the tuned relationship bit with every sim in the other job. The\n relationship bits are added and remain as long as the sims are\n assigned to the tuned pair of jobs.\n \n This creates a relationship between the two sims if one does not exist.\n \n E.g. Date situation uses this feature to add bits to the sims'\n relationship in order to drive autonomous behavior during the \n lifetime of the date. \n ", tunable=TunableTuple(job_x=SituationJob.TunableReference(), job_y=SituationJob.TunableReference(), relationship_bits_to_add=TunableSet(description='\n A set of RelationshipBits to add to relationship between the sims.\n ', tunable=RelationshipBit.TunableReference())), tuning_group=GroupNames.TRIGGERS), '_implies_greeted_status': Tunable(description='\n If checked then a sim, in this situation, on a residential lot\n they do not own, is consider greeted on that lot.\n \n Greeted status related design documents:\n //depot/Sims4Projects/docs/Design/Gameplay/HouseholdState/Ungreeted_Lot_Behavior_DD.docx\n //depot/Sims4Projects/docs/Design/Gameplay/Simulation/Active Lot Changing Edge Cases.docx\n ', tunable_type=bool, default=False), 'screen_slam_no_medal': OptionalTunable(description='\n Screen slam to show when this situation is completed and no\n medal is earned.\n Localization Tokens: Event Name - {0.String}, Medal Awarded - \n {1.String}\n ', tunable=ui.screen_slam.TunableScreenSlamSnippet()), 'screen_slam_bronze': OptionalTunable(description='\n Screen slam to show when this situation is completed and a\n bronze medal is earned.\n Localization Tokens: Event Name - {0.String}, Medal Awarded - \n {1.String}\n ', tunable=ui.screen_slam.TunableScreenSlamSnippet()), 'screen_slam_silver': OptionalTunable(description='\n Screen slam to show when this situation is completed and a\n silver medal is earned.\n Localization Tokens: Event Name - {0.String}, Medal Awarded - \n {1.String}\n ', tunable=ui.screen_slam.TunableScreenSlamSnippet()), 'screen_slam_gold': OptionalTunable(description='\n Screen slam to show when this situation is completed and a\n bronze medal is earned.\n Localization Tokens: Event Name - {0.String}, Medal Awarded - \n {1.String}\n ', tunable=ui.screen_slam.TunableScreenSlamSnippet()), 'time_jump': TunableSituationTimeJumpVariant(description='\n Determine how the situation handles the zone time being different on\n load than what it was on save. This is primarily useful for\n commercial venue background situations and career event situations.\n ', tuning_group=GroupNames.SPECIAL_CASES), 'can_remove_sims_from_work': Tunable(description='\n If checked then this situation will cause sims to end work early\n when they are on the guest list. If unchecked, it will not. This\n option will not affect active career situations or NPC career\n situations like tending the bar.\n ', tunable_type=bool, default=True, tuning_group=GroupNames.SPECIAL_CASES), '_survives_active_household_change': Tunable(description='\n If checked then this situation will load even if the active\n household has changed since it was saved. It will attempt to\n restore Sims to their saved jobs. This is primarily useful for\n commercial venue background situations.\n ', tunable_type=bool, default=False, tuning_group=GroupNames.SPECIAL_CASES), '_maintain_sims_consistency': Tunable(description="\n If checked, Sims in the saved situation that were pushed home \n because they had been saved in the zone for many Sim hours will \n be back. Otherwise, we will find replacement.\n \n Ex. We don't want to replace Butler with new Sim if previous\n Butler is no longer in the lot.\n ", tunable_type=bool, default=False, tuning_group=GroupNames.SPECIAL_CASES), '_hidden_scoring_override': Tunable(description='\n If checked then even if this situation has its scoring disabled it\n still will count score and provide rewards to the player.\n ', tunable_type=bool, default=False, tuning_group=GroupNames.SPECIAL_CASES), '_ensemble': OptionalTunable(description='\n If enabled then we will keep Sims in a specific ensemble for the\n duration of the situation.\n ', tunable=TunableTuple(description='\n Tunables for putting Sims into ensembles.\n ', ensemble_type=TunableReference(description='\n The type of Ensemble to put the sims into.\n ', manager=services.get_instance_manager(sims4.resources.Types.ENSEMBLE), pack_safe=True), remove_before_add=Tunable(description='\n If checked then before we add the Sim to the ensemble we\n will remove them from from ensembles of the specified type.\n This can be used to force Sims into only an ensemble of\n Sims in this situation.\n ', tunable_type=bool, default=False), ignore_situation_removal=Tunable(description='\n If checked then we will not remove the Sim from the\n ensemble of this type when the Sim is removed from the\n situation.\n ', tunable_type=bool, default=True), ensemble_option=TunableEnumEntry(description='\n How we want to add Sims to an ensemble:\n ONLY_WITHIN_SITUATION: Put the Sims in this situation into\n an ensemble of this type. Every time a sim is added we\n try and do this so if the user destroys the ensemble and\n then another Sim is spawned for it the ensemble will be\n recreated.\n \n ADD_TO_ACTIVE_HOUSEHOLD: Every time a Sim is spawned for\n this situation they are put into an ensemble with the\n instanced active household. This is useful if you want to\n put the Sims in a situation with someone who is not in it. \n \n ADD_TO_HOST: Every time a Sim is spawned for this situation\n they are put into an ensemble with the host of the\n situation. This is useful if you want to put the Sims in\n a situation with someone who is not in it.\n ', tunable_type=EnsembleOption, default=EnsembleOption.ONLY_WITHIN_SITUATION)), tuning_group=GroupNames.SPECIAL_CASES), 'blocks_super_speed_three': Tunable(description='\n If enabled, this situation will block any requests to go into super\n speed 3.\n ', tunable_type=bool, default=False), 'travel_request_behavior': TunableSituationTravelRequestBehaviorVariant(description='\n Define how this situation handles incoming travel requests from\n other situations when running as user-facing.\n '), 'allowed_in_super_speed_3': Tunable(description="\n If enabled, this situation will skip the super speed 3 rules and\n be allowed to trigger at that speed.\n This will only affect walkby's as they are the only restricted\n by speed 3.\n ", tunable_type=bool, default=False), 'should_send_on_lot_home_in_super_speed_3': Tunable(description='\n If enabled, on_lot sims in this situation will not prevent SS3. If\n SS3 is triggered they will be sent home.\n ', tunable_type=bool, default=False), 'super_speed3_replacement_speed': OptionalTunable(description='\n If enabled and this situation blocks super speed 3, the situation will attempt to request\n this speed if it is running when super speed 3 tries to kick in.\n ', tunable=TunableEnumEntry(tunable_type=ClockSpeedMode, invalid_enums=(ClockSpeedMode.PAUSED, ClockSpeedMode.INTERACTION_STARTUP_SPEED, ClockSpeedMode.SUPER_SPEED3), default=ClockSpeedMode.SPEED3)), 'weight_multipliers': TunableMultiplier.TunableFactory(description="\n Tunable tested multiplier to apply to any weight this situation\n might have as part of a Situation Curve. These multipliers will be\n applied globally anywhere this situation is tuned as part of a\n situation curve (i.e. Walkby Tuning) so it should only be used in\n cases where you ALWAYS want this multiplier applied.\n \n NOTE: You can still tune more multipliers on the individual walk by\n instances. The multipliers will all stack together.\n \n *IMPORTANT* The only participants that work are ones\n available globally, such as Lot and ActiveHousehold. Only\n use these participant types or use tests that don't rely\n on any, such as testing all objects via Object Criteria\n test or testing active zone with the Zone test.\n ", locked_args={'base_value': 1}), 'disallows_curfew_violation': Tunable(description='\n If this is checked then the Sim is unable to violate curfew while\n in the situation. If this is not checked then the Sim can vioalte\n curfew as normal.\n ', tunable_type=bool, default=False), 'suppress_scoring_progress_bar': Tunable(description='\n If this is checked, UI will no longer show the scoring progress bar\n and instead show the situation name in its stead.\n ', tunable_type=bool, default=False, tuning_group=GroupNames.UI), 'highlight_first_incomplete_minor_goal': Tunable(description='\n If this is checked, we will tell user-facing Situation UI \n to highlight the first uncompleted minor goal set.\n \n Note that gameplay currently does not guard against being able \n to complete the other goal sets in the situation currently, \n so the goalsets should be tuned in such a manner \n that they do not overlap.\n ', tunable_type=bool, default=False, tuning_group=GroupNames.UI), '_use_spawner_tags_on_travel': Tunable(description='\n If checked the situation will spawn sims according to its job spawner tags\n instead of always defaulting to the Arrival spawner.\n ', tunable_type=bool, default=False)} SITUATION_SCORING_REMOVE_INSTANCE_TUNABLES = ('main_goal', '_main_goal_visibility_test', 'minor_goal_chains', 'main_goal_audio_sting', 'highlight_first_incomplete_minor_goal', 'suppress_scoring_progress_bar', '_level_data', 'screen_slam_gold', 'screen_slam_silver', 'screen_slam_bronze', 'screen_slam_no_medal') SITUATION_START_FROM_UI_REMOVE_INSTANCE_TUNABLES = ('_cost', 'compatible_venues', 'venue_invitation_message', 'venue_situation_player_job', 'category', 'max_participants', '_initiating_sim_tests', '_icon', 'entitlement', 'job_display_ordering') SITUATION_USER_FACING_REMOVE_INSTANCE_TUNABLES = ('_display_name', 'travel_request_behavior', 'recommended_job_object_notification', 'recommended_job_object_text', 'situation_description') NON_USER_FACING_REMOVE_INSTANCE_TUNABLES = ('_buff', 'targeted_situation', '_resident_job', '_relationship_between_job_members', 'audio_sting_on_start', 'background_audio', 'force_invite_only') + SITUATION_SCORING_REMOVE_INSTANCE_TUNABLES + SITUATION_START_FROM_UI_REMOVE_INSTANCE_TUNABLES + SITUATION_USER_FACING_REMOVE_INSTANCE_TUNABLES SITUATION_EVENT_REMOVE_INSTANCE_TUNABLES = ('_buff', '_cost', 'venue_invitation_message', 'venue_situation_player_job', 'category', 'main_goal', '_main_goal_visibility_test', 'minor_goal_chains', 'highlight_first_incomplete_minor_goal', 'suppress_scoring_progress_bar', 'max_participants', '_initiating_sim_tests', '_icon', 'targeted_situation', '_resident_job', 'situation_description', 'job_display_ordering', 'entitlement', '_relationship_between_job_members', 'main_goal_audio_sting', 'audio_sting_on_start', 'background_audio', '_level_data', '_display_name', 'screen_slam_gold', 'screen_slam_silver', 'screen_slam_bronze', 'screen_slam_no_medal', 'force_invite_only', 'recommended_job_object_notification', 'recommended_job_object_text', 'travel_request_behavior') situation_level_data = None SituationLevel = collections.namedtuple('SituationLevel', ['min_score_threshold', 'level_data']) @classmethod def _tuning_loaded_callback(cls): cls.situation_level_data = [] current_score = cls._level_data.tin.score_delta cls.situation_level_data.append(Situation.SituationLevel(current_score, cls._level_data.tin)) current_score += cls._level_data.bronze.score_delta cls.situation_level_data.append(Situation.SituationLevel(current_score, cls._level_data.bronze)) current_score += cls._level_data.silver.score_delta cls.situation_level_data.append(Situation.SituationLevel(current_score, cls._level_data.silver)) current_score += cls._level_data.gold.score_delta cls.situation_level_data.append(Situation.SituationLevel(current_score, cls._level_data.gold)) @classmethod def _verify_tuning_callback(cls): if cls._resident_job is not None and cls._resident_job.filter is None: logger.error('Resident Job: {} has no filter,', cls._resident_job, owner='manus') if cls.targeted_situation is not None and (cls.targeted_situation.target_job is None or cls.targeted_situation.actor_job is None): logger.error('target_job and actor_job are required if targeted_situation is enabled.', owner='manus') tuned_jobs = frozenset(cls.get_tuned_jobs()) for job_relationships in cls.relationship_between_job_members: if job_relationships.job_x not in tuned_jobs: logger.error('job_x: {} has relationship tuning but is not functionally used in situation {}.', job_relationships.job_x, cls, owner='manus') if job_relationships.job_y not in tuned_jobs: logger.error('job_y: {} has relationship tuning but is not functionally used in situation {}.', job_relationships.job_y, cls, owner='manus') if len(job_relationships.relationship_bits_to_add) == 0: logger.error("relationship_bits_to_add cannot be empty for situation {}'s job pairs {} and {}.", cls, job_relationships.job_x, job_relationships.job_y, owner='manus') else: for bit in job_relationships.relationship_bits_to_add: if bit is None: logger.error("relationship_bits_to_add cannot contain empty bit for situation {}'s job pairs {} and {}.", cls, job_relationships.job_x, job_relationships.job_y, owner='manus') def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._duration_alarm_handle = None self._goal_tracker = None self._dynamic_goals = self._seed.extra_kwargs.get('dynamic_goals', None) @classproperty def allow_user_facing_goals(cls): return cls.main_goal is not None or len(cls.minor_goal_chains) > 0 @classmethod def level_data_gen(cls): for level in cls.situation_level_data: yield level @classmethod def fake_perform_job(cls): pass @classmethod def get_level_data(cls, medal:SituationMedal=SituationMedal.TIN): if cls.situation_level_data is None: return return cls.situation_level_data[medal].level_data @classmethod def get_level_min_threshold(cls, medal:SituationMedal=SituationMedal.TIN): if cls.situation_level_data is None: return return cls.situation_level_data[medal].min_score_threshold @classmethod def get_level_icon(cls, medal:SituationMedal=SituationMedal.TIN): if cls.situation_level_data is None: return return cls.situation_level_data[medal].level_data.icon @classmethod def get_possible_zone_ids_for_situation(cls, host_sim_info=None, guest_ids=None): possible_zones = [] venue_manager = services.get_instance_manager(sims4.resources.Types.VENUE) venue_service = services.current_zone().venue_service for venue_tuning in cls.compatible_venues: if venue_tuning.is_residential: if host_sim_info is not None: home_zone_id = host_sim_info.household.home_zone_id home_venue_tuning = venue_manager.get(build_buy.get_current_venue(home_zone_id)) if home_venue_tuning.is_residential: possible_zones.append(home_zone_id) if guest_ids is not None: for guest_id in guest_ids: guest_id = int(guest_id) guest_info = services.sim_info_manager().get(guest_id) if guest_info is not None: guest_zone_id = guest_info.household.home_zone_id if guest_zone_id is not None and guest_zone_id and guest_zone_id not in possible_zones: guest_venue_tuning = venue_manager.get(build_buy.get_current_venue(guest_zone_id)) if guest_venue_tuning.is_residential: possible_zones.append(guest_zone_id) travel_group = guest_info.travel_group if travel_group is not None: travel_group_zone_id = travel_group.zone_id if travel_group_zone_id is not None and travel_group_zone_id and travel_group_zone_id not in possible_zones: possible_zones.append(travel_group_zone_id) else: possible_zones.extend(venue_service.get_zones_for_venue_type_gen(venue_tuning)) else: possible_zones.extend(venue_service.get_zones_for_venue_type_gen(venue_tuning)) return possible_zones @classmethod def default_job(cls): return cls._default_job @classmethod def resident_job(cls): return cls._resident_job @classmethod def get_prepopulated_job_for_sims(cls, sim, target_sim_id=None): if target_sim_id and cls.targeted_situation is not None: sim_info = services.sim_info_manager().get(target_sim_id) if sim_info is None: return else: prepopulated = [(sim.id, cls.targeted_situation.actor_job.guid64), (target_sim_id, cls.targeted_situation.target_job.guid64)] return prepopulated def _display_role_objects_notification(self, sim, bullets): text = self.recommended_job_object_text(bullets) notification = self.recommended_job_object_notification(sim, text=lambda *_, **__: text) notification.show_dialog() @property def pie_menu_icon(self): return self._pie_menu_icon @classproperty def display_name(self): return self._display_name @property def description(self): return self.situation_description @classproperty def icon(self): return self._icon @property def start_audio_sting(self): return self.audio_sting_on_start @property def audio_background(self): return self.background_audio def get_target_object(self): pass def get_created_object(self): pass @property def end_audio_sting(self): current_level = self.get_level() level_data = self.get_level_data(current_level) if level_data is not None and level_data.audio_sting_on_end is not None: return level_data.audio_sting_on_end else: return @classproperty def relationship_between_job_members(cls): return cls._relationship_between_job_members @classproperty def implies_greeted_status(cls): return cls._implies_greeted_status @classmethod def cost(cls): return cls._cost @classproperty def survives_active_household_change(cls): return cls._survives_active_household_change @classproperty def maintain_sims_consistency(cls): return cls._maintain_sims_consistency def _get_duration(self): if self._seed.duration_override is not None: return self._seed.duration_override return self.duration + random.randint(0, self.duration_randomizer) def _get_remaining_time(self): if self._duration_alarm_handle is None: return return self._duration_alarm_handle.get_remaining_time() def _get_remaining_time_for_gsi(self): return self._get_remaining_time() def _get_remaining_time_in_minutes(self): time_span = self._get_remaining_time() if time_span is None: return 0 return time_span.in_minutes() def _get_goal_tracker(self): return self._goal_tracker def _save_custom(self, seed): super()._save_custom(seed) if self._goal_tracker is not None: self._goal_tracker.save_to_seed(seed) def start_situation(self): super().start_situation() self._set_duration_alarm() if self.is_user_facing: if self.should_track_score: if self._dynamic_goals is None: self._goal_tracker = situations.situation_goal_tracker.SituationGoalTracker(self) else: self._goal_tracker = situations.dynamic_situation_goal_tracker.DynamicSituationGoalTracker(self) def _load_situation_states_and_phases(self): super()._load_situation_states_and_phases() self._set_duration_alarm() if not self._seed.goal_tracker_seedling: return if self._seed.goal_tracker_seedling.goal_tracker_type == GoalTrackerType.STANDARD_GOAL_TRACKER: self._goal_tracker = situations.situation_goal_tracker.SituationGoalTracker(self) elif self._seed.goal_tracker_seedling.goal_tracker_type == GoalTrackerType.DYNAMIC_GOAL_TRACKER: self._goal_tracker = situations.dynamic_situation_goal_tracker.DynamicSituationGoalTracker(self) def change_duration(self, duration): if not self.is_running: logger.error("Trying to change the duration of a situation {} that's not running.", self) self._set_duration_alarm(duration_override=duration) if self.is_user_facing: self.add_situation_duration_change_op() def _set_duration_alarm(self, duration_override=None): if duration_override is not None: duration = duration_override else: duration = self._get_duration() self.set_end_time(duration) if duration > 0: if self._duration_alarm_handle is not None: alarms.cancel_alarm(self._duration_alarm_handle) self._duration_alarm_handle = alarms.add_alarm(self, clock.interval_in_sim_minutes(duration), self._situation_timed_out) def _cancel_duration_alarm(self): if self.is_user_facing: logger.error('Canceling duration alarm for a User-Facing Situation {}', self, owner='rmccord') if self._duration_alarm_handle is not None: alarms.cancel_alarm(self._duration_alarm_handle) def pre_destroy(self): pass def _destroy(self): if self._duration_alarm_handle is not None: alarms.cancel_alarm(self._duration_alarm_handle) if self._goal_tracker is not None: self._goal_tracker.destroy() self._goal_tracker = None super()._destroy() def _situation_timed_out(self, _): logger.debug('Situation time expired: {}', self) self._self_destruct() @classmethod def is_situation_available(cls, initiating_sim, target_sim_id=0): is_targeted = cls.targeted_situation is not None and cls.targeted_situation.target_job is not None if is_targeted and target_sim_id: if not cls.targeted_situation.target_job.can_sim_be_given_job(target_sim_id, initiating_sim.sim_info): return TestResult(False) elif (target_sim_id == 0) != (is_targeted == False): return TestResult(False) single_sim_resolver = event_testing.resolver.SingleSimResolver(initiating_sim.sim_info) return cls._initiating_sim_tests.run_tests(single_sim_resolver) @classmethod def get_predefined_guest_list(cls): pass @classmethod def is_venue_location_valid(cls, zone_id): compatible_region = services.current_region() if cls.venue_region_must_be_compatible else None return services.current_zone().venue_service.is_zone_valid_for_venue_type(zone_id, cls.compatible_venues, compatible_region=compatible_region) @classmethod def get_venue_location(cls): compatible_region = services.current_region() if cls.venue_region_must_be_compatible else None (zone_id, _) = services.current_zone().venue_service.get_zone_and_venue_type_for_venue_types(cls.compatible_venues, compatible_region=compatible_region) return zone_id @classmethod def has_venue_location(cls): compatible_region = services.current_region() if cls.venue_region_must_be_compatible else None return services.current_zone().venue_service.has_zone_for_venue_type(cls.compatible_venues, compatible_region=compatible_region) @classproperty def main_goal_visibility_test(cls): return cls._main_goal_visibility_test @classproperty def _ensemble_data(cls): return cls._ensemble @property def should_track_score(self): return self.scoring_enabled or self._hidden_scoring_override @property def should_give_rewards(self): return self.scoring_enabled or self._hidden_scoring_override def is_in_joinable_state(self): return True @property def custom_event_keys(self): return [type(self)] + list(self.tags) @classproperty def use_spawner_tags_on_travel(cls): return cls._use_spawner_tags_on_travel
class Buff(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(sims4.resources.Types.BUFF)): __qualname__ = 'Buff' INSTANCE_TUNABLES = {'buff_name': TunableLocalizedString(description='\n Name of buff.\n ', tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'buff_description': TunableLocalizedString(description='\n Tooltip description of the Buff Effect.\n ', tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'icon': TunableResourceKey(description='\n Icon to be displayed for buff\n ', default=None, needs_tuning=True, resource_types=sims4.resources.CompoundTypes.IMAGE, tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'icon_highlight': TunableResourceKey(description=" \n Icon to be displayed for when Mood Type is the Sim's active mood.\n ", default=None, resource_types=sims4.resources.CompoundTypes.IMAGE, tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'ui_sort_order': Tunable(description='\n Order buff should be sorted in UI.\n ', tunable_type=int, tuning_group=GroupNames.UI, default=1, export_modes=ExportModes.All), 'visible': Tunable(description='\n Whether this buff should be visible in the UI.\n ', tunable_type=bool, default=True, tuning_group=GroupNames.UI), 'audio_sting_on_remove': TunableResourceKey(description='\n The sound to play when this buff is removed.\n ', default=None, resource_types=(sims4.resources.Types.PROPX,), export_modes=ExportModes.All), 'audio_sting_on_add': TunableResourceKey(description='\n The sound to play when this buff is added.\n ', default=None, resource_types=(sims4.resources.Types.PROPX,), export_modes=ExportModes.All), 'show_timeout': Tunable(description='\n Whether timeout should be shown in the UI.\n ', tunable_type=bool, default=True, tuning_group=GroupNames.UI), 'success_modifier': Tunable(description='\n Base chance delta for interaction success\n ', tunable_type=int, default=0), 'interactions': OptionalTunable(TunableTuple(weight=Tunable(description='\n The selection weight to apply to all interactions added by this\n buff. This takes the place of the SI weight that would be used on\n SuperInteractions.\n ', tunable_type=float, default=1), scored_commodity=statistics.commodity.Commodity.TunableReference(description="\n The commodity that is scored when deciding whether or not to \n perform these interactions. This takes the place of the commodity\n scoring for the SuperInteraction when Subaction Autonomy scores\n all of the SI's in the SI State. If this is None, the default \n value of autonomy.autonomy_modes.SUBACTION_MOTIVE_UTILITY_FALLBACK_SCORE \n will be used.\n "), interaction_items=TunableAffordanceLinkList(description='\n Mixer interactions to add to the Sim when this buff is active.\n ', class_restrictions=(interactions.base.mixer_interaction.MixerInteraction,))), tuning_group=GroupNames.ANIMATION), 'topics': TunableList(description='\n Topics that should be added to sim when buff is added.\n ', tunable=TunableReference(manager=services.topic_manager(), class_restrictions=topics.topic.Topic)), 'game_effect_modifiers': GameEffectModifiers.TunableFactory(description="\n A list of effects that that can modify a Sim's behavior.\n "), 'mood_type': TunableReference(description='\n The mood that this buff pushes onto the owning Sim. If None, does\n not affect mood.\n ', manager=services.mood_manager(), needs_tuning=True, export_modes=ExportModes.All), 'mood_weight': TunableRange(description='\n Weight for this mood. The active mood is determined by summing all\n buffs and choosing the mood with the largest weight.\n ', tunable_type=int, default=0, minimum=0, export_modes=ExportModes.All), 'proximity_detection_tests': OptionalTunable(description="\n Whether or not this buff should be added because of a Sim's proximity\n to an object with a Proximity Component with this buff in its buffs\n list.\n ", tunable=event_testing.tests.TunableTestSet(description="\n A list of tests groups. At least one must pass all its sub-tests to\n pass the TestSet.\n \n Actor is the one who receives the buff.\n \n If this buff is for two Sims in proximity to each other, only Actor\n and TargetSim should be tuned as Participant Types. Example: A Neat\n Sim is disgusted when around a Sim that has low hygiene. The test\n will be for Actor having the Neat trait and for TargetSim with low\n hygiene motive.\n\n If this buff is for a Sim near an object, only use participant\n types Actor and Object. Example: A Sim who likes classical music\n should get a buff when near a stereo that's playing classical\n music. The test will be for Actor liking classical music and for\n Object in the state of playing classical music.\n "), enabled_by_default=False, disabled_name='no_proximity_detection', enabled_name='proximity_tests'), 'proximity_buff_added_reason': OptionalTunable(tunable=TunableLocalizedString(description="\n If this is a proximity buff, this field will be the reason for why\n the Sim received this buff. Doesn't use tokens.\n "), enabled_by_default=False, disabled_name='no_proximity_add_reason', enabled_name='proximity_add_reason'), '_add_test_set': OptionalTunable(description='\n Whether or not this buff should be added.\n ', tunable=event_testing.tests.TunableTestSet(description='\n A list of tests groups. At least one must pass all its sub-tests to\n pass the TestSet. Only Actor should be tuned as Participant\n Types.The Actor is the Sim that will receive the buff if all tests\n pass."\n '), enabled_by_default=False, disabled_name='always_allowed', enabled_name='tests_set'), 'walkstyle': OptionalTunable(TunableWalkstyle(description="\n A walkstyle override to apply to the Sim while this buff is active.\n Example: you can have Sims with the 'bummed' buff walk in a sad\n fashion.\n "), needs_tuning=True, tuning_group=GroupNames.ANIMATION), 'allow_running_for_long_distance_routes': Tunable(bool, True, description='\n Sims will run when routing long distances outside. Setting this to False\n will disable that functionality. Example: pregnant Sims and walk-by Sims\n should probably never run for this reason.'), 'vfx': OptionalTunable(description='\n vfx to play on the sim when buff is added.\n ', tunable=PlayEffect.TunableFactory(), disabled_name='no_effect', enabled_name='play_effect', tuning_group=GroupNames.ANIMATION), 'static_commodity_to_add': TunableSet(description='\n Static commodity that is added to the sim when buff is added to sim.\n ', tunable=TunableReference(manager=services.static_commodity_manager(), class_restrictions=statistics.static_commodity.StaticCommodity)), '_operating_commodity': statistics.commodity.Commodity.TunableReference(description='\n This is the commodity that is considered the owning commodity of the\n buff. Multiple commodities can reference the same buff. This field\n is used to determine which commodity is considered the authoritative\n commodity. This only needs to be filled if there are more than one\n commodity referencing this buff.\n \n For example, motive_hunger and starvation_commodity both reference\n the same buff. Starvation commodity is marked as the operating\n commodity. If outcome action asks the buff what commodity it should\n apply changes to it will modify the starvation commodity.\n '), '_temporary_commodity_info': OptionalTunable(TunableTuple(description='\n Tunables relating to the generation of a temporary commodity to control\n the lifetime of this buff. If enabled, this buff has no associated\n commodities and will create its own to manage its lifetime.\n ', max_duration=Tunable(description='\n The maximum time buff can last for. Example if set to 100, buff\n only last at max 100 sim minutes. If washing hands gives +10 sim\n minutes for buff. Trying to run interaction for more than 10 times,\n buff time will not increase\n ', tunable_type=int, default=100), categories=TunableSet(description='\n List of categories that this commodity is part of. Used for buff\n removal by category.\n ', tunable=StatisticCategory, needs_tuning=True))), '_appropriateness_tags': TunableSet(description='\n A set of tags that define the appropriateness of the\n interactions allowed by this buff. All SIs are allowed by\n default, so adding this tag generally implies that it is always\n allowed even if another buff has said that it is\n inappropriate.\n ', tunable=TunableEnumEntry(tunable_type=Tag, default=Tag.INVALID)), '_inappropriateness_tags': TunableSet(description="\n A set of tags that define the inappropriateness of the\n interactions allowed by this buff. All SIs are allowed by\n default, so adding this tag generally implies that it's not\n allowed.\n ", tunable=TunableEnumEntry(tunable_type=Tag, default=Tag.INVALID)), 'communicable': OptionalTunable(tunable=LootActions.TunableReference(description='\n The loot to give to Sims that this Sim interacts with while the buff is active.\n This models transmitting the buff, so make sure to tune a percentage chance\n on the loot action to determine the chance of the buff being transmitted.\n ')), '_add_buff_on_remove': OptionalTunable(tunable=TunableBuffReference(description='\n A buff to add to the Sim when this buff is removed.\n ')), '_loot_on_addition': TunableList(description='\n Loot that will be applied when buff is added to sim.\n ', tunable=LootActions.TunableReference()), '_loot_on_removal': TunableList(description='\n Loot that will be applied when buff is removed from sim.\n ', tunable=LootActions.TunableReference()), 'refresh_on_add': Tunable(description='\n This buff will have its duration refreshed if it gets added to a Sim\n who already has the same buff.\n ', tunable_type=bool, needs_tuning=True, default=True), 'flip_arrow_for_progress_update': Tunable(description='\n This only for visible buffs with an owning commodity.\n \n If unchecked and owning commodity is increasing an up arrow will\n appear on the buff and if owning commodity is decreasing a down arrow\n will appear.\n \n If checked and owning commodity is increasing then a down arrow will\n appear on the buff and if owning commodity is decreasing an up arrow\n will appear.\n \n Example of being checked is motive failing buffs, when the commodity is\n increasing we need to show down arrows for the buff.\n ', tunable_type=bool, default=False, tuning_group=GroupNames.UI), 'timeout_string': TunableLocalizedStringFactory(description='\n String to override the the timeout text. The first token (0.TimeSpan)\n will be the timeout time and the second token will (1.String) will be\n the buff this buff is transitioning to.\n \n If this buff is not transitioning to another buff the only token valid\n in string is 0.Timespan\n \n Example: If this is the hungry buff, then the commodity is decaying to\n starving buff. Normally timeout in tooltip will say \'5 hours\'. With\n this set it will pass in the next buff name as the first token into\n this localized string. So if string provided is \'Becomes {1.String}\n in: {0.TimeSpan}. Timeout tooltip for buff now says \'Becomes Starving\n in: 5 hours\'.\n \n Example: If buff is NOT transitioning into another buff. Localized\n string could be "Great time for :{0.Timespan}". Buff will now say\n "Great time for : 5 hours"\n ', tuning_group=GroupNames.UI, export_modes=(ExportModes.ClientBinary,))} is_mood_buff = False exclusive_index = None exclusive_weight = None trait_replacement_buffs = None _owning_commodity = None @classmethod def _verify_tuning_callback(cls): if cls.visible and not cls.mood_type: logger.error('No mood type set for visible buff: {}. Either provide a mood or make buff invisible.', cls, owner='Tuning') @classmethod def _tuning_loaded_callback(cls): if cls._temporary_commodity_info is not None: if cls._owning_commodity is None: cls._create_temporary_commodity() elif issubclass(cls._owning_commodity, RuntimeCommodity): cls._create_temporary_commodity(proxied_commodity=cls._owning_commodity) def __init__(self, owner, commodity_guid, replacing_buff_type, transition_into_buff_id): self._owner = owner self.commodity_guid = commodity_guid self.effect_modification = self.game_effect_modifiers(owner) self.buff_reason = None self.handle_ids = [] self._static_commodites_added = None self._replacing_buff_type = replacing_buff_type self._mood_override = None self._vfx = None self.transition_into_buff_id = transition_into_buff_id self._walkstyle_active = False @classmethod def _cls_repr(cls): return '{}'.format(cls.__name__) @classmethod def can_add(cls, owner): if cls._add_test_set is not None: resolver = event_testing.resolver.SingleSimResolver(owner) result = cls._add_test_set.run_tests(resolver) if not result: return False return True @classproperty def polarity(cls): if cls.mood_type is not None: return cls.mood_type.buff_polarity return BuffPolarity.NEUTRAL @classproperty def buff_type(cls): return cls @classproperty def get_success_modifier(cls): return cls.success_modifier/100 @classproperty def is_changeable(cls): if cls.mood_type is not None: return cls.mood_type.is_changeable return False @classmethod def add_owning_commodity(cls, commodity): if cls._owning_commodity is None: cls._owning_commodity = commodity elif cls._operating_commodity is None and cls._owning_commodity is not commodity: logger.error('Please fix tuning: Multiple commodities reference {} : commodity:{}, commodity:{}, Set _operating_commodity to authoratative commodity', cls, cls._owning_commodity, commodity) @flexproperty def commodity(cls, inst): if inst is not None and inst._replacing_buff_type is not None: return inst._replacing_buff_type.commodity return cls._operating_commodity or cls._owning_commodity @classmethod def build_critical_section(cls, sim, buff_reason, *sequence): buff_handler = BuffHandler(sim, cls, buff_reason=buff_reason) return build_critical_section_with_finally(buff_handler.begin, sequence, buff_handler.end) @classmethod def _create_temporary_commodity(cls, proxied_commodity=None, create_buff_state=True, initial_value=DEFAULT): if proxied_commodity is None: proxied_commodity = RuntimeCommodity.generate(cls.__name__) proxied_commodity.decay_rate = 1 proxied_commodity.convergence_value = 0 proxied_commodity.remove_on_convergence = True proxied_commodity.visible = False proxied_commodity.max_value_tuning = cls._temporary_commodity_info.max_duration proxied_commodity.min_value_tuning = 0 proxied_commodity.initial_value = initial_value if initial_value is not DEFAULT else cls._temporary_commodity_info.max_duration proxied_commodity._categories = cls._temporary_commodity_info.categories proxied_commodity._time_passage_fixup_type = CommodityTimePassageFixupType.FIXUP_USING_TIME_ELAPSED if create_buff_state: buff_to_add = BuffReference(buff_type=cls) new_state_add_buff = CommodityState(value=0.1, buff=buff_to_add) new_state_remove_buff = CommodityState(value=0, buff=BuffReference()) proxied_commodity.commodity_states = [new_state_remove_buff, new_state_add_buff] cls.add_owning_commodity(proxied_commodity) @classmethod def get_appropriateness(cls, tags): if cls._appropriateness_tags & tags: return Appropriateness.ALLOWED if cls._inappropriateness_tags & tags: return Appropriateness.NOT_ALLOWED return Appropriateness.DONT_CARE @property def mood_override(self): return self._mood_override @mood_override.setter def mood_override(self, value): if not self.is_changeable: logger.error('Trying to override mood for buff:{}, but mood for this is not considered changeable.', self, owner='msantander') self._mood_override = value def on_add(self, load_in_progress): self.effect_modification.on_add() for topic_type in self.topics: self._owner.add_topic(topic_type) tracker = self._owner.static_commodity_tracker for static_commodity_type in self.static_commodity_to_add: tracker.add_statistic(static_commodity_type) if self._static_commodites_added is None: self._static_commodites_added = [] self._static_commodites_added.append(static_commodity_type) self._apply_walkstyle() self.apply_interaction_lockout_to_owner() if not load_in_progress: sim = self._owner.get_sim_instance(allow_hidden_flags=ALL_HIDDEN_REASONS) if sim is not None: self._start_vfx() if self._loot_on_addition: self._apply_all_loot_actions() def _apply_all_loot_actions(self): sim = self._owner.get_sim_instance(allow_hidden_flags=ALL_HIDDEN_REASONS) if sim is not None: resolver = sim.get_resolver() for loot_action in self._loot_on_addition: loot_action.apply_to_resolver(resolver) def on_remove(self, apply_loot_on_remove=True): self.effect_modification.on_remove() for topic_type in self.topics: self._owner.remove_topic(topic_type) if self._static_commodites_added is not None: tracker = self._owner.static_commodity_tracker for static_commodity_type in self._static_commodites_added: tracker.remove_statistic(static_commodity_type) if self._add_buff_on_remove is not None: self._owner.add_buff_from_op(self._add_buff_on_remove.buff_type, self._add_buff_on_remove.buff_reason) self._release_walkstyle() self.on_sim_removed() if apply_loot_on_remove: sim = self._owner.get_sim_instance(allow_hidden_flags=ALL_HIDDEN_REASONS) if sim is not None: resolver = sim.get_resolver() while True: for loot_action in self._loot_on_removal: loot_action.apply_to_resolver(resolver) def clean_up(self): self.effect_modification.on_remove(on_destroy=True) self._release_walkstyle() self.on_sim_removed() if self._static_commodites_added: self._static_commodites_added.clear() self._static_commodites_added = None def on_sim_ready_to_simulate(self): for topic_type in self.topics: self._owner.add_topic(topic_type) self.apply_interaction_lockout_to_owner() self._start_vfx() def _apply_walkstyle(self): if self.walkstyle is not None and not self._walkstyle_active: self._owner.request_walkstyle(self.walkstyle, id(self)) self._walkstyle_active = True def _release_walkstyle(self): if self._walkstyle_active: self._owner.remove_walkstyle(id(self)) self._walkstyle_active = False def on_sim_removed(self, immediate=False): if self._vfx is not None: self._vfx.stop(immediate=immediate) self._vfx = None def apply_interaction_lockout_to_owner(self): if self.interactions is not None: for mixer_affordance in self.interactions.interaction_items: while mixer_affordance.lock_out_time_initial is not None: self._owner.set_sub_action_lockout(mixer_affordance, initial_lockout=True) def add_handle(self, handle_id, buff_reason=None): self.handle_ids.append(handle_id) self.buff_reason = buff_reason def remove_handle(self, handle_id): if handle_id not in self.handle_ids: return False self.handle_ids.remove(handle_id) if self.handle_ids: return False return True def _start_vfx(self): if self._vfx is None and self.vfx: sim = self._owner.get_sim_instance(allow_hidden_flags=ALL_HIDDEN_REASONS) self._vfx = self.vfx(sim) self._vfx.start() def _get_tracker(self): if self.commodity is not None: return self._owner.get_tracker(self.commodity) def _get_commodity_instance(self): if self.commodity is None: return tracker = self._get_tracker() if tracker is None: return commodity_instance = tracker.get_statistic(self.commodity) if commodity_instance is None: return return commodity_instance def _get_absolute_timeout_time(self, commodity_instance, threshold): rate_multiplier = commodity_instance.get_decay_rate_modifier() if rate_multiplier < 1: time = commodity_instance.get_decay_time(threshold) rate_multiplier = 1 else: time = commodity_instance.get_decay_time(threshold, use_decay_modifier=False) if time is not None and time != 0: time_now = services.time_service().sim_now time_stamp = time_now + interval_in_sim_minutes(time) return (time_stamp.absolute_ticks(), rate_multiplier) return NO_TIMEOUT def get_timeout_time(self): commodity_instance = self._get_commodity_instance() if commodity_instance is None: return NO_TIMEOUT buff_type = self.buff_type if self._replacing_buff_type is not None: buff_type = self._replacing_buff_type else: buff_type = self.buff_type state_index = commodity_instance.get_state_index_matches_buff_type(buff_type) if state_index is None: return NO_TIMEOUT state_lower_bound_value = commodity_instance.commodity_states[state_index].value if commodity_instance.convergence_value <= state_lower_bound_value: threshold_value = state_lower_bound_value comparison = operator.le else: comparison = operator.ge next_state_index = state_index + 1 if next_state_index >= len(commodity_instance.commodity_states): threshold_value = commodity_instance.convergence_value else: threshold_value = commodity_instance.commodity_states[next_state_index].value threshold = sims4.math.Threshold(threshold_value, comparison) return self._get_absolute_timeout_time(commodity_instance, threshold)
class TimedAspiration(TimedAspirationDefinitionDisplayMixin, AspirationBasic): INSTANCE_TUNABLES = { 'duration': TunableVariant( weekly_schedule=WeeklySchedule.TunableFactory( description= '\n Determines days of the week when the aspiration will deactivate.\n ' ), duration=TunableTimeSpan( description= '\n The time that this aspiration is active within the tracker.\n ' )), 'on_complete_loot_actions': TunableList( description= '\n List of loots operations that will be awarded when \n this aspiration complete.\n ', tunable=LootActions.TunableReference()), 'on_failure_loot_actions': TunableList( description= '\n List of loots operations that will be awarded when \n this aspiration fails.\n ', tunable=LootActions.TunableReference()), 'on_cancel_loot_actions': TunableList( description= '\n List of loots operations that will be awarded when \n this aspiration is cancelled.\n ', tunable=LootActions.TunableReference()), 'warning_buff': OptionalTunable( description= '\n If enabled, the buff is given to the Sim as a warning the aspiration\n duration is ending.\n ', tunable=TunableBuffReference( description= '\n The buff that is given to the Sim when the aspiration is getting\n close to timing out.\n ' )), 'tests': TunableTestSetWithTooltip( description= '\n Test set that must pass for this aspiration to be available.\n ' ) } @constproperty def aspiration_type(): return AspriationType.TIMED_ASPIRATION @classmethod def apply_on_complete_loot_actions(cls, sim_info): resolver = SingleSimResolver(sim_info) for loot_action in cls.on_complete_loot_actions: loot_action.apply_to_resolver(resolver) @classmethod def apply_on_failure_loot_actions(cls, sim_info): resolver = SingleSimResolver(sim_info) for loot_action in cls.on_failure_loot_actions: loot_action.apply_to_resolver(resolver) @classmethod def apply_on_cancel_loot_actions(cls, sim_info): resolver = SingleSimResolver(sim_info) for loot_action in cls.on_cancel_loot_actions: loot_action.apply_to_resolver(resolver) def generate_aspiration_data(self, aspiration, **kwargs): return TimedAspirationData(self, aspiration)
class FamiliarTracker(SimInfoTracker): FAMILIAR_DATA = TunableMapping(description='\n A mapping between the familiar type and data associated with that familiar type.\n ', key_type=TunableEnumEntry(description='\n The type of familiar associated with this data.\n ', tunable_type=FamiliarType, default=FamiliarType.CAT), value_type=TunableTuple(description='\n Data associated with a specific familiar type.\n ', icon=TunableIconAllPacks(description='\n The icon of the familiar within the picker.\n '), familiar_type=OptionalTunable(description="\n The type of familiar this is.\n Object Familiars have a special object associated with them that is created whenever the Sim is created\n and has an interaction pushed on the owning Sim to places the pet familiar in a routing formation with\n the owning Sim.\n \n Pet Based Familiars are instead based on Pets and rely on the Pet's autonomy to drive most behavior\n with the familiar. \n ", tunable=TunableTuple(description='\n Data related to Object Based Familiars.\n ', familiar_object=TunablePackSafeReference(description='\n The definition of the familiar object that will be created.\n ', manager=services.definition_manager()), name_list=TunableEnumEntry(description="\n The name list associated with this familiar type.\n \n Since familiars don't have any specific gender associated with them we always just use Male\n names.\n ", tunable_type=SimNameType, default=SimNameType.DEFAULT), follow_affordance=TunablePackSafeReference(description='\n The specific affordance to follow a familiar.\n ', manager=services.get_instance_manager(sims4.resources.Types.INTERACTION)), familiar_description=TunableLocalizedStringFactory(description='\n The description of this familiar as it appears in the familiar rename dialogs.\n '), familiar_token_object=TunableReference(description="\n The definition of the familiar token object that will be created and played into the user's\n inventory when the familiar is unbound.\n ", manager=services.definition_manager(), pack_safe=True)), enabled_by_default=True, disabled_name='pet_based_familiar', enabled_name='object_based_familiar'))) FAMILIAR_PLACEMENT = _PlacementStrategyLocation.TunableFactory(description="\n Method for placing the familiar's initial position based on the Sim.\n ") FAMILIAR_ENSEMBLE = TunablePackSafeReference(description='\n The ensemble to place pet familiars in with their master.\n ', manager=services.get_instance_manager(sims4.resources.Types.ENSEMBLE)) PET_FAMILIAR_BIT = TunablePackSafeReference(description='\n The relationship bit to indicate that a pet is a familiar.\n ', manager=services.get_instance_manager(sims4.resources.Types.RELATIONSHIP_BIT)) FAMILIAR_SUMMON_FAILURE_NOTIFICATION = UiDialogNotification.TunableFactory(description='\n A TNS that is displayed when the familiar fails to be summoned.\n ') PET_FAMILIAR_SET_ACTIVE_AFFORDANCE = TunablePackSafeReference(description='\n An interaction pushed on pet Sims when they are set to be the active familiar.\n ', manager=services.get_instance_manager(sims4.resources.Types.INTERACTION)) PET_FAMILIAR_BUFF = TunableMapping(description='\n A buff added to pet familiars based on age and species.\n ', key_type=TunableEnumEntry(description='\n The age that this buff will be applied to.\n ', tunable_type=Age, default=Age.ADULT), value_type=TunableMapping(key_type=TunableEnumEntry(description='\n The species that this buff will be applied to.\n ', tunable_type=SpeciesExtended, default=SpeciesExtended.HUMAN, invalid_enums=(SpeciesExtended.INVALID,)), value_type=TunableBuffReference(description='\n The buff that will be given to the Familiar of this age/species pair.\n ', pack_safe=True))) ACTIVE_FAMILIAR_BUFF = TunableBuffReference(description='\n The buff that will be given to the Sim when they have an active familiar.\n ', pack_safe=True) def __init__(self, owner, *args, **kwargs): super().__init__(*args, **kwargs) self._owner = owner self._active_familiar_id = None self._active_familiar_obj_id = None self._familiars = {} self._sim_buff_handle = None self._pet_buff_handle = None def __iter__(self): yield from self._familiars.values() @property def has_familiars(self): return len(self._familiars) > 0 @property def active_familiar_id(self): return self._active_familiar_id @property def active_familiar_id_pet_id(self): if self._active_familiar_id is None: return return self._familiars[self._active_familiar_id].pet_familiar_id @property def active_familiar_type(self): if self._active_familiar_id is None: return return self._familiars[self._active_familiar_id].familiar_type def get_active_familiar(self): return services.object_manager().get(self._active_familiar_obj_id) def get_familiar_name(self, familiar_uid): if familiar_uid not in self._familiars: logger.error("Attempting to get name of familiar that does not exist within {}'s familiar tracker", self._owner) return return self._familiars[familiar_uid].name def get_familiar_icon(self, familiar_uid): if familiar_uid not in self._familiars: logger.error("Attempting to get icon of familiar that does not exist within {}'s familiar tracker", self._owner) return return IconInfoData(FamiliarTracker.FAMILIAR_DATA[self._familiars[familiar_uid].familiar_type].icon) def get_familiar_description(self, familiar_uid): if familiar_uid not in self._familiars: logger.error("Attempting to get description of familiar that does not exist within {}'s familiar tracker", self._owner) return familiar_type = FamiliarTracker.FAMILIAR_DATA[self._familiars[familiar_uid].familiar_type].familiar_type if familiar_type is None: logger.error('Attempting to get the description of a Pet familiar. These familiars do not need descriptions for rename dialogs.') return return familiar_type.familiar_description def bind_familiar(self, familiar_type, pet_familiar=None): if pet_familiar is None: name = SimSpawner.get_random_first_name(Gender.MALE, sim_name_type_override=FamiliarTracker.FAMILIAR_DATA[familiar_type].familiar_type.name_list) pet_familiar_id = None else: name = None pet_familiar_id = pet_familiar.sim_id services.relationship_service().add_relationship_bit(self._owner.sim_id, pet_familiar.sim_id, FamiliarTracker.PET_FAMILIAR_BIT) familiar_uid = id_generator.generate_object_id() new_familiar = FamiliarInfo(familiar_uid, familiar_type, name, pet_familiar_id) self._familiars[new_familiar.uid] = new_familiar return new_familiar.uid def unbind_familiar(self, familiar_uid): if familiar_uid not in self._familiars: logger.error('Attemting to unbind familiar that is not in the familiar tracker.') return if self._active_familiar_id is not None and self._active_familiar_id == familiar_uid: self.dismiss_familiar() familiar_info = self._familiars[familiar_uid] pet_familiar_id = familiar_info.pet_familiar_id if pet_familiar_id is not None: services.relationship_service().remove_relationship_bit(self._owner.sim_id, pet_familiar_id, FamiliarTracker.PET_FAMILIAR_BIT) else: sim = self._owner.get_sim_instance(allow_hidden_flags=ALL_HIDDEN_REASONS) if sim is not None: familiar_token = create_object(self.FAMILIAR_DATA[familiar_info.familiar_type].familiar_type.familiar_token_object) if familiar_token is not None: familiar_token.set_household_owner_id(self._owner.household_id) sim.inventory_component.system_add_object(familiar_token) else: logger.error('Attempting to create familiar token on unbind, but failed to do so.') else: logger.error('Sim is not instanced when unbinding familiar. The familiar token will not be generated.') del self._familiars[familiar_uid] def set_familiar_name(self, familiar_id, new_name): if familiar_id is None: if self._active_familiar_id: logger.error('Trying to set the name of a familiar with both no specified familiar nor no active familiar.') return familiar_id = self._active_familiar_id self._familiars[familiar_id].name = new_name def _on_familiar_summon_failure(self, error_message, familiar_object=None, exc=None, warn=False): if exc is not None: logger.exception(error_message, exc=exc) elif warn: logger.warn(error_message) else: logger.error(error_message) if familiar_object is not None: familiar_object.destroy() self._active_familiar_id = None self._active_familiar_obj_id = None resolver = SingleSimResolver(self._owner) dialog = self.FAMILIAR_SUMMON_FAILURE_NOTIFICATION(self._owner, resolver) dialog.show_dialog() def _on_familiar_follow_interaction_finished_prematurely(self, interaction): if interaction.is_finishing_naturally or interaction.has_been_reset: return self._active_familiar_id = None self._active_familiar_obj_id = None if self._sim_buff_handle is not None: self._owner.remove_buff(self._sim_buff_handle) self._sim_buff_handle = None def _create_and_establish_familiar_link(self): sim = self._owner.get_sim_instance(allow_hidden_flags=ALL_HIDDEN_REASONS_EXCEPT_UNINITIALIZED) if sim is None: return familiar_type = self._familiars[self._active_familiar_id].familiar_type familiar_data = FamiliarTracker.FAMILIAR_DATA[familiar_type] if familiar_data.familiar_type is None: try: self._active_familiar_obj_id = self._familiars[self._active_familiar_id].pet_familiar_id pet_familiar_sim_info = services.sim_info_manager().get(self._active_familiar_obj_id) buff_info_to_add = self.PET_FAMILIAR_BUFF[pet_familiar_sim_info.age][pet_familiar_sim_info.extended_species] self._pet_buff_handle = pet_familiar_sim_info.add_buff(buff_info_to_add.buff_type, buff_reason=buff_info_to_add.buff_reason) pet_familiar = services.object_manager().get(self._active_familiar_obj_id) if services.current_zone().is_zone_running: if pet_familiar is None: services.current_zone().venue_service.active_venue.summon_npcs((pet_familiar_sim_info,), NPCSummoningPurpose.BRING_PLAYER_SIM_TO_LOT) else: context = InteractionContext(pet_familiar, InteractionContext.SOURCE_SCRIPT, Priority.Critical, insert_strategy=QueueInsertStrategy.NEXT) pet_familiar.push_super_affordance(FamiliarTracker.PET_FAMILIAR_SET_ACTIVE_AFFORDANCE, None, context) except Exception as e: self._on_familiar_summon_failure('Exception encountered when trying to create familiar. Deactivating familiar.', familiar_object=pet_familiar, exc=e) if self._sim_buff_handle is None: self._sim_buff_handle = self._owner.add_buff(self.ACTIVE_FAMILIAR_BUFF.buff_type, buff_reason=self.ACTIVE_FAMILIAR_BUFF.buff_reason) return familiar = services.object_manager().get(self._active_familiar_obj_id) if familiar is None: try: familiar_obj_def = familiar_data.familiar_type.familiar_object familiar = create_object(familiar_obj_def) if familiar is None: self._on_familiar_summon_failure('Failure to create familiar object. Deactivating familiar.') return resolver = SingleSimResolver(self._owner) if not FamiliarTracker.FAMILIAR_PLACEMENT.try_place_object(familiar, resolver): self._on_familiar_summon_failure('Failure to create familiar object. Deactivating familiar.', familiar_object=familiar, warn=True) return self._active_familiar_obj_id = familiar.id except Exception as e: self._on_familiar_summon_failure('Exception encountered when trying to create familiar. Deactivating familiar.', familiar_object=familiar, exc=e) return context = InteractionContext(sim, InteractionSource.SCRIPT, Priority.Critical, insert_strategy=QueueInsertStrategy.NEXT) result = sim.push_super_affordance(familiar_data.familiar_type.follow_affordance, familiar, context) if not result: self._on_familiar_summon_failure('Failed to push familiar follow interaction. Deactivating Familiar.', familiar_object=familiar) return result.interaction.register_on_finishing_callback(self._on_familiar_follow_interaction_finished_prematurely) if self._sim_buff_handle is None: self._sim_buff_handle = self._owner.add_buff(self.ACTIVE_FAMILIAR_BUFF.buff_type, buff_reason=self.ACTIVE_FAMILIAR_BUFF.buff_reason) def remove_active_pet_familiar_buff(self): if self._pet_buff_handle is None: return pet_sim_info = services.sim_info_manager().get(self._familiars[self._active_familiar_id].pet_familiar_id) if pet_sim_info is None: self._pet_buff_handle = None return pet_sim_info.remove_buff(self._pet_buff_handle) self._pet_buff_handle = None def set_active_familiar(self, familiar_uid): if familiar_uid not in self._familiars: logger.error("Attempting to set a familiar as active that isn't an actual familiar.") return if self._active_familiar_obj_id is not None: active_familiar_obj = services.object_manager().get(self._active_familiar_obj_id) if active_familiar_obj is not None and not active_familiar_obj.is_sim: active_familiar_obj.destroy() self.remove_active_pet_familiar_buff() self._active_familiar_obj_id = None self._active_familiar_id = familiar_uid self._create_and_establish_familiar_link() def dismiss_familiar(self): if self._active_familiar_id is None: return if self._sim_buff_handle is not None: self._owner.remove_buff(self._sim_buff_handle) self._sim_buff_handle = None self.remove_active_pet_familiar_buff() familiar = self.get_active_familiar() if familiar is None: if self._familiars[self._active_familiar_id].pet_familiar_id is None: logger.error("Attempting to dismiss a familiar that is active, but doesn't have a familiar object.") self._active_familiar_obj_id = None self._active_familiar_id = None return if not familiar.is_sim: owner_sim = self._owner.get_sim_instance(allow_hidden_flags=ALL_HIDDEN_REASONS) if owner_sim is None: familiar.destroy() else: follow_affordance = self.FAMILIAR_DATA[self._familiars[self._active_familiar_id].familiar_type].familiar_type.follow_affordance sim_interactions = owner_sim.get_all_running_and_queued_interactions() for interaction in sim_interactions: if interaction.affordance is follow_affordance: interaction.cancel(FinishingType.NATURAL, cancel_reason_msg='User changed familiars.') self._active_familiar_obj_id = None self._active_familiar_id = None def on_sim_startup(self): if self._active_familiar_id is None: return if self._sim_buff_handle is None: self._sim_buff_handle = self._owner.add_buff(self.ACTIVE_FAMILIAR_BUFF.buff_type, buff_reason=self.ACTIVE_FAMILIAR_BUFF.buff_reason) self._create_and_establish_familiar_link() def on_sim_removed(self): if self._active_familiar_id is None: return if self._sim_buff_handle is not None: self._owner.remove_buff(self._sim_buff_handle) self._sim_buff_handle = None self.remove_active_pet_familiar_buff() active_familiar = self.get_active_familiar() if active_familiar is None or active_familiar.is_sim: return active_familiar.destroy() def on_household_member_removed(self): sim_info_manager = services.sim_info_manager() for familiar_data in tuple(self._familiars.values()): pet_familiar = sim_info_manager.get(familiar_data.pet_familiar_id) if pet_familiar is None: continue if pet_familiar.household_id == self._owner.household_id: continue services.relationship_service().remove_relationship_bit(self._owner.sim_id, familiar_data.pet_familiar_id, FamiliarTracker.PET_FAMILIAR_BIT) if self._active_familiar_id == familiar_data.uid: self.dismiss_familiar() del self._familiars[familiar_data.uid] return def save(self): data = SimObjectAttributes_pb2.PersistableFamiliarTracker() if self._active_familiar_id is not None: data.active_familiar_uid = self._active_familiar_id for familiar_info in self._familiars.values(): with ProtocolBufferRollback(data.familiars) as entry: familiar_info.save_familiar_info(entry) return data def load(self, data): if data.HasField('active_familiar_uid'): self._active_familiar_id = data.active_familiar_uid sim_info_manager = services.sim_info_manager() for familiar_data in data.familiars: if familiar_data.HasField('familiar_name'): familiar_name = familiar_data.familiar_name else: familiar_name = None if familiar_data.HasField('pet_familiar_id'): pet_familiar_id = familiar_data.pet_familiar_id else: pet_familiar_id = None try: loaded_familiar = FamiliarInfo(familiar_data.familiar_uid, FamiliarType(familiar_data.familiar_type), familiar_name, pet_familiar_id=pet_familiar_id) except Exception as e: logger.exception('Exception encountered when trying to load familiar. Skipping familiar.', exc=e) if pet_familiar_id is not None: services.relationship_service().remove_relationship_bit(self._owner.sim_id, pet_familiar_id, FamiliarTracker.PET_FAMILIAR_BIT) self._familiars[familiar_data.familiar_uid] = loaded_familiar def on_all_sim_infos_loaded(self): sim_info_manager = services.sim_info_manager() for familiar_data in tuple(self._familiars.values()): pet_familiar_id = familiar_data.pet_familiar_id if pet_familiar_id is None: continue pet_familiar = sim_info_manager.get(pet_familiar_id) if pet_familiar is not None and pet_familiar.household_id == self._owner.household_id: continue self.unbind_familiar(familiar_data.uid) @classproperty def _tracker_lod_threshold(cls): return SimInfoLODLevel.ACTIVE def on_lod_update(self, old_lod, new_lod): if new_lod < self._tracker_lod_threshold: self.dismiss_familiar() self._clean_up() elif old_lod < self._tracker_lod_threshold: sim_msg = services.get_persistence_service().get_sim_proto_buff(self._owner.id) if sim_msg is not None: self.load(sim_msg.attributes.familiar_tracker) def _clean_up(self): self._active_familiar_obj_id = None self._active_familiar_id = None self._familiars.clear() self._familiars = None
class BucksPerk(metaclass=TunedInstanceMetaclass, manager=services.get_instance_manager(sims4.resources.Types.BUCKS_PERK)): INSTANCE_TUNABLES = {'associated_bucks_type': TunableEnumEntry(description='\n The type of Bucks required to unlock this Perk.\n ', tunable_type=BucksType, default=BucksType.INVALID, pack_safe=True, invalid_enums=(BucksType.INVALID,)), 'unlock_cost': Tunable(description='\n How many Bucks of the specified type this Perk costs to unlock.\n ', tunable_type=int, default=100), 'rewards': TunableList(description='\n A list of rewards to grant the household when this Perk is\n unlocked.\n ', tunable=TunableSpecificReward()), 'linked_perks': TunableList(description='\n A list of Perks that will be unlocked along with this one if not\n already unlocked.\n ', tunable=TunableReference(manager=services.get_instance_manager(sims4.resources.Types.BUCKS_PERK))), 'next_level_perk': TunableReference(description='\n The next perk within this particular chain of perks. \n If tunable is None, then it either does not belong to a chain\n or is the last in the chain.\n ', manager=services.get_instance_manager(sims4.resources.Types.BUCKS_PERK), allow_none=True), 'temporary_perk_information': OptionalTunable(TunableTuple(description='\n Tunables associated with temporary Perks.\n ', duration=TunableRange(description='\n The tunable number of Sim hours this Perk should last for, if\n temporary.\n ', tunable_type=int, default=1, minimum=1), unlocked_tooltip=OptionalTunable(TunableLocalizedStringFactory(description='\n A tooltip that will be shown on this Perk when unlocked so the\n user knows when they will be able to buy it again. No expected\n arguments.\n ')))), 'display_name': TunableLocalizedStringFactory(description="\n This Perk's display name. No expected arguments.\n ", tuning_group=GroupNames.UI), 'perk_description': TunableLocalizedStringFactory(description='\n The description for this Perk. No expected arguments.\n ', tuning_group=GroupNames.UI), 'undiscovered_description': OptionalTunable(description='\n When enabled will cause a different description to be displayed \n if the Perk has never been acquired by the Sim.\n ', tunable=TunableLocalizedStringFactory(description='\n The description for this perk when it has never been acquired\n by this Sim.\n '), tuning_group=GroupNames.UI), 'icon': TunableIconFactory(tuning_group=GroupNames.UI), 'ui_display_flags': TunableEnumFlags(description='\n The display flags for this Perk in the Perks Panel UI.\n LINK_TOP: Display a line connecting this perk to the perk above it\n ', enum_type=BucksUIDisplayFlag, allow_no_flags=True, tuning_group=GroupNames.UI), 'required_unlocks': OptionalTunable(description='\n A list of all of the bucks perks that must be unlocked for this one\n to be available for purchase.\n ', tunable=TunableList(description='\n List of required perks for this perk to be available.\n ', tunable=TunableReference(description='\n Reference to a bucks perk that must be unlocked.\n ', manager=services.get_instance_manager(sims4.resources.Types.BUCKS_PERK)))), 'lock_on_purchase': OptionalTunable(description='\n A list of perks to lock when this perk is purchased.\n ', tunable=TunableList(description='\n List of perks to lock when this perk is unlocked.\n ', tunable=TunableReference(description='\n Reference to a bucks perk that must be locked.\n ', manager=services.get_instance_manager(sims4.resources.Types.BUCKS_PERK)))), 'buffs_to_award': TunableList(description='\n A list of references to buffs to add to the owner of \n bucks tracker this perk is unlocked in and optional reason for the\n buffs.\n ', tunable=TunableBuffReference(description='\n A pair of Buff and Reason for the buff.\n ', pack_safe=True)), 'conflicting_perks': TunableList(description='\n A list of perks that this perk is mutually exclusive with.\n \n When a perk is mutually exclusive with another perk it means that\n the perk cannot be purchased if that perk has already been purchased.\n ', tunable=TunableReference(description='\n A reference to a perk that this perk is mutually exclusive with.\n ', manager=services.get_instance_manager(sims4.resources.Types.BUCKS_PERK))), 'loots_on_unlock': TunableList(description='\n A list of loots to award when this perk is Unlocked.\n ', tunable=TunableReference(description='\n A loot to be applied.\n \n Actor is the Sim that the perk is being unlocked for.\n ', manager=services.get_instance_manager(sims4.resources.Types.ACTION), pack_safe=True)), 'loots_on_lock': TunableList(description='\n A list of loots to award when this perk is Locked.\n ', tunable=TunableReference(description='\n A loot to be applied.\n \n Actor is the Sim that the Perk is being locked for.\n ', manager=services.get_instance_manager(sims4.resources.Types.ACTION), pack_safe=True)), 'available_for_puchase_tests': TunableTestSetWithTooltip(description='\n A set of tests that must pass in order for this perk to be\n available for purchase. \n \n This is enforced on the UI side. If the tests returns False then\n we will mark the perk as locked and pass along a tooltip, the one\n from the failed test.\n \n For the tooltip the first token is the Sim attempting to unlock the\n perk. \n '), 'progression_statistic': OptionalTunable(description='\n If enabled, this ranked statistic tracks the progress towards\n unlocking this perk. This statistic should tune an AwardPerkLoot\n in its event data with the ability to award this perk.\n \n Use an AwardPerkLoot with the progress strategy to give progress \n to obtaining the perk.\n ', tunable=TunableReference(manager=services.get_instance_manager(sims4.resources.Types.STATISTIC), class_restrictions=('RankedStatistic',)))} @classmethod def _verify_tuning_callback(cls): if cls.temporary_perk_information is not None and cls.linked_perks: logger.error("A Bucks Perk has been created that's both temporary and has linked Perks. This is not supported. {}", cls)
class Baby(GameObject): __qualname__ = 'Baby' MAX_PLACEMENT_ATTEMPTS = 8 BABY_BASSINET_DEFINITION_MAP = TunableMapping( description= '\n The corresponding mapping for each definition pair of empty bassinet\n and bassinet with baby inside. The reason we need to have two of\n definitions is one is deletable and the other one is not.\n ', key_name='Baby', key_type=TunableReference( description= '\n Bassinet with Baby object definition id.\n ', manager=services.definition_manager()), value_name='EmptyBassinet', value_type=TunableReference( description= '\n Bassinet with Baby object definition id.\n ', manager=services.definition_manager())) BASSINET_EMPTY_STATE = TunableStateValueReference( description='\n The state value for an empty bassinet.\n ' ) BASSINET_BABY_STATE = TunableStateValueReference( description= '\n The state value for a non-empty bassinet.\n ') STATUS_STATE = ObjectState.TunableReference( description= '\n The state defining the overall status of the baby (e.g. happy, crying,\n sleeping). We use this because we need to reapply this state to restart\n animations after a load.\n ' ) BABY_SKIN_TONE_STATE_MAPPING = TunableMapping( description= '\n From baby skin tone enum to skin tone state mapping.\n ', key_type=TunableEnumEntry(tunable_type=BabySkinTone, default=BabySkinTone.MEDIUM), value_type=TunableTuple(boy=TunableStateValueReference(), girl=TunableStateValueReference())) BABY_MOOD_MAPPING = TunableMapping( description= '\n From baby state (happy, crying, sleep) to in game mood.\n ', key_type=TunableStateValueReference(), value_type=Mood.TunableReference()) BABY_SKIN_TONE_TO_CAS_SKIN_TONE = TunableMapping( description= '\n From baby skin tone enum to cas skin tone id mapping.\n ', key_type=TunableEnumEntry(tunable_type=BabySkinTone, default=BabySkinTone.MEDIUM), value_type=TunableList( description= '\n The Skin Tones CAS reference under Catalog/InGame/CAS/Skintones.\n ', tunable=TunableSkinTone()), export_modes=ExportModes.All, tuple_name='BabySkinToneToCasTuple') SEND_BABY_TO_DAYCARE_NOTIFICATION_SINGLE_BABY = TunableUiDialogNotificationSnippet( description= '\n The message appearing when a single baby is sent to daycare. You can\n reference this single baby by name.\n ' ) SEND_BABY_TO_DAYCARE_NOTIFICATION_MULTIPLE_BABIES = TunableUiDialogNotificationSnippet( description= '\n The message appearing when multiple babies are sent to daycare. You can\n not reference any of these babies by name.\n ' ) BRING_BABY_BACK_FROM_DAYCARE_NOTIFICATION_SINGLE_BABY = TunableUiDialogNotificationSnippet( description= '\n The message appearing when a single baby is brought back from daycare.\n You can reference this single baby by name.\n ' ) BRING_BABY_BACK_FROM_DAYCARE_NOTIFICATION_MULTIPLE_BABIES = TunableUiDialogNotificationSnippet( description= '\n The message appearing when multiple babies are brought back from\n daycare. You can not reference any of these babies by name.\n ' ) BABY_AGE_UP = TunableTuple( description= '\n Multiple settings for baby age up moment.\n ', age_up_affordance=TunableReference( description= '\n The affordance to run when baby age up to kid.\n ', manager=services.affordance_manager(), class_restrictions='SuperInteraction'), copy_states=TunableList( description= '\n The list of the state we want to copy from the original baby\n bassinet to the new bassinet to play idle.\n ', tunable=TunableReference(manager=services.object_state_manager(), class_restrictions='ObjectState')), idle_state_value=TunableReference( description= '\n The state value to apply on the new baby bassinet with the age up\n special idle animation/vfx linked.\n ', manager=services.object_state_manager(), class_restrictions='ObjectStateValue')) BABY_PLACEMENT_TAGS = TunableList( TunableEnumEntry( tag.Tag, tag.Tag.INVALID, description= '\n Attempt to place the baby near objects with this tag set.\n ' ), description= '\n When trying to place a baby bassinet on the lot, we attempt to place it\n near other objects on the lot. Those objects are determined in priority\n order by this tuned list. It will try to place next to all objects of the\n matching types, before trying to place the baby in the middle of the lot,\n and then finally trying the mailbox. If all FGL placements fail, we put\n the baby into the household inventory.\n ' ) BABY_THUMBNAIL_DEFINITION = TunableReference( description= '\n The thumbnail definition for client use only.\n ', manager=services.definition_manager(), export_modes=(ExportModes.ClientBinary, )) NEGLECTED_STATES = TunableList( description= '\n If the baby enters any of these states, the neglected moment will begin.\n ', tunable=TunableStateValueReference( description= '\n The state to listen for in order to push the neglected moment on the baby.\n ' )) NEGLECT_ANIMATION = TunableReference( description= '\n The animation to play on the baby for the neglect moment.\n ', manager=services.get_instance_manager(sims4.resources.Types.ANIMATION), class_restrictions='ObjectAnimationElement') NEGLECT_NOTIFICATION = UiDialogNotification.TunableFactory( description= '\n The notification to show when the baby neglect moment happens.\n ' ) NEGLECT_EFFECT = Tunable( description= '\n The VFX to play during the neglect moment.\n ', tunable_type=str, default='s40_Sims_neglected') NEGLECT_EMPTY_BASSINET_STATE = TunableStateValueReference( description= '\n The state that will be set on the bassinet when it is emptied due to\n neglect. This should control any reaction broadcasters that we want to\n happen when the baby is taken away. This MUST be tuned.\n ' ) NEGLECT_BUFF_IMMEDIATE_FAMILY = TunableBuffReference( description= "\n The buff to be applied to the baby's immediate family during the \n neglect moment.\n " ) FAILED_PLACEMENT_NOTIFICATION = UiDialogNotification.TunableFactory( description= '\n The notification to show if a baby could not be spawned into the world\n because FGL failed. This is usually due to the player cluttering their\n lot with objects. Token 0 is the baby.\n ' ) @classmethod def get_baby_skin_tone_enum(cls, sim_info): if sim_info.is_baby: skin_tone_id = sim_info.skin_tone for (skin_enum, tone_ids) in cls.BABY_SKIN_TONE_TO_CAS_SKIN_TONE.items(): while skin_tone_id in tone_ids: return skin_enum logger.error( 'Baby with skin tone id {} not in BABY_SKIN_TONE_TO_CAS_SKIN_TONE. Setting light skin tone instead.', skin_tone_id, owner='jjacobson') return BabySkinTone.LIGHT return BabySkinTone.ADULT_SIM @classmethod def get_baby_skin_tone_state(cls, sim_info): skin_tone_state_value = None baby_skin_enum = cls.get_baby_skin_tone_enum(sim_info) if baby_skin_enum is not None and baby_skin_enum in cls.BABY_SKIN_TONE_STATE_MAPPING: skin_state_tuple = cls.BABY_SKIN_TONE_STATE_MAPPING[baby_skin_enum] if sim_info.gender == Gender.FEMALE: skin_tone_state_value = skin_state_tuple.girl elif sim_info.gender == Gender.MALE: skin_tone_state_value = skin_state_tuple.boy return skin_tone_state_value @classmethod def get_corresponding_definition(cls, definition): if definition in cls.BABY_BASSINET_DEFINITION_MAP: return cls.BABY_BASSINET_DEFINITION_MAP[definition] for (baby_def, bassinet_def) in cls.BABY_BASSINET_DEFINITION_MAP.items(): while bassinet_def is definition: return baby_def @classmethod def get_default_baby_def(cls): return next(iter(cls.BABY_BASSINET_DEFINITION_MAP), None) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._sim_info = None self.state_component.state_trigger_enabled = False self._started_neglect_moment = False self._ignore_daycare = False def set_sim_info(self, sim_info, ignore_daycare=False): self._sim_info = sim_info self._ignore_daycare = ignore_daycare if self._sim_info is not None: self.state_component.state_trigger_enabled = True self.enable_baby_state() @property def sim_info(self): return self._sim_info @property def is_selectable(self): return self.sim_info.is_selectable @property def sim_id(self): if self._sim_info is not None: return self._sim_info.sim_id return self.id @property def household_id(self): if self._sim_info is not None: return self._sim_info.household.id return 0 def populate_localization_token(self, *args, **kwargs): if self.sim_info is not None: return self.sim_info.populate_localization_token(*args, **kwargs) logger.warn( 'self.sim_info is None in baby.populate_localization_token', owner='epanero', trigger_breakpoint=True) return super().populate_localization_token(*args, **kwargs) def enable_baby_state(self): if self._sim_info is None: return self.set_state(self.BASSINET_BABY_STATE.state, self.BASSINET_BABY_STATE) status_state = self.get_state(self.STATUS_STATE) self.set_state(status_state.state, status_state, force_update=True) skin_tone_state = self.get_baby_skin_tone_state(self._sim_info) if skin_tone_state is not None: self.set_state(skin_tone_state.state, skin_tone_state) def empty_baby_state(self): self.set_state(self.BASSINET_EMPTY_STATE.state, self.BASSINET_EMPTY_STATE) def on_state_changed(self, state, old_value, new_value): super().on_state_changed(state, old_value, new_value) if new_value in self.NEGLECTED_STATES and not self._started_neglect_moment: start_baby_neglect(self) elif self.manager is not None and new_value in self.BABY_MOOD_MAPPING: mood = self.BABY_MOOD_MAPPING[new_value] mood_msg = Commodities_pb2.MoodUpdate() mood_msg.sim_id = self.id mood_msg.mood_key = mood.guid64 mood_msg.mood_intensity = 1 distributor.shared_messages.add_object_message( self, MSG_SIM_MOOD_UPDATE, mood_msg, False) def load_object(self, object_data): super().load_object(object_data) self._sim_info = services.sim_info_manager().get(self.sim_id) def on_finalize_load(self): sim_info = services.sim_info_manager().get(self.sim_id) if sim_info is None or sim_info.household is not services.active_lot( ).get_household(): _replace_bassinet(sim_info, bassinet=self) else: self.set_sim_info(sim_info)
class _BabyRemovalMoment(HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = { 'notification': OptionalTunable( description= '\n If enabled, specify a notification to show when this moment is\n executed.\n ', tunable=TunableUiDialogNotificationSnippet( description= '\n The notification to show when this moment is executed.\n ' )), 'vfx': OptionalTunable( description= '\n If enabled, play a visual effect when this moment is executed.\n ', tunable=PlayEffect.TunableFactory( description= '\n The visual effect to play when this moment is executed.\n ' )), 'buff': OptionalTunable( description= "\n If enabled, specify a buff to apply to the baby's immediate family\n when this moment is executed.\n ", tunable=TunableBuffReference( description= "\n The buff to be applied to the baby's immediate family when this\n moment is executed.\n " )), 'empty_state': TunableStateValueReference( description= '\n The state to set on the empty bassinet after this moment is\n executed. This should control any reaction broadcasters that we\n might want to happen when this baby is no longer present.\n ', allow_none=True) } def execute_removal_moment(self, baby): baby.is_being_removed = True sim_info = baby.sim_info if self.notification is not None: dialog = self.notification(sim_info, SingleSimResolver(sim_info)) dialog.show_dialog() if self.vfx is not None: vfx = self.vfx(baby) vfx.start() camera.focus_on_sim(baby, follow=False) sim_info_manager = services.sim_info_manager() if self.buff is not None: with genealogy_caching(): for member_id in sim_info.genealogy.get_immediate_family_sim_ids_gen( ): member_info = sim_info_manager.get(member_id) if member_info.lod != SimInfoLODLevel.MINIMUM: member_info.add_buff_from_op(self.buff.buff_type, self.buff.buff_reason) baby.cancel_interactions_running_on_object( FinishingType.TARGET_DELETED, cancel_reason_msg='Baby is being removed.') empty_bassinet = replace_bassinet(sim_info, safe_destroy=True) if self.empty_state is not None: empty_bassinet.set_state(self.empty_state.state, self.empty_state) client = sim_info.client if client is not None: client.set_next_sim_or_none(only_if_this_active_sim_info=sim_info) client.selectable_sims.remove_selectable_sim_info(sim_info) sim_info.inject_into_inactive_zone(DeathTracker.DEATH_ZONE_ID, start_away_actions=False, skip_instanced_check=True, skip_daycare=True) sim_info.household.remove_sim_info(sim_info, destroy_if_empty_household=True) sim_info.transfer_to_hidden_household() sim_info.request_lod(SimInfoLODLevel.MINIMUM)
class HolidayTradition(HasTunableReference, HolidayTraditionDisplayMixin, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager( sims4.resources.Types.HOLIDAY_TRADITION)): INSTANCE_TUNABLES = { 'situation_goal': TunableReference( description= '\n This is the situation goal that will be offered when this tradition\n is active.\n ', manager=services.get_instance_manager( sims4.resources.Types.SITUATION_GOAL)), 'pre_holiday_buffs': TunableList( description= '\n A list of buffs that will be given out to all of the player Sims\n during the pre-holiday period of each holiday.\n ', tunable=TunableReference( description= '\n A buff that is given to all of the player Sims when it is the\n pre-holiday period.\n ', manager=services.get_instance_manager( sims4.resources.Types.BUFF)), unique_entries=True), 'pre_holiday_buff_reason': OptionalTunable( description= '\n If set, specify a reason why the buff was added.\n ', tunable=TunableLocalizedString( description= '\n The reason the buff was added. This will be displayed in the\n buff tooltip.\n ' )), 'holiday_buffs': TunableList( description= '\n A list of buffs that will be given out to all Sims during each\n holiday.\n ', tunable=TunableReference( description= '\n A buff that is given to all Sims during the holiday.\n ', manager=services.get_instance_manager( sims4.resources.Types.BUFF)), unique_entries=True), 'holiday_buff_reason': OptionalTunable( description= '\n If set, specify a reason why the buff was added.\n ', tunable=TunableLocalizedString( description= '\n The reason the buff was added. This will be displayed in the\n buff tooltip.\n ' )), 'drama_nodes_to_score': TunableList( description= '\n Drama nodes that we will attempt to schedule and score when this\n tradition becomes active.\n ', tunable=TunableReference( description= '\n A drama node that we will put in the scoring pass when this\n tradition becomes active.\n ', manager=services.get_instance_manager( sims4.resources.Types.DRAMA_NODE)), unique_entries=True), 'drama_nodes_to_run': TunableList( description= '\n Drama nodes that will be run when the tradition is activated.\n ', tunable=TunableReference( description= '\n A drama node that we will run when the holiday becomes active.\n ', manager=services.get_instance_manager( sims4.resources.Types.DRAMA_NODE)), unique_entries=True), 'additional_walkbys': SituationCurve.TunableFactory( description= '\n An additional walkby schedule that will be added onto the walkby\n schedule when the tradition is active.\n ', get_create_params={'user_facing': False}), 'preference': TunableList( description= '\n A list of pairs of preference categories and tests. To determine\n what a Sim feels about a tradition each set of tests in this list\n will be run in order. When one of the test sets passes then we\n will set that as the preference. If none of them pass we will\n default to LIKES.\n ', tunable=TunableTuple( description= '\n A pair of preference and test set.\n ', preference=TunableEnumEntry( description= '\n The preference that the Sim will have to this tradition if\n the test set passes.\n ', tunable_type=TraditionPreference, default=TraditionPreference.LIKES), tests=TunablePreferenceTestList( description= '\n A set of tests that need to pass for the Sim to have the\n tuned preference.\n ' ), reason=OptionalTunable( description= '\n If enabled then we will also give this reason as to why the\n preference is the way it is.\n ', tunable=TunableLocalizedString( description= '\n The reason that the Sim has this preference.\n ' )))), 'preference_reward_buff': OptionalTunable( description= '\n If enabled then if the Sim loves this tradition when the holiday is\n completed they will get a special buff if they completed the\n tradition.\n ', tunable=TunableBuffReference( description= '\n The buff given if this Sim loves the tradition and has completed\n it at the end of the holiday.\n ' )), 'selectable': Tunable( description= '\n If checked then this tradition will appear in the tradition\n selection.\n ', tunable_type=bool, default=True, tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'lifecycle_actions': TunableList( description= '\n Actions that occur as a result of the tradition activation/de-activation.\n ', tunable=TraditionActions.TunableFactory()), 'events': TunableList( description= '\n A list of times and things we want to happen at that time.\n ', tunable=TunableTuple( description= '\n A pair of a time of day and event of something that we want\n to occur.\n ', time=TunableTimeOfDay( description= '\n The time of day this event will occur.\n ' ), event=TunableVariant( description= '\n What we want to occur at this time.\n ', modify_items=ModifyAllItems(), start_situation=StartSituation(), default='start_situation'))), 'core_object_tags': TunableTags( description= '\n Tags of all the core objects used in this tradition.\n ', filter_prefixes=('func', ), tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'deco_object_tags': TunableTags( description= '\n Tags of all the deco objects used in this tradition.\n ', filter_prefixes=('func', ), tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'business_cost_multiplier': TunableMapping( description= '\n A mapping between the business type and the cost multiplier that\n we want to use if this tradition is active.\n ', key_type=TunableEnumEntry( description= '\n The type of business that we want to apply this price modifier\n on.\n ', tunable_type=BusinessType, default=BusinessType.INVALID, invalid_enums=(BusinessType.INVALID, )), value_type=TunableRange( description= '\n The value of the multiplier to use.\n ', tunable_type=float, default=1.0, minimum=0.0)) } @classmethod def _verify_tuning_callback(cls): if cls._display_data.instance_display_description is None: logger.error('Tradition {} missing display description', cls) if cls._display_data.instance_display_icon is None: logger.error('Tradition {} missing display icon', cls) if cls._display_data.instance_display_name is None: logger.error('Tradition {} missing display name', cls) def __init__(self): self._state = HolidayState.INITIALIZED self._buffs_added = defaultdict(list) self._event_alarm_handles = {} self._drama_node_processor = None @property def state(self): return self._state @classmethod def get_buiness_multiplier(cls, business_type): return cls.business_cost_multiplier.get(business_type, 1.0) @classmethod def get_sim_preference(cls, sim_info): resolver = SingleSimResolver(sim_info) for possible_preference in cls.preference: if possible_preference.tests.run_tests(resolver): return (possible_preference.preference, possible_preference.reason) return (TraditionPreference.LIKES, None) def on_sim_spawned(self, sim): if self._state == HolidayState.PRE_DAY: if sim.is_npc: return for buff in self.pre_holiday_buffs: buff_handle = sim.add_buff( buff, buff_reason=self.pre_holiday_buff_reason) if buff_handle is not None: self._buffs_added[sim.sim_id].append(buff_handle) elif self._state == HolidayState.RUNNING: for buff in self.holiday_buffs: buff_handle = sim.add_buff( buff, buff_reason=self.holiday_buff_reason) if buff_handle is not None: self._buffs_added[sim.sim_id].append(buff_handle) def activate_pre_holiday(self): if self._state >= HolidayState.PRE_DAY: logger.error( 'Tradition {} is trying to be put into the pre_holiday, but is already in {} which is farther along.', self, self._state) return self._state = HolidayState.PRE_DAY if self.pre_holiday_buffs: services.sim_spawner_service().register_sim_spawned_callback( self.on_sim_spawned) for sim_info in services.active_household().instanced_sims_gen(): for buff in self.pre_holiday_buffs: buff_handle = sim_info.add_buff( buff, buff_reason=self.pre_holiday_buff_reason) if buff_handle is not None: self._buffs_added[sim_info.sim_id].append(buff_handle) def _remove_all_buffs(self): sim_info_manager = services.sim_info_manager() for (sim_id, buff_handles) in self._buffs_added.items(): sim_info = sim_info_manager.get(sim_id) if sim_info is None: continue if sim_info.Buffs is None: continue for buff_handle in buff_handles: sim_info.remove_buff(buff_handle) self._buffs_added.clear() def _deactivate_pre_holiday(self): if self.pre_holiday_buffs: services.sim_spawner_service().unregister_sim_spawned_callback( self.on_sim_spawned) self._remove_all_buffs() def deactivate_pre_holiday(self): if self._state != HolidayState.PRE_DAY: logger.error( 'Tradition {} is trying to deactivate the preday, but it is in the {} state, not that one.', self, self._state) self._state = HolidayState.SHUTDOWN self._deactivate_pre_holiday() def _create_event_alarm(self, key, event): def callback(_): event.event.perform(GlobalResolver()) del self._event_alarm_handles[key] now = services.time_service().sim_now time_to_event = now.time_till_next_day_time(event.time) if key in self._event_alarm_handles: alarms.cancel_alarm(self._event_alarm_handles[key]) self._event_alarm_handles[key] = alarms.add_alarm( self, time_to_event, callback) def _process_scoring_gen(self, timeline): try: yield from services.drama_scheduler_service( ).score_and_schedule_nodes_gen(self.drama_nodes_to_score, 1, timeline=timeline) except GeneratorExit: raise except Exception as exception: logger.exception('Exception while scoring DramaNodes: ', exc=exception, level=sims4.log.LEVEL_ERROR) finally: self._drama_node_processor = None def activate_holiday(self, from_load=False, from_customization=False): if self._state >= HolidayState.RUNNING: logger.error( 'Tradition {} is trying to be put into the Running state, but is already in {} which is farther along.', self, self._state) return self._deactivate_pre_holiday() self._state = HolidayState.RUNNING if self.holiday_buffs: services.sim_spawner_service().register_sim_spawned_callback( self.on_sim_spawned) for sim_info in services.sim_info_manager().instanced_sims_gen(): for buff in self.holiday_buffs: buff_handle = sim_info.add_buff( buff, buff_reason=self.holiday_buff_reason) if buff_handle is not None: self._buffs_added[sim_info.sim_id].append(buff_handle) for (key, event) in enumerate(self.events): self._create_event_alarm(key, event) if not from_load: resolver = GlobalResolver() for actions in self.lifecycle_actions: actions.try_perform( resolver, TraditionActivationEvent.TRADITION_ADD if from_customization else TraditionActivationEvent.HOLIDAY_ACTIVATE) if self.drama_nodes_to_score: sim_timeline = services.time_service().sim_timeline self._drama_node_processor = sim_timeline.schedule( elements.GeneratorElement(self._process_scoring_gen)) drama_scheduler = services.drama_scheduler_service() for drama_node in self.drama_nodes_to_run: drama_scheduler.run_node(drama_node, resolver) def deactivate_holiday(self, from_customization=False): if self._state != HolidayState.RUNNING: logger.error( 'Tradition {} is trying to deactivate the tradition, but it is in the {} state, not that one.', self, self._state) self._state = HolidayState.SHUTDOWN if self.holiday_buffs: services.sim_spawner_service().unregister_sim_spawned_callback( self.on_sim_spawned) self._remove_all_buffs() for alarm in self._event_alarm_handles.values(): alarms.cancel_alarm(alarm) self._event_alarm_handles.clear() resolver = GlobalResolver() for actions in self.lifecycle_actions: actions.try_perform( resolver, TraditionActivationEvent.TRADITION_REMOVE if from_customization else TraditionActivationEvent.HOLIDAY_DEACTIVATE) def get_additional_walkbys(self, predicate=lambda _: True): weighted_situations = self.additional_walkbys.get_weighted_situations( predicate=predicate) if weighted_situations is None: return () return weighted_situations
class Mood(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.mood_manager()): __qualname__ = 'Mood' INSTANCE_TUNABLES = {'mood_asm_param': OptionalTunable(description='\n If set, then this mood will specify an asm parameter to affect\n animations. If not set, then the ASM parameter will be determined by\n the second most prevalent mood.\n ', tunable=Tunable(description="\n The asm parameter for Sim's mood, if not set, will use 'xxx'\n from instance name pattern with 'mood_xxx'.\n ", tunable_type=str, default='', source_query=SourceQueries.SwingEnumNamePattern.format('mood')), enabled_name='Specify', disabled_name='Determined_By_Other_Moods'), 'intensity_thresholds': TunableList(int, description='\n List of thresholds at which the intensity of this mood levels up.\n If empty, this mood has a single threshold and all mood tuning lists should\n have a single item in them.\n For each threshold added, you may add a new item to the Buffs, Mood Names,\n Portrait Pose Indexes and Portrait Frames lists.'), 'buffs': TunableList(TunableBuffReference(reload_dependent=True), description='\n A list of buffs that will be added while this mood is the active mood\n on a Sim. \n The first item is applied for the initial intensity, and each\n subsequent item replaces the previous buff as the intensity levels up.'), 'mood_names': TunableList(TunableLocalizedString(), description='\n A list of localized names of this mood.\n The first item is applied for the initial intensity, and each\n subsequent item replaces the name as the intensity levels up.', export_modes=(ExportModes.ServerXML, ExportModes.ClientBinary)), 'portrait_pose_indexes': TunableList(Tunable(tunable_type=int, default=0), description='\n A list of the indexes of the pose passed to thumbnail generation on the\n client to pose the Sim portrait when they have this mood.\n You can find the list of poses in tuning\n (Client_ThumnailPoses)\n The first item is applied for the initial intensity, and each\n subsequent item replaces the pose as the intensity levels up.', export_modes=(ExportModes.ClientBinary,)), 'portrait_frames': TunableList(Tunable(tunable_type=str, default=''), description='\n A list of the frame labels (NOT numbers!) from the UI .fla file that the\n portrait should be set to when this mood is active. Determines\n background color, font color, etc.\n The first item is applied for the initial intensity, and each\n subsequent item replaces the pose as the intensity levels up.', export_modes=(ExportModes.ClientBinary,)), 'environment_scoring_commodity': Commodity.TunableReference(description="\n Defines the ranges and corresponding buffs to apply for this\n mood's environmental contribution.\n \n Be sure to tune min, max, and the different states. The\n convergence value is what will remove the buff. Suggested to be\n 0.\n "), 'descriptions': TunableList(TunableLocalizedString(), description='\n Description for the UI tooltip, per intensity.', export_modes=(ExportModes.ClientBinary,)), 'icons': TunableList(TunableResourceKey(None, resource_types=sims4.resources.CompoundTypes.IMAGE), description='\n Icon for the UI tooltip, per intensity.', export_modes=(ExportModes.ClientBinary,)), 'descriptions_age_override': TunableMapping(description='\n Mapping of age to descriptions text for mood. If age does not\n exist in mapping will use default description text.\n ', key_type=TunableEnumEntry(sim_info_types.Age, sim_info_types.Age.CHILD), value_type=TunableList(description='\n Description for the UI tooltip, per intensity.\n ', tunable=TunableLocalizedString()), key_name='Age', value_name='description_text', export_modes=(ExportModes.ClientBinary,)), 'descriptions_trait_override': TunableMoodDescriptionTraitOverride(description='\n Trait override for mood descriptions. If a Sim has this trait\n and there is not a valid age override for the Sim, this\n description text will be used.\n ', export_modes=(ExportModes.ClientBinary,)), 'audio_stings_on_add': TunableList(description="\n The audio to play when a mood or it's intensity changes. Tune one for each intensity on the mood.\n ", tunable=TunableResourceKey(description='\n The sound to play.\n ', default=None, resource_types=(sims4.resources.Types.PROPX,), export_modes=ExportModes.ClientBinary)), 'mood_colors': TunableList(description='\n A list of the colors displayed on the steel series mouse when the active Sim has this mood. The first item is applied for the initial intensity, and each subsequent item replaces the color as the intensity levels up.\n ', tunable=TunableVector3(description='\n Color.\n ', default=sims4.math.Vector3.ZERO(), export_modes=ExportModes.ClientBinary)), 'mood_frequencies': TunableList(description='\n A list of the flash frequencies on the steel series mouse when the active Sim has this mood. The first item is applied for the initial intensity, and each subsequent item replaces the value as the intensity levels up. 0 => solid color, otherwise, value => value hertz.\n ', tunable=Tunable(tunable_type=float, default=0.0, description=',\n Hertz.\n ', export_modes=ExportModes.ClientBinary)), 'buff_polarity': TunableEnumEntry(description='\n Setting the polarity will determine how up/down arrows\n appear for any buff that provides this mood.\n ', tunable_type=BuffPolarity, default=BuffPolarity.NEUTRAL, tuning_group=GroupNames.UI, needs_tuning=True, export_modes=ExportModes.All), 'is_changeable': Tunable(description='\n If this is checked, any buff with this mood will change to\n the highest current mood of the same polarity. If there is no mood\n with the same polarity it will default to use this mood type\n ', tunable_type=bool, default=False, needs_tuning=True)} _asm_param_name = None excluding_traits = None @classmethod def _tuning_loaded_callback(cls): cls._asm_param_name = cls.mood_asm_param if not cls._asm_param_name: name_list = cls.__name__.split('_', 1) if len(name_list) <= 1: logger.error("Mood {} has an invalid name for asm parameter, please either set 'mood_asm_param' or change the tuning file name to match the format 'mood_xxx'.", cls.__name__) cls._asm_param_name = name_list[1] cls._asm_param_name = cls._asm_param_name.lower() for buff_ref in cls.buffs: my_buff = buff_ref.buff_type while my_buff is not None: if my_buff.mood_type is not None: logger.error('Mood {} will apply a buff ({}) that affects mood. This can cause mood calculation errors. Please select a different buff or remove the mood change.', cls.__name__, my_buff.mood_type.__name__) my_buff.is_mood_buff = True prev_threshold = 0 for threshold in cls.intensity_thresholds: if threshold <= prev_threshold: logger.error('Mood {} has Intensity Thresholds in non-ascending order.') break prev_threshold = threshold @classmethod def _verify_tuning_callback(cls): num_thresholds = len(cls.intensity_thresholds) + 1 if len(cls.buffs) != num_thresholds: logger.error('Mood {} does not have the correct number of Buffs tuned. It has {} thresholds, but {} buffs.', cls.__name__, num_thresholds, len(cls.buffs)) if len(cls.mood_names) != num_thresholds: logger.error('Mood {} does not have the correct number of Mood Names tuned. It has {} thresholds, but {} names.', cls.__name__, num_thresholds, len(cls.mood_names)) if len(cls.portrait_pose_indexes) != num_thresholds: logger.error('Mood {} does not have the correct number of Portrait Pose Indexes tuned. It has {} thresholds, but {} poses.', cls.__name__, num_thresholds, len(cls.portrait_pose_indexes)) if len(cls.portrait_frames) != num_thresholds: logger.error('Mood {} does not have the correct number of Portrait Frames tuned. It has {} thresholds, but {} frames.', cls.__name__, num_thresholds, len(cls.portrait_frames)) for (age, descriptions) in cls.descriptions_age_override.items(): while len(descriptions) != num_thresholds: logger.error('Mood {} does not have the correct number of descriptions age override tuned. For age:({}) It has {} thresholds, but {} descriptions.', cls.__name__, age, num_thresholds, len(descriptions)) if cls.descriptions_trait_override.trait is not None and len(cls.descriptions_trait_override.descriptions) != num_thresholds: logger.error('Mood {} does not have the correct number of trait override descriptions tuned. For trait:({}) It has {} thresholds, but {} descriptions.', cls.__name__, cls.descriptions_trait_override.trait.__name__, num_thresholds, len(cls.descriptions_trait_override.descriptions)) @classproperty def asm_param_name(cls): return cls._asm_param_name
class DetectiveCareerEventZoneDirector(CareerEventZoneDirector): INSTANCE_TUNABLES = {'criminal_trait': TunableReference(description='\n The trait that signifies that a sim is a criminal at the police station.\n ', manager=services.get_instance_manager(sims4.resources.Types.TRAIT)), 'in_holding_cell_buff': TunableBuffReference(description='\n The buff that indicates that this sim is in a holding cell.\n ')} def _did_sim_overstay(self, sim_info): if sim_info.has_trait(self.criminal_trait): return False if sim_info.has_buff(self.in_holding_cell_buff.buff_type): return False return super()._did_sim_overstay(sim_info)
def __init__(self, **kwargs): super().__init__(value=Tunable(description='\n lower bound value of the commodity state\n ', tunable_type=int, default=0, export_modes=ExportModes.All), buff=TunableBuffReference(description='\n Buff that will get added to sim when commodity is at\n this current state.\n ', reload_dependent=True), buff_add_threshold=OptionalTunable(TunableThreshold(description='\n When enabled, buff will not be added unless threshold\n has been met. Value for threshold must be within this\n commodity state.\n ')), icon=TunableResourceKey(description='\n Icon that is displayed for the current state of this\n commodity.\n ', default='PNG:missing_image', resource_types=sims4.resources.CompoundTypes.IMAGE, export_modes=ExportModes.All), fill_level=TunableEnumEntry(description='\n If set, this will determine how to color the motive bar.\n ', tunable_type=MotiveFillColorLevel, default=MotiveFillColorLevel.NO_FILL, export_modes=ExportModes.All), data_description=TunableLocalizedString(description='\n Localized description of the current commodity state.\n ', export_modes=ExportModes.All), fill_color=TunableColor.TunableColorRGBA(description='\n Fill color for motive bar\n ', export_modes=(ExportModes.ClientBinary,)), background_color=TunableColor.TunableColorRGBA(description='\n Background color for motive bar\n ', export_modes=(ExportModes.ClientBinary,)), tooltip_icon_list=TunableList(description='\n A list of icons to show in the tooltip of this\n commodity state.\n ', tunable=TunableResourceKey(description='\n Icon that is displayed what types of objects help\n solve this motive.\n ', default='PNG:missing_image', resource_types=sims4.resources.CompoundTypes.IMAGE), export_modes=(ExportModes.ClientBinary,)), loot_list_on_enter=TunableList(description='\n List of loots that will be applied when commodity\n value enters this state if owner of the commodity is a sim.\n ', tunable=TunableReference(services.get_instance_manager(sims4.resources.Types.ACTION))), **kwargs)