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)
Exemple #2
0
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
Exemple #5
0
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)
Exemple #6
0
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
        })
Exemple #8
0
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)
Exemple #12
0
 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')
Exemple #13
0
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)
Exemple #15
0
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
Exemple #16
0
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, ))
Exemple #18
0
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)
Exemple #20
0
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
Exemple #24
0
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)
Exemple #26
0
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
Exemple #28
0
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
Exemple #29
0
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)