Beispiel #1
0
class EmergencyFrequency(HasTunableSingletonFactory, AutoFactoryInit):
    FACTORY_TUNABLES = {
        'max_emergency_situations_per_shift':
        TunableRange(
            description=
            '\n            The maximum number of times during a shift that an emergency\n            situation can be created/started.\n            ',
            tunable_type=int,
            default=1,
            minimum=0),
        'inital_lockout_in_sim_minutes':
        TunableSimMinute(
            description=
            '\n            The time, in Sim minutes, that pass at the beginning of a shift\n            before the first check for creating/starting an emergency happens.\n            ',
            default=60),
        'cool_down_in_sim_minutes':
        TunableSimMinute(
            description=
            '\n            How often a check for whether or not to create/start an emergency\n            happens.\n            ',
            default=60),
        'percent_chance_of_emergency':
        TunablePercent(
            description=
            '\n            The percentage chance that on any given check an emergency is to\n            to be created/started.\n            ',
            default=30),
        'weighted_situations':
        TunableList(
            description=
            '\n            A weighted list of situations to be used as emergencies. When a\n            check passes to create/start an emergency, this is the list\n            of emergency situations that will be chosen from.\n            ',
            tunable=TunableTuple(situation=Situation.TunableReference(),
                                 weight=Tunable(tunable_type=int, default=1)))
    }
Beispiel #2
0
class BillReduction(GlobalPolicyEffect, HasTunableSingletonFactory,
                    HasTunableFactory, AutoFactoryInit):
    FACTORY_TUNABLES = {
        'percent_reduction':
        TunableTuple(
            description=
            '\n            A mapping of bill reduction reason to percent reduction. Reasons for bill\n            reduction can be added to sims.bills tuning.\n            ',
            reduction_reason=TunableEnumEntry(
                description=
                '\n                Reason for bill reduction.\n                ',
                tunable_type=BillReductionEnum,
                default=BillReductionEnum.GlobalPolicy_ControlInvasiveSpecies),
            reduction_amount=TunablePercent(
                description=
                '\n                Percent by which all household bills are reduced.\n                ',
                default=50))
    }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def turn_on(self, _, from_load=False):
        services.global_policy_service().add_bill_reduction(
            self.percent_reduction.reduction_reason,
            self.percent_reduction.reduction_amount)

    def turn_off(self, _):
        services.global_policy_service().remove_bill_reduction(
            self.percent_reduction.reduction_reason)
Beispiel #3
0
 def __init__(self, **kwargs):
     super().__init__(percent_change_amount=TunablePercent(
         description=
         '\n                             Percent of current value of statistic should amount\n                             be changed.  If you want to decrease the amount by\n                             50% enter -50% into the tuning field.',
         default=-50,
         minimum=-100),
                      **kwargs)
Beispiel #4
0
def _get_tunable_household_member_list(template_type, is_optional=False):
    if template_type == SimTemplateType.PREMADE_HOUSEHOLD:
        template_reference_type = PremadeSimTemplate
    else:
        template_reference_type = TunableSimTemplate
    tuple_elements = {
        'sim_template':
        template_reference_type.TunableReference(
            description=
            '            \n            A template to use for creating a household member. If this\n            references a resource that is not installed, the household member is\n            ignored and the family is going to be created without this\n            individual.\n            ',
            pack_safe=is_optional),
        'household_member_tag':
        TunableEnumWithFilter(
            description=
            '            \n            Tag to be used to create relationship between sim members. This does\n            NOT have to be unique for all household templates. If you want to\n            add more tags in the tag tuning just add with prefix of\n            household_member.r.\n            ',
            tunable_type=tag.Tag,
            default=tag.Tag.INVALID,
            filter_prefixes=HOUSEHOLD_FILTER_PREFIX)
    }
    if is_optional:
        tuple_elements['chance'] = TunablePercent(
            description=
            '\n            The chance that this household member is created when the household\n            is created. This is useful for "optional" Sims. For example, you\n            might want to tune a third of typical nuclear families to own a dog,\n            should the resource be available.\n            ',
            default=100)
    else:
        tuple_elements['locked_args'] = {'chance': 1}
    return TunableList(
        description=
        '\n        A list of sim templates that will make up the sims in this household.\n        ',
        tunable=TunableTuple(**tuple_elements))
class RelationshipBitLock(metaclass=HashedTunedInstanceMetaclass,
                          manager=services.get_instance_manager(
                              sims4.resources.Types.RELATIONSHIP_LOCK)):
    INSTANCE_TUNABLES = {
        'group_id':
        TunableEnumEntry(
            description=
            '\n            The group that this lock applies to.  No two locks can belong to\n            the same group.\n            ',
            tunable_type=RelationshipBitType,
            default=RelationshipBitType.Invalid,
            invalid_enums=(RelationshipBitType.Invalid,
                           RelationshipBitType.NoGroup)),
        'timeout':
        TunableSimMinute(
            description=
            '\n            The amount of time in Sim minutes that this Relationship Bit Lock\n            will be locked before potentially allowing a Relationship Bit\n            Change.\n            ',
            default=360,
            minimum=1),
        'relock_percentage':
        TunablePercent(
            description=
            '\n            The percent chance that we will just relock this Relationship Bit\n            Lock and prevent a change when one attempts to occur.  If we are\n            relocked then we will not change the bit.\n            ',
            default=0)
    }
    relationship_bit_cache = None

    @classmethod
    def get_lock_type_for_group_id(cls, group_id):
        return cls.relationship_bit_cache.get(group_id, None)

    def __init__(self):
        self._locked_time = DATE_AND_TIME_ZERO

    @property
    def end_time(self):
        return self._locked_time + clock.interval_in_sim_minutes(self.timeout)

    def lock(self):
        self._locked_time = services.time_service().sim_now

    def unlock(self):
        self._locked_time = DATE_AND_TIME_ZERO

    def try_and_aquire_lock_permission(self):
        if self._locked_time == DATE_AND_TIME_ZERO:
            return True
        now = services.time_service().sim_now
        if now < self.end_time:
            return False
        elif sims4.random.random_chance(self.relock_percentage * 100):
            self.lock()
            return False
        return True

    def save(self, msg):
        msg.relationship_bit_lock_type = self.guid64
        msg.locked_time = self._locked_time.absolute_ticks()

    def load(self, msg):
        self._locked_time = DateAndTime(msg.locked_time)
class _TargetActionRules(HasTunableFactory, AutoFactoryInit):
    FACTORY_TUNABLES = {
        'chance':
        TunablePercent(
            description=
            '\n            A random chance of this action getting applied (default 100%).\n            ',
            default=100),
        'test':
        TunableTestSet(
            description=
            '\n            A test to decide whether or not to apply this particular set of actions to the target object.\n            ',
            tuning_group=GroupNames.TESTS),
        'actions':
        TunableList(
            description=
            '\n            A list of one or more ObjectRoutingBehaviorActions to run on the\n            target object after routing to it. These are applied in sequence.\n            ',
            tunable=TunableVariant(
                play_animation=ObjectRoutingBehaviorActionAnimation.
                TunableFactory(),
                destroy_objects=ObjectRoutingBehaviorActionDestroyObjects.
                TunableFactory(),
                apply_loot=ObjectRoutingBehaviorActionApplyLoot.TunableFactory(
                ),
                default='play_animation')),
        'abort_if_applied':
        Tunable(
            description=
            "\n            Don't run any further actions from this list of action rules if \n            conditions are met and this action is executed.\n            ",
            tunable_type=bool,
            default=False)
    }
class BroadcasterEffectStartFire(_BroadcasterEffectTested):
    __qualname__ = 'BroadcasterEffectStartFire'
    FACTORY_TUNABLES = {'percent_chance': TunablePercent(description='\n            A value between 0 - 100 which represents the percent chance to \n            start a fire when reacting to the broadcaster.\n            ', default=50)}

    def _apply_broadcaster_effect(self, broadcaster, affected_object):
        if random.random() <= self.percent_chance:
            fire_service = services.get_fire_service()
            fire_service.spawn_fire_at_object(affected_object)
class SuccessChance(HasTunableSingletonFactory, AutoFactoryInit):
    __qualname__ = 'SuccessChance'
    FACTORY_TUNABLES = {'base_chance': TunablePercent(description='\n            The basic chance of success.\n            ', default=100), 'multipliers': TunableList(description='\n            A list of multipliers to apply to base_chance.\n            ', tunable=TunableTuple(multiplier=TunableRange(description='\n                    The multiplier to apply to base_chance if the associated\n                    tests pass.\n                    ', tunable_type=float, default=1, minimum=0), tests=TunableTestSet(description='\n                    A series of tests that must pass in order for multiplier to\n                    be applied.\n                    ')))}

    def get_chance(self, participant_resolver):
        chance = self.base_chance
        for multiplier_data in self.multipliers:
            while multiplier_data.tests.run_tests(participant_resolver):
                chance *= multiplier_data.multiplier
        return min(chance, 1)
Beispiel #9
0
class ScheduleUtilityShutOff(GlobalPolicyEffect,
                             ZoneModifierHouseholdShutOffUtility,
                             HasTunableFactory):
    FACTORY_TUNABLES = {
        'chance':
        OptionalTunable(
            description=
            '\n            The percent chance that, after an effect is turned on, that utility\n            will turn off day-to-day. \n            ',
            tunable=TunablePercent(default=10)),
        'schedule_data':
        WeeklySchedule.TunableFactory(
            description=
            '\n            The information to schedule points during the week that\n            the Global Policy Effect, if enacted, will turn off the tuned\n            utility.\n            '
        )
    }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._schedule = None
        self._stop_schedule_entry = None

    def turn_on(self, global_policy_id, from_load=False):
        self._schedule = self.schedule_data(
            start_callback=self.scheduled_start_action,
            schedule_immediate=True)
        if not from_load:
            services.global_policy_service().add_utility_effect(
                global_policy_id, self)

    def turn_off(self, global_policy_id):
        household_id = services.active_household().id
        self.stop_action(household_id)
        if self._schedule is not None:
            self._schedule.destroy()
        if self._stop_schedule_entry is not None:
            alarms.cancel_alarm(self._stop_schedule_entry)
        services.global_policy_service().remove_utility_effect(
            global_policy_id)

    def scheduled_start_action(self, scheduler, alarm_data, extra_data):
        if self.chance and random.random() < self.chance:
            return
        household_id = services.active_household().id
        self.start_action(household_id)
        blackout_end_time = alarm_data[1] - alarm_data[0]
        self._stop_schedule_entry = alarms.add_alarm(
            self,
            blackout_end_time,
            lambda _: self.scheduled_stop_action(household_id),
            cross_zone=True)

    def scheduled_stop_action(self, household_id):
        self.stop_action(household_id)
Beispiel #10
0
class OutfitGeneratorRandomizationMixin:
    INSTANCE_TUNABLES = {
        'filter_flag':
        TunableEnumFlags(
            description=
            '\n            Define how to handle part randomization for the generated outfit.\n            ',
            enum_type=OutfitFilterFlag,
            default=OutfitFilterFlag.USE_EXISTING_IF_APPROPRIATE
            | OutfitFilterFlag.USE_VALID_FOR_LIVE_RANDOM,
            allow_no_flags=True),
        'body_type_chance_overrides':
        TunableMapping(
            description=
            '\n            Define body type chance overrides for the generate outfit. For\n            example, if BODYTYPE_HAT is mapped to 100%, then the outfit is\n            guaranteed to have a hat if any hat matches the specified tags.\n            \n            If used in an appearance modifier, these body types will contribute\n            to the flags that determine which body types can be generated,\n            regardless of their percent chance.\n            ',
            key_type=BodyType,
            value_type=TunablePercent(
                description=
                '\n                The chance that a part is applied to the corresponding body\n                type.\n                ',
                default=100)),
        'body_type_match_not_found_policy':
        TunableMapping(
            description=
            '\n            The policy we should take for a body type that we fail to find a\n            match for. Primary example is to use MATCH_NOT_FOUND_KEEP_EXISTING\n            for generating a tshirt and making sure a sim wearing full body has\n            a lower body cas part.\n            \n            If used in an appearance modifier, these body types will contribute\n            to the flags that determine which body types can be generated.\n            ',
            key_type=BodyType,
            value_type=MatchNotFoundPolicy)
    }
    FACTORY_TUNABLES = INSTANCE_TUNABLES

    def get_body_type_flags(self):
        tuned_flags = 0
        for body_type in itertools.chain(
                self.body_type_chance_overrides.keys(),
                self.body_type_match_not_found_policy.keys()):
            tuned_flags |= 1 << body_type
        return tuned_flags or BodyTypeFlag.CLOTHING_ALL

    def _generate_outfit(self,
                         sim_info,
                         outfit_category,
                         outfit_index=0,
                         tag_list=(),
                         seed=None):
        sim_info.generate_outfit(
            outfit_category,
            outfit_index=outfit_index,
            tag_list=tag_list,
            filter_flag=self.filter_flag,
            body_type_chance_overrides=self.body_type_chance_overrides,
            body_type_match_not_found_overrides=self.
            body_type_match_not_found_policy,
            seed=seed)
Beispiel #11
0
class SetFireState(HasTunableFactory, AutoFactoryInit):
    FACTORY_TUNABLES = {'chance': TunablePercent(description='\n            Chance that the fire will trigger\n            ', default=100)}

    def __init__(self, target, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.target = target

    def start(self, *_, **__):
        if random.random() < self.chance:
            fire_service = services.get_fire_service()
            fire_service.spawn_fire_at_object(self.target)

    def stop(self, *_, **__):
        pass
Beispiel #12
0
class SimInfoFireMeter:
    FIREMETER_FREQUENCY = Tunable(
        description=
        "\n        The game creates a repeating alarm at this frequency to check whether\n        the SimInfoManager has more than the fire meter's threshold before\n        triggering a purge of sim_infos.\n\n        Please consult a member of performance group before updating this\n        tuning.\n        ",
        tunable_type=int,
        default=120)
    SIM_INFO_ALLOWED_PERCENTAGE_ABOVE_CAP = TunablePercent(
        description=
        "\n        The game triggers an out-of-cycle story progression action to control\n        population when we cross the current sim info cap plus this percentage.\n        For instance, if this percentage is set to 15% and the current Sim Info\n        cap is 100 Sim Infos, we'll start culling Sim Infos once we cross 115\n        (100 + 15%).\n        \n        Please consult a member of the performance group before updating this\n        tuning.\n        ",
        default=15)

    def __init__(self):
        self._alarm_handle = alarms.add_alarm(
            self,
            create_time_span(minutes=self.FIREMETER_FREQUENCY),
            self._firemeter_callback,
            repeating=True,
            use_sleep_time=False)

    def shutdown(self):
        if self._alarm_handle is not None:
            alarms.cancel_alarm(self._alarm_handle)
            self._alarm_handle = None

    def _firemeter_callback(self, _):
        sim_info_manager = services.sim_info_manager()
        sim_info_count = len(sim_info_manager)
        sim_info_cap = sim_info_manager.SIM_INFO_CAP
        if sim_info_count <= sim_info_cap:
            return
        adjusted_sim_info_cap = int(
            sim_info_cap * (1 + self.SIM_INFO_ALLOWED_PERCENTAGE_ABOVE_CAP))
        if sim_info_count < adjusted_sim_info_cap:
            return
        logger.debug(
            'FireMeter: We have {} sim_infos in the save file. Current cap: {}. Cap after adjustments: {}. Purge begins.',
            sim_info_count, sim_info_cap, adjusted_sim_info_cap)
        self.trigger()

    def trigger(self):
        story_progression_service = services.get_story_progression_service()
        if story_progression_service is None:
            return
        for action in story_progression_service.ACTIONS:
            if isinstance(action, StoryProgressionActionMaxPopulation):
                action.process_action(StoryProgressionFlags.SIM_INFO_FIREMETER)
                break
Beispiel #13
0
 def __init__(self, **kwargs):
     super().__init__(
         valid_objects=TunableVariant(
             description=
             '\n            The items to which the rebate will be applied.\n            ',
             by_tag=TunableSet(
                 description=
                 '\n                The rebate will only be applied to objects purchased with the\n                tags in this list.\n                ',
                 tunable=TunableEnumEntry(tunable_type=tag.Tag,
                                          default=tag.Tag.INVALID,
                                          invalid_enums=(tag.Tag.INVALID, ))
             ),
             locked_args={'all_purchases': None},
             default='all_purchases'),
         rebate_payout_type=TunableVariant(
             percentage=TunablePercent(
                 description=
                 '\n                The percentage of the catalog price that the player will get\n                back in the rebate.\n                ',
                 default=10),
             per_item=TunableRange(
                 description=
                 '\n                The amount per valid object the player will get back in the\n                rebate\n                ',
                 tunable_type=int,
                 default=1,
                 minimum=1)),
         notification_text=TunableLocalizedStringFactory(
             description=
             '\n            A string representing the line item on the notification\n            explaining why Sims with this trait received a rebate.\n            \n            This string is provided one token: the percentage discount\n            obtained due to having this trait.\n            \n            e.g.:\n             {0.Number}% off for purchasing Art and leveraging Critical\n             Connections.\n            '
         ),
         tests_set=TunableTestSet(
             description=
             '\n            If these tests pass, then the object is scheduled for the next \n            scheduled rebate event.\n            '
         ),
         rebate_category=TunableVariant(
             description=
             '\n            Specify a rebate category for this rebate item.\n            \n            GAMEPLAY_OBJECT: A GAMEPLAY_OBJECT category rebate item has the option\n            of either being a one-time rebate or a cyclical rebate. If tests are\n            tuned, the object has two opportunities to get added to rebates\n            before the next scheduled rebate event: once on add and its tests\n            pass, the next when its tests pass.\n            \n            BUILD_BUY: A BUILD_BUY category rebate item will give a one-time rebate\n            of all the valid objects purchased through build-buy.\n            ',
             buildbuy=RebateCategory(
                 locked_args={
                     'rebate_category_enum': RebateCategoryEnum.BUILD_BUY,
                     'cyclical': False
                 }),
             gameplay_object=RebateCategory(locked_args={
                 'rebate_category_enum':
                 RebateCategoryEnum.GAMEPLAY_OBJECT
             }),
             default='buildbuy'))
class BalloonCategory(HasTunableReference,
                      metaclass=HashedTunedInstanceMetaclass,
                      manager=services.get_instance_manager(
                          sims4.resources.Types.BALLOON)):
    INSTANCE_TUNABLES = {
        'balloon_type':
        TunableEnumEntry(
            description=
            '\n             The visual style of the balloon background.\n             ',
            tunable_type=BalloonTypeEnum,
            default=BalloonTypeEnum.THOUGHT),
        'balloon_chance':
        TunablePercent(
            description=
            '\n             The chance that a balloon from the list is actually shown.\n             ',
            default=100),
        'balloons':
        TunableList(
            description=
            '\n             The list of possible balloons.\n             ',
            tunable=BalloonVariant.TunableFactory(balloon_type=None))
    }

    @classmethod
    def get_balloon_icons(cls,
                          resolver,
                          balloon_type=DEFAULT,
                          gsi_category=None,
                          **kwargs):
        if gsi_category is None:
            gsi_category = cls.__name__
        else:
            gsi_category = '{}/{}'.format(gsi_category, cls.__name__)
        possible_balloons = []
        if random.random() <= cls.balloon_chance:
            for balloon in cls.balloons:
                for balloon_icon in balloon.get_balloon_icons(
                        resolver,
                        balloon_type=cls.balloon_type,
                        gsi_category=gsi_category,
                        **kwargs):
                    if balloon_icon:
                        possible_balloons.append(balloon_icon)
        return possible_balloons
Beispiel #15
0
class SuccessChance(HasTunableSingletonFactory, AutoFactoryInit):
    ONE = None
    FACTORY_TUNABLES = {
        'base_chance':
        TunablePercent(
            description=
            '\n            The basic chance of success.\n            ',
            default=100),
        'multipliers':
        TunableList(
            description=
            '\n            A list of multipliers to apply to base_chance.\n            ',
            tunable=TunableTuple(
                multiplier=TunableRange(
                    description=
                    '\n                    The multiplier to apply to base_chance if the associated\n                    tests pass.\n                    ',
                    tunable_type=float,
                    default=1,
                    minimum=0),
                tests=TunableTestSet(
                    description=
                    '\n                    A series of tests that must pass in order for multiplier to\n                    be applied.\n                    '
                )))
    }

    def get_chance(self, participant_resolver):
        chance = self.base_chance
        for multiplier_data in self.multipliers:
            if multiplier_data.tests.run_tests(participant_resolver):
                chance *= multiplier_data.multiplier
        return min(chance, 1)

    def __hash__(self):
        return hash(self.base_chance) ^ hash(self.multipliers)

    def __eq__(self, other_chance):
        if type(self) is not type(other_chance):
            return False
        return self.base_chance == other_chance.base_chance and self.multipliers == other_chance.multipliers

    def __ne__(self, other_chance):
        return not self.__eq__(other_chance)
class OtherEvaluation(EvaluationBase):
    FACTORY_TUNABLES = {
        'base_chance':
        TunablePercent(
            description=
            '\n            The base chance a scholarship is earned.\n            ',
            default=20),
        'additive_chance_scores':
        TunableReference(
            description=
            '\n            For each passing score, the sum is added onto the base\n            scholarship acceptance chance.\n            ',
            manager=services.test_based_score_manager())
    }

    def get_value(self, sim_info):
        pass

    def get_score(self, _, resolver, **kwargs):
        return self.base_chance * 100 + self.additive_chance_scores.get_score(
            resolver)
Beispiel #17
0
class NormalizeStatisticsOp(BaseTargetedLootOperation):
    __qualname__ = 'NormalizeStatisticsOp'
    FACTORY_TUNABLES = {
        'stats_to_normalize':
        TunableList(
            description=
            '\n            Stats to be affected by the normalization.\n            ',
            tunable=TunableReference(
                services.get_instance_manager(sims4.resources.Types.STATISTIC),
                class_restrictions=statistics.commodity.Commodity)),
        'normalize_percent':
        TunablePercent(
            description=
            '\n            In seeking the average value, this is the percent of movement toward the average value \n            the stat will move to achieve the new value. For example, if you have a Sim with 50 \n            fun, and a Sim with 100 fun, and want to normalize them exactly halfway to their \n            average of 75, tune this to 100%. A value of 50% would move one Sim to 67.5 and the other\n            to 77.5\n            ',
            default=100,
            maximum=100,
            minimum=0)
    }

    def __init__(self, stats_to_normalize, normalize_percent, **kwargs):
        super().__init__(**kwargs)
        self._stats = stats_to_normalize
        self._normalize_percent = normalize_percent

    def _apply_to_subject_and_target(self, subject, target, resolver):
        for stat_type in self._stats:
            source_tracker = target.get_tracker(stat_type)
            if not source_tracker.has_statistic(stat_type):
                pass
            target_tracker = subject.get_tracker(stat_type)
            source_value = source_tracker.get_value(stat_type)
            target_value = target_tracker.get_value(stat_type)
            average_value = (source_value + target_value) / 2
            source_percent_gain = (source_value -
                                   average_value) * self._normalize_percent
            target_percent_gain = (target_value -
                                   average_value) * self._normalize_percent
            target_tracker.set_value(stat_type,
                                     source_value - source_percent_gain)
            source_tracker.set_value(stat_type,
                                     target_value - target_percent_gain)
Beispiel #18
0
    class _Inheritance(HasTunableSingletonFactory, AutoFactoryInit):
        __qualname__ = 'GardeningTuning._Inheritance'

        @staticmethod
        def _verify_tunable_callback(instance_class, tunable_name, source,
                                     value):
            if not value.inherit_from_mother and not value.inherit_from_father:
                raise ValueError('Must inherit from at least one parent.')

        FACTORY_TUNABLES = {
            'inherited_state':
            ObjectState.TunableReference(
                description=
                '\n            Controls the state value that will be inherited by offspring.\n            '
            ),
            'inherit_from_mother':
            Tunable(
                description=
                "\n            If checked, the mother's (root stock's) state value and fitness will\n            be considered when deciding what state value the child should\n            inherit.\n            ",
                tunable_type=bool,
                needs_tuning=True,
                default=True),
            'inherit_from_father':
            Tunable(
                description=
                "\n            If checked, the father's (a spliced fruit's genes) state value and\n            fitness will be considered when deciding what state value the child\n            should inherit.  In the case a plant is spawning the type of fruit\n            it grew from, this will be the same as the mother's contribution.\n            ",
                tunable_type=bool,
                needs_tuning=True,
                default=True),
            'inheritance_chance':
            TunablePercent(
                description=
                "\n            The chance the offspring will inherit this state value from its\n            parents at all.  If the check doesn't pass, the default value for\n            the state will be used.\n            ",
                default=1),
            'verify_tunable_callback':
            _verify_tunable_callback
        }
class PregnancyTracker(SimInfoTracker):
    PREGNANCY_COMMODITY_MAP = TunableMapping(
        description=
        '\n        The commodity to award if conception is successful.\n        ',
        key_type=TunableEnumEntry(
            description=
            '\n            Species these commodities are intended for.\n            ',
            tunable_type=Species,
            default=Species.HUMAN,
            invalid_enums=(Species.INVALID, )),
        value_type=TunableReference(
            description=
            '\n            The commodity reference controlling pregnancy.\n            ',
            pack_safe=True,
            manager=services.get_instance_manager(
                sims4.resources.Types.STATISTIC)))
    PREGNANCY_TRAIT = TunableReference(
        description=
        '\n        The trait that all pregnant Sims have during pregnancy.\n        ',
        manager=services.trait_manager())
    PREGNANCY_ORIGIN_TRAIT_MAPPING = TunableMapping(
        description=
        '\n        A mapping from PregnancyOrigin to a set of traits to be added at the\n        start of the pregnancy, and removed at the end of the pregnancy.\n        ',
        key_type=PregnancyOrigin,
        value_type=TunableTuple(
            description=
            '\n            A tuple of the traits that should be added/removed with a pregnancy\n            that has this origin, and the content pack they are associated with.\n            ',
            traits=TunableSet(
                description=
                '\n                The traits to be added/removed.\n                ',
                tunable=Trait.TunablePackSafeReference()),
            pack=TunableEnumEntry(
                description=
                '\n                The content pack associated with this set of traits. If the pack\n                is uninstalled, the pregnancy will be auto-completed.\n                ',
                tunable_type=Pack,
                default=Pack.BASE_GAME)))
    PREGNANCY_RATE = TunableRange(
        description='\n        The rate per Sim minute of pregnancy.\n        ',
        tunable_type=float,
        default=0.001,
        minimum=EPSILON)
    MULTIPLE_OFFSPRING_CHANCES = TunableList(
        description=
        '\n        A list defining the probabilities of multiple births.\n        ',
        tunable=TunableTuple(
            size=Tunable(
                description=
                '\n                The number of offspring born.\n                ',
                tunable_type=int,
                default=1),
            weight=Tunable(
                description=
                '\n                The weight, relative to other outcomes.\n                ',
                tunable_type=float,
                default=1),
            npc_dialog=UiDialogOk.TunableFactory(
                description=
                '\n                A dialog displayed when a NPC Sim gives birth to an offspring\n                that was conceived by a currently player-controlled Sim. The\n                dialog is specifically used when this number of offspring is\n                generated.\n                \n                Three tokens are passed in: the two parent Sims and the\n                offspring\n                ',
                locked_args={'text_tokens': None}),
            modifiers=TunableMultiplier.TunableFactory(
                description=
                '\n                A tunable list of test sets and associated multipliers to apply\n                to the total chance of this number of potential offspring.\n                '
            ),
            screen_slam_one_parent=OptionalTunable(
                description=
                '\n                Screen slam to show when only one parent is available.\n                Localization Tokens: Sim A - {0.SimFirstName}\n                ',
                tunable=TunableScreenSlamSnippet()),
            screen_slam_two_parents=OptionalTunable(
                description=
                '\n                Screen slam to show when both parents are available.\n                Localization Tokens: Sim A - {0.SimFirstName}, Sim B -\n                {1.SimFirstName}\n                ',
                tunable=TunableScreenSlamSnippet())))
    MONOZYGOTIC_OFFSPRING_CHANCE = TunablePercent(
        description=
        '\n        The chance that each subsequent offspring of a multiple birth has the\n        same genetics as the first offspring.\n        ',
        default=50)
    GENDER_CHANCE_STAT = TunableReference(
        description=
        '\n        A commodity that determines the chance that an offspring is female. The\n        minimum value guarantees the offspring is male, whereas the maximum\n        value guarantees it is female.\n        ',
        manager=services.statistic_manager())
    BIRTHPARENT_BIT = RelationshipBit.TunableReference(
        description=
        '\n        The bit that is added on the relationship from the Sim to any of its\n        offspring.\n        '
    )
    AT_BIRTH_TESTS = TunableGlobalTestSet(
        description=
        '\n        Tests to run between the pregnant sim and their partner, at the time of\n        birth. If any test fails, the the partner sim will not be set as the\n        other parent. This is intended to prevent modifications to the partner\n        sim during the time between impregnation and birth that would make the\n        partner sim an invalid parent (age too young, relationship incestuous, etc).\n        '
    )
    PREGNANCY_ORIGIN_MODIFIERS = TunableMapping(
        description=
        '\n        Define any modifiers that, given the origination of the pregnancy,\n        affect certain aspects of the generated offspring.\n        ',
        key_type=TunableEnumEntry(
            description=
            '\n            The origin of the pregnancy.\n            ',
            tunable_type=PregnancyOrigin,
            default=PregnancyOrigin.DEFAULT,
            pack_safe=True),
        value_type=TunableTuple(
            description=
            '\n            The aspects of the pregnancy modified specifically for the specified\n            origin.\n            ',
            default_relationships=TunableTuple(
                description=
                '\n                Override default relationships for the parents.\n                ',
                father_override=OptionalTunable(
                    description=
                    '\n                    If set, override default relationships for the father.\n                    ',
                    tunable=TunableEnumEntry(
                        description=
                        '\n                        The default relationships for the father.\n                        ',
                        tunable_type=DefaultGenealogyLink,
                        default=DefaultGenealogyLink.FamilyMember)),
                mother_override=OptionalTunable(
                    description=
                    '\n                    If set, override default relationships for the mother.\n                    ',
                    tunable=TunableEnumEntry(
                        description=
                        '\n                        The default relationships for the mother.\n                        ',
                        tunable_type=DefaultGenealogyLink,
                        default=DefaultGenealogyLink.FamilyMember))),
            trait_entries=TunableList(
                description=
                '\n                Sets of traits that might be randomly applied to each generated\n                offspring. Each group is individually randomized.\n                ',
                tunable=TunableTuple(
                    description=
                    '\n                    A set of random traits. Specify a chance that a trait from\n                    the group is selected, and then specify a set of traits.\n                    Only one trait from this group may be selected. If the\n                    chance is less than 100%, no traits could be selected.\n                    ',
                    chance=TunablePercent(
                        description=
                        '\n                        The chance that a trait from this set is selected.\n                        ',
                        default=100),
                    traits=TunableList(
                        description=
                        '\n                        The set of traits that might be applied to each\n                        generated offspring. Specify a weight for each trait\n                        compared to other traits in the same set.\n                        ',
                        tunable=TunableTuple(
                            description=
                            '\n                            A weighted trait that might be applied to the\n                            generated offspring. The weight is relative to other\n                            entries within the same set.\n                            ',
                            weight=Tunable(
                                description=
                                '\n                                The relative weight of this trait compared to\n                                other traits within the same set.\n                                ',
                                tunable_type=float,
                                default=1),
                            trait=Trait.TunableReference(
                                description=
                                '\n                                A trait that might be applied to the generated\n                                offspring.\n                                ',
                                pack_safe=True)))))))

    def __init__(self, sim_info):
        self._sim_info = sim_info
        self._clear_pregnancy_data()
        self._completion_callback_listener = None
        self._completion_alarm_handle = None

    @property
    def account(self):
        return self._sim_info.account

    @property
    def is_pregnant(self):
        if self._seed:
            return True
        return False

    @property
    def offspring_count(self):
        return max(len(self._offspring_data), 1)

    @property
    def offspring_count_override(self):
        return self._offspring_count_override

    @offspring_count_override.setter
    def offspring_count_override(self, value):
        self._offspring_count_override = value

    def _get_parent(self, sim_id):
        sim_info_manager = services.sim_info_manager()
        if sim_id in sim_info_manager:
            return sim_info_manager.get(sim_id)

    def get_parents(self):
        if self._parent_ids:
            parent_a = self._get_parent(self._parent_ids[0])
            parent_b = self._get_parent(self._parent_ids[1]) or parent_a
            return (parent_a, parent_b)
        return (None, None)

    def get_partner(self):
        (owner, partner) = self.get_parents()
        if partner is not owner:
            return partner

    def start_pregnancy(self,
                        parent_a,
                        parent_b,
                        pregnancy_origin=PregnancyOrigin.DEFAULT):
        if self.is_pregnant:
            return
        if not parent_a.incest_prevention_test(parent_b):
            return
        self._seed = random.randint(1, MAX_UINT32)
        self._parent_ids = (parent_a.id, parent_b.id)
        self._offspring_data = []
        self._origin = pregnancy_origin
        self.enable_pregnancy()

    def enable_pregnancy(self):
        if self.is_pregnant:
            if not self._is_enabled:
                pregnancy_commodity_type = self.PREGNANCY_COMMODITY_MAP.get(
                    self._sim_info.species)
                tracker = self._sim_info.get_tracker(pregnancy_commodity_type)
                pregnancy_commodity = tracker.get_statistic(
                    pregnancy_commodity_type, add=True)
                pregnancy_commodity.add_statistic_modifier(self.PREGNANCY_RATE)
                threshold = sims4.math.Threshold(pregnancy_commodity.max_value,
                                                 operator.ge)
                self._completion_callback_listener = tracker.create_and_add_listener(
                    pregnancy_commodity.stat_type, threshold,
                    self._on_pregnancy_complete)
                if threshold.compare(pregnancy_commodity.get_value()):
                    self._on_pregnancy_complete()
                tracker = self._sim_info.get_tracker(self.GENDER_CHANCE_STAT)
                tracker.add_statistic(self.GENDER_CHANCE_STAT)
                self._sim_info.add_trait(self.PREGNANCY_TRAIT)
                traits_pack_tuple = self.PREGNANCY_ORIGIN_TRAIT_MAPPING.get(
                    self._origin)
                if traits_pack_tuple is not None:
                    for trait in traits_pack_tuple.traits:
                        self._sim_info.add_trait(trait)
                self._is_enabled = True

    def _on_pregnancy_complete(self, *_, **__):
        if not self.is_pregnant:
            return
        if self._sim_info.is_npc:
            current_zone = services.current_zone()
            if not current_zone.is_zone_running or self._sim_info.is_instanced(
                    allow_hidden_flags=ALL_HIDDEN_REASONS):
                if self._completion_alarm_handle is None:
                    self._completion_alarm_handle = alarms.add_alarm(
                        self,
                        clock.interval_in_sim_minutes(1),
                        self._on_pregnancy_complete,
                        repeating=True,
                        cross_zone=True)
            else:
                self._create_and_name_offspring()
                self._show_npc_dialog()
                self.clear_pregnancy()

    def complete_pregnancy(self):
        services.get_event_manager().process_event(
            TestEvent.OffspringCreated,
            sim_info=self._sim_info,
            offspring_created=self.offspring_count)
        for tuning_data in self.MULTIPLE_OFFSPRING_CHANCES:
            if tuning_data.size == self.offspring_count:
                (parent_a, parent_b) = self.get_parents()
                if parent_a is parent_b:
                    screen_slam = tuning_data.screen_slam_one_parent
                else:
                    screen_slam = tuning_data.screen_slam_two_parents
                if screen_slam is not None:
                    screen_slam.send_screen_slam_message(
                        self._sim_info, parent_a, parent_b)
                break

    def _clear_pregnancy_data(self):
        self._seed = 0
        self._parent_ids = []
        self._offspring_data = []
        self._offspring_count_override = None
        self._origin = PregnancyOrigin.DEFAULT
        self._is_enabled = False

    def clear_pregnancy_visuals(self):
        if self._sim_info.pregnancy_progress:
            self._sim_info.pregnancy_progress = 0

    def clear_pregnancy(self):
        pregnancy_commodity_type = self.PREGNANCY_COMMODITY_MAP.get(
            self._sim_info.species)
        tracker = self._sim_info.get_tracker(pregnancy_commodity_type)
        if tracker is not None:
            stat = tracker.get_statistic(pregnancy_commodity_type, add=True)
            if stat is not None:
                stat.set_value(stat.min_value)
                stat.remove_statistic_modifier(self.PREGNANCY_RATE)
            if self._completion_callback_listener is not None:
                tracker.remove_listener(self._completion_callback_listener)
                self._completion_callback_listener = None
        tracker = self._sim_info.get_tracker(self.GENDER_CHANCE_STAT)
        if tracker is not None:
            tracker.remove_statistic(self.GENDER_CHANCE_STAT)
        if self._sim_info.has_trait(self.PREGNANCY_TRAIT):
            self._sim_info.remove_trait(self.PREGNANCY_TRAIT)
        traits_pack_tuple = self.PREGNANCY_ORIGIN_TRAIT_MAPPING.get(
            self._origin)
        if traits_pack_tuple is not None:
            for trait in traits_pack_tuple.traits:
                if self._sim_info.has_trait(trait):
                    self._sim_info.remove_trait(trait)
        if self._completion_alarm_handle is not None:
            alarms.cancel_alarm(self._completion_alarm_handle)
            self._completion_alarm_handle = None
        self.clear_pregnancy_visuals()
        self._clear_pregnancy_data()

    def _create_and_name_offspring(self, on_create=None):
        self.create_offspring_data()
        for offspring_data in self.get_offspring_data_gen():
            offspring_data.first_name = self._get_random_first_name(
                offspring_data)
            sim_info = self.create_sim_info(offspring_data)
            if on_create is not None:
                on_create(sim_info)

    def validate_partner(self):
        impregnator = self.get_partner()
        if impregnator is None:
            return
        resolver = DoubleSimResolver(self._sim_info, impregnator)
        if not self.AT_BIRTH_TESTS.run_tests(resolver):
            self._parent_ids = (self._sim_info.id, self._sim_info.id)

    def create_sim_info(self, offspring_data):
        self.validate_partner()
        (parent_a, parent_b) = self.get_parents()
        sim_creator = SimCreator(age=offspring_data.age,
                                 gender=offspring_data.gender,
                                 species=offspring_data.species,
                                 first_name=offspring_data.first_name,
                                 last_name=offspring_data.last_name)
        household = self._sim_info.household
        zone_id = household.home_zone_id
        (sim_info_list, _) = SimSpawner.create_sim_infos(
            (sim_creator, ),
            household=household,
            account=self.account,
            zone_id=zone_id,
            generate_deterministic_sim=True,
            creation_source='pregnancy')
        sim_info = sim_info_list[0]
        sim_info.world_id = services.get_persistence_service(
        ).get_world_id_from_zone(zone_id)
        for trait in tuple(sim_info.trait_tracker.personality_traits):
            sim_info.remove_trait(trait)
        for trait in offspring_data.traits:
            sim_info.add_trait(trait)
        sim_info.apply_genetics(parent_a,
                                parent_b,
                                seed=offspring_data.genetics)
        sim_info.resend_extended_species()
        sim_info.resend_physical_attributes()
        default_track_overrides = {}
        mother = parent_a if parent_a.gender == Gender.FEMALE else parent_b
        father = parent_a if parent_a.gender == Gender.MALE else parent_b
        if self._origin in self.PREGNANCY_ORIGIN_MODIFIERS:
            father_override = self.PREGNANCY_ORIGIN_MODIFIERS[
                self._origin].default_relationships.father_override
            if father_override is not None:
                default_track_overrides[father] = father_override
            mother_override = self.PREGNANCY_ORIGIN_MODIFIERS[
                self._origin].default_relationships.mother_override
            if mother_override is not None:
                default_track_overrides[mother] = mother_override
        self.initialize_sim_info(
            sim_info,
            parent_a,
            parent_b,
            default_track_overrides=default_track_overrides)
        self._sim_info.relationship_tracker.add_relationship_bit(
            sim_info.id, self.BIRTHPARENT_BIT)
        return sim_info

    @staticmethod
    def initialize_sim_info(sim_info,
                            parent_a,
                            parent_b,
                            default_track_overrides=None):
        sim_info.add_parent_relations(parent_a, parent_b)
        if sim_info.household is not parent_a.household:
            parent_a.household.add_sim_info_to_household(sim_info)
        sim_info.set_default_relationships(
            reciprocal=True, default_track_overrides=default_track_overrides)
        services.sim_info_manager().set_default_genealogy(
            sim_infos=(sim_info, ))
        parent_generation = max(
            parent_a.generation,
            parent_b.generation if parent_b is not None else 0)
        sim_info.generation = parent_generation + 1 if sim_info.is_played_sim else parent_generation
        services.get_event_manager().process_event(TestEvent.GenerationCreated,
                                                   sim_info=sim_info)
        client = services.client_manager().get_client_by_household_id(
            sim_info.household_id)
        if client is not None:
            client.add_selectable_sim_info(sim_info)
        parent_b_sim_id = parent_b.sim_id if parent_b is not None else 0
        RelgraphService.relgraph_add_child(parent_a.sim_id, parent_b_sim_id,
                                           sim_info.sim_id)

    @classmethod
    def select_traits_for_offspring(cls,
                                    offspring_data,
                                    parent_a,
                                    parent_b,
                                    num_traits,
                                    origin=PregnancyOrigin.DEFAULT,
                                    random=random):
        traits = []
        personality_trait_slots = num_traits

        def _add_trait_if_possible(selected_trait):
            nonlocal personality_trait_slots
            if selected_trait in traits:
                return False
            if any(t.is_conflicting(selected_trait) for t in traits):
                return False
            if selected_trait.is_personality_trait:
                if not personality_trait_slots:
                    return False
                personality_trait_slots -= 1
            traits.append(selected_trait)
            return True

        if origin in cls.PREGNANCY_ORIGIN_MODIFIERS:
            trait_entries = cls.PREGNANCY_ORIGIN_MODIFIERS[
                origin].trait_entries
            for trait_entry in trait_entries:
                if random.random() >= trait_entry.chance:
                    continue
                selected_trait = pop_weighted(
                    [(t.weight, t.trait) for t in trait_entry.traits
                     if t.trait.is_valid_trait(offspring_data)],
                    random=random)
                if selected_trait is not None:
                    _add_trait_if_possible(selected_trait)
        if parent_a is not None:
            if parent_b is not None:
                for inherited_trait_entries in parent_a.trait_tracker.get_inherited_traits(
                        parent_b):
                    selected_trait = pop_weighted(
                        list(inherited_trait_entries), random=random)
                    if selected_trait is not None:
                        _add_trait_if_possible(selected_trait)
        if not personality_trait_slots:
            return traits
        personality_traits = get_possible_traits(offspring_data)
        random.shuffle(personality_traits)
        while True:
            current_trait = personality_traits.pop()
            if _add_trait_if_possible(current_trait):
                break
            if not personality_traits:
                return traits
        if not personality_trait_slots:
            return traits
        traits_a = set(parent_a.trait_tracker.personality_traits)
        traits_b = set(parent_b.trait_tracker.personality_traits)
        shared_parent_traits = list(
            traits_a.intersection(traits_b) - set(traits))
        random.shuffle(shared_parent_traits)
        while personality_trait_slots:
            while shared_parent_traits:
                current_trait = shared_parent_traits.pop()
                if current_trait in personality_traits:
                    personality_traits.remove(current_trait)
                did_add_trait = _add_trait_if_possible(current_trait)
                if did_add_trait:
                    if not personality_trait_slots:
                        return traits
        remaining_parent_traits = list(
            traits_a.symmetric_difference(traits_b) - set(traits))
        random.shuffle(remaining_parent_traits)
        while personality_trait_slots:
            while remaining_parent_traits:
                current_trait = remaining_parent_traits.pop()
                if current_trait in personality_traits:
                    personality_traits.remove(current_trait)
                did_add_trait = _add_trait_if_possible(current_trait)
                if did_add_trait:
                    if not personality_trait_slots:
                        return traits
        while personality_trait_slots:
            while personality_traits:
                current_trait = personality_traits.pop()
                _add_trait_if_possible(current_trait)
        return traits

    def create_offspring_data(self):
        r = random.Random()
        r.seed(self._seed)
        if self._offspring_count_override is not None:
            offspring_count = self._offspring_count_override
        else:
            offspring_count = pop_weighted([
                (p.weight *
                 p.modifiers.get_multiplier(SingleSimResolver(self._sim_info)),
                 p.size) for p in self.MULTIPLE_OFFSPRING_CHANCES
            ],
                                           random=r)
        offspring_count = min(self._sim_info.household.free_slot_count + 1,
                              offspring_count)
        species = self._sim_info.species
        age = self._sim_info.get_birth_age()
        aging_data = AgingTuning.get_aging_data(species)
        num_personality_traits = aging_data.get_personality_trait_count(age)
        self._offspring_data = []
        for offspring_index in range(offspring_count):
            if offspring_index and r.random(
            ) < self.MONOZYGOTIC_OFFSPRING_CHANCE:
                gender = self._offspring_data[offspring_index - 1].gender
                genetics = self._offspring_data[offspring_index - 1].genetics
            else:
                gender_chance_stat = self._sim_info.get_statistic(
                    self.GENDER_CHANCE_STAT)
                if gender_chance_stat is None:
                    gender_chance = 0.5
                else:
                    gender_chance = (gender_chance_stat.get_value() -
                                     gender_chance_stat.min_value) / (
                                         gender_chance_stat.max_value -
                                         gender_chance_stat.min_value)
                gender = Gender.FEMALE if r.random(
                ) < gender_chance else Gender.MALE
                genetics = r.randint(1, MAX_UINT32)
            last_name = SimSpawner.get_last_name(self._sim_info.last_name,
                                                 gender, species)
            offspring_data = PregnancyOffspringData(age,
                                                    gender,
                                                    species,
                                                    genetics,
                                                    last_name=last_name)
            (parent_a, parent_b) = self.get_parents()
            offspring_data.traits = self.select_traits_for_offspring(
                offspring_data,
                parent_a,
                parent_b,
                num_personality_traits,
                origin=self._origin)
            self._offspring_data.append(offspring_data)

    def get_offspring_data_gen(self):
        for offspring_data in self._offspring_data:
            yield offspring_data

    def _get_random_first_name(self, offspring_data):
        tries_left = 10

        def is_valid(first_name):
            nonlocal tries_left
            if not first_name:
                return False
            tries_left -= 1
            if tries_left and any(sim.first_name == first_name
                                  for sim in self._sim_info.household):
                return False
            elif any(sim.first_name == first_name
                     for sim in self._offspring_data):
                return False
            return True

        first_name = None
        while not is_valid(first_name):
            first_name = SimSpawner.get_random_first_name(
                offspring_data.gender, offspring_data.species)
        return first_name

    def assign_random_first_names_to_offspring_data(self):
        for offspring_data in self.get_offspring_data_gen():
            offspring_data.first_name = self._get_random_first_name(
                offspring_data)

    def _show_npc_dialog(self):
        for tuning_data in self.MULTIPLE_OFFSPRING_CHANCES:
            if tuning_data.size == self.offspring_count:
                npc_dialog = tuning_data.npc_dialog
                if npc_dialog is not None:
                    for parent in self.get_parents():
                        if parent is None:
                            logger.error(
                                'Pregnancy for {} has a None parent for IDs {}. Please file a DT with a save attached.',
                                self._sim_info, ','.join(
                                    str(parent_id)
                                    for parent_id in self._parent_ids))
                            return
                        parent_instance = parent.get_sim_instance()
                        if parent_instance is not None:
                            if parent_instance.client is not None:
                                additional_tokens = list(
                                    itertools.chain(self.get_parents(),
                                                    self._offspring_data))
                                dialog = npc_dialog(
                                    parent_instance,
                                    DoubleSimResolver(additional_tokens[0],
                                                      additional_tokens[1]))
                                dialog.show_dialog(
                                    additional_tokens=additional_tokens)
                return

    def save(self):
        data = SimObjectAttributes_pb2.PersistablePregnancyTracker()
        data.seed = self._seed
        data.origin = self._origin
        data.parent_ids.extend(self._parent_ids)
        return data

    def load(self, data):
        self._seed = int(data.seed)
        try:
            self._origin = PregnancyOrigin(data.origin)
        except KeyError:
            self._origin = PregnancyOrigin.DEFAULT
        self._parent_ids.clear()
        self._parent_ids.extend(data.parent_ids)

    def refresh_pregnancy_data(self, on_create=None):
        if not self.is_pregnant:
            self.clear_pregnancy()
            return
        traits_pack_tuple = self.PREGNANCY_ORIGIN_TRAIT_MAPPING.get(
            self._origin)
        if traits_pack_tuple is not None and not is_available_pack(
                traits_pack_tuple.pack):
            self._create_and_name_offspring(on_create=on_create)
            self.clear_pregnancy()
        self.enable_pregnancy()

    def on_lod_update(self, old_lod, new_lod):
        if new_lod == SimInfoLODLevel.MINIMUM:
            self.clear_pregnancy()
Beispiel #20
0
class WhimsTracker(SimInfoTracker):
    MAX_GOALS = 2
    EMOTIONAL_WHIM_PRIORITY = 1

    class WhimAwardTypes(enum.Int):
        MONEY = 0
        BUFF = 1
        OBJECT = 2
        TRAIT = 3
        CASPART = 4

    SATISFACTION_STORE_ITEMS = TunableMapping(
        description=
        '\n        A list of Sim based Tunable Rewards offered from the Satisfaction Store.\n        ',
        key_type=TunableReference(
            description='\n            The reward to offer.\n            ',
            manager=services.get_instance_manager(
                sims4.resources.Types.REWARD),
            pack_safe=True),
        value_type=TunableTuple(
            description=
            '\n            A collection of data about this reward.\n            ',
            cost=Tunable(tunable_type=int, default=100),
            award_type=TunableEnumEntry(WhimAwardTypes, WhimAwardTypes.MONEY)))
    WHIM_THRASHING_CHANCE = TunablePercent(
        description=
        '\n        The tunable percent chance that the activation of a whimset will try\n        and cancel a whim of a lower whimset priority as long as that whim is\n        not locked, and not on the anti thrashing cooldown.\n        ',
        default=50)
    WHIM_ANTI_THRASHING_TIME = TunableSimMinute(
        description=
        '\n        The amount of time in sim minutes that a whim will not be overwritten\n        by another whimset becoming active.  This is essentially a period of\n        time after a whim becomes active that it is considered locked.\n        ',
        default=5)

    @classproperty
    def max_whims(cls):
        return WhimsTracker.MAX_GOALS + 1

    @classproperty
    def emotional_whim_index(cls):
        return WhimsTracker.MAX_GOALS

    def __init__(self, sim_info):
        self._sim_info = sim_info
        self._goal_id_generator = uid.UniqueIdGenerator(1)
        self._active_whimsets_data = {}
        self._active_whims = [_ActiveWhimData() for _ in range(self.max_whims)]
        self._hidden = False
        self._cooldown_alarms = {}
        self._whim_goal_proto = None
        self._completed_goals = {}
        self._test_results_map = {}
        self._goals_dirty = True
        self._score_multipliers = []

    def start_whims_tracker(self):
        self._offer_whims()

    def activate_whimset_from_objective_completion(self, whimset):
        self._activate_whimset(whimset)
        self._try_and_thrash_whims(whimset.activated_priority)

    def validate_goals(self):
        sim = self._sim_info.get_sim_instance()
        if sim is None:
            return
        for whim_data in self._active_whims:
            whim = whim_data.whim
            if whim is None:
                continue
            required_sim_info = whim.get_required_target_sim_info()
            if not whim.can_be_given_as_goal(
                    sim, None, inherited_target_sim_info=required_sim_info):
                self._remove_whim(whim,
                                  TelemetryWhimEvents.NO_LONGER_AVAILABLE)
        self._offer_whims()

    def whims_and_parents_gen(self):
        for whim_data in self._active_whims:
            if whim_data.whim is None:
                continue
            yield (whim_data.whim, whim_data.whimset)

    def get_active_whimsets(self):
        whim_sets = set(self._active_whimsets_data.keys())
        if self._sim_info.primary_aspiration is not None and self._sim_info.primary_aspiration.whim_set is not None:
            whim_sets.add(self._sim_info.primary_aspiration.whim_set)
        current_venue = services.get_current_venue()
        if current_venue.whim_set is not None:
            whim_sets.add(current_venue.whim_set)
        for trait in self._sim_info.trait_tracker:
            if trait.whim_set is not None:
                whim_sets.add(trait.whim_set)
        season_service = services.season_service()
        if season_service is not None:
            season_content = season_service.season_content
            if season_content.whim_set is not None:
                whim_sets.add(season_content.whim_set)
        object_manager = services.object_manager()
        whim_sets.update(object_manager.active_whim_sets)
        zone_director = services.venue_service().get_zone_director()
        open_street_director = zone_director.open_street_director
        if open_street_director is not None and open_street_director.whim_set:
            whim_sets.add(open_street_director.whim_set)
        return whim_sets

    def get_active_whim_data(self):
        return tuple(self._active_whims)

    def get_whimset_target(self, whimset):
        whimset_data = self._active_whimsets_data.get(whimset)
        if whimset_data is None:
            return
        return whimset_data.target

    def get_emotional_whimset(self):
        return self._sim_mood().whim_set

    def refresh_emotion_whim(self):
        emotional_whim = self._active_whims[self.emotional_whim_index].whim
        if emotional_whim is not None:
            self._remove_whim(emotional_whim,
                              TelemetryWhimEvents.NO_LONGER_AVAILABLE)
        self._offer_whims()

    def get_priority(self, whimset):
        return whimset.get_priority(self._sim_info)

    def clean_up(self):
        for whim_data in self._active_whims:
            whim = whim_data.whim
            if whim is not None:
                whim.destroy()
            if whim_data.anti_thrashing_alarm_handle is not None:
                alarms.cancel_alarm(whim_data.anti_thrashing_alarm_handle)
        self._active_whims = [_ActiveWhimData() for _ in range(self.max_whims)]
        for alarm_handle in self._cooldown_alarms.values():
            alarms.cancel_alarm(alarm_handle)
        self._cooldown_alarms.clear()
        self._test_results_map.clear()
        self._active_whimsets_data.clear()

    def refresh_whim(self, whim_type):
        whim = self._get_whim_by_whim_type(whim_type)
        if whim is None:
            logger.error(
                'Trying to refresh whim type {} when there are no active whims of that type.',
                whim_type)
            return
        self._remove_whim(whim, TelemetryWhimEvents.CANCELED)
        self._offer_whims(prohibited_whims={whim_type})

    def toggle_whim_lock(self, whim_type):
        whim = self._get_whim_by_whim_type(whim_type)
        if whim is None:
            logger.error(
                'Trying to toggle the locked status of whim type {} when there are no active whims of that type.',
                whim_type)
            return
        whim.toggle_locked_status()
        self._goals_dirty = True
        self._send_goals_update()

    def hide_whims(self):
        if self._hidden:
            logger.error('Trying to hide whims when they are already hidden.')
            return
        self._hidden = True
        self._goals_dirty = True
        self._send_goals_update()

    def show_whims(self, reset=False):
        if not self._hidden:
            logger.error("Trying to show whims when they aren't hidden.")
            return
        self._hidden = False
        self._goals_dirty = True
        if reset:
            self.refresh_whims()
        self._send_goals_update()

    def refresh_whims(self):
        prohibited_whims = set()
        for whim_data in self._active_whims:
            whim = whim_data.whim
            if whim is not None:
                if whim.locked:
                    continue
                prohibited_whims.add(type(whim))
                self._remove_whim(whim, TelemetryWhimEvents.CANCELED)
        self._offer_whims(prohibited_whims=prohibited_whims)

    def add_score_multiplier(self, multiplier):
        self._score_multipliers.append(multiplier)
        self._goals_dirty = True
        self._send_goals_update()

    def get_score_multiplier(self):
        return reduce(operator.mul, self._score_multipliers, 1)

    def get_score_for_whim(self, score):
        return int(score * self.get_score_multiplier())

    def remove_score_multiplier(self, multiplier):
        if multiplier in self._score_multipliers:
            self._score_multipliers.remove(multiplier)
        self._goals_dirty = True
        self._send_goals_update()

    def on_zone_unload(self):
        if not game_services.service_manager.is_traveling:
            return
        self._whim_goal_proto = GameplaySaveData_pb2.WhimsetTrackerData()
        self.save_whims_info_to_proto(self._whim_goal_proto,
                                      copy_existing=False)
        self.clean_up()

    def purchase_whim_award(self, reward_guid64):
        reward_instance = services.get_instance_manager(
            sims4.resources.Types.REWARD).get(reward_guid64)
        award = reward_instance
        cost = self.SATISFACTION_STORE_ITEMS[reward_instance].cost
        if self._sim_info.get_whim_bucks() < cost:
            logger.debug(
                'Attempting to purchase a whim award with insufficient funds: Cost: {}, Funds: {}',
                cost, self._sim_info.get_whim_bucks())
            return
        self._sim_info.add_whim_bucks(-cost,
                                      SetWhimBucks.PURCHASED_REWARD,
                                      source=reward_guid64)
        award.give_reward(self._sim_info)

    def send_satisfaction_reward_list(self):
        msg = Sims_pb2.SatisfactionRewards()
        for (reward, data) in self.SATISFACTION_STORE_ITEMS.items():
            reward_msg = Sims_pb2.SatisfactionReward()
            reward_msg.reward_id = reward.guid64
            reward_msg.cost = data.cost
            reward_msg.affordable = True if data.cost <= self._sim_info.get_whim_bucks(
            ) else False
            reward_msg.available = reward.is_valid(self._sim_info)
            reward_msg.type = data.award_type
            unavailable_tooltip = reward.get_unavailable_tooltip(
                self._sim_info)
            if unavailable_tooltip is not None:
                reward_msg.unavailable_tooltip = unavailable_tooltip
            msg.rewards.append(reward_msg)
        msg.sim_id = self._sim_info.id
        distributor = Distributor.instance()
        distributor.add_op_with_no_owner(
            GenericProtocolBufferOp(Operation.SIM_SATISFACTION_REWARDS, msg))

    def cache_whim_goal_proto(self, whim_tracker_proto, skip_load=False):
        if skip_load:
            return
        if self._sim_info.is_npc:
            return
        if self._sim_info.whim_tracker is None:
            return
        self._whim_goal_proto = GameplaySaveData_pb2.WhimsetTrackerData()
        self._whim_goal_proto.CopyFrom(whim_tracker_proto)

    def load_whims_info_from_proto(self):
        if self._sim_info.is_npc:
            return
        if self._whim_goal_proto is None:
            return
        for whim_data in self._active_whims:
            whim = whim_data.whim
            if whim is not None:
                self._remove_whim(whim, None)
        if len(self._whim_goal_proto.active_whims) > self.max_whims:
            logger.error(
                'More whims saved than the max number of goals allowed')
        aspiration_manager = services.get_instance_manager(
            sims4.resources.Types.ASPIRATION)
        sim_info_manager = services.sim_info_manager()
        for active_whim_msg in self._whim_goal_proto.active_whims:
            if not active_whim_msg.HasField('index'):
                continue
            whimset = aspiration_manager.get(active_whim_msg.whimset_guid)
            if whimset is None:
                logger.info(
                    'Trying to load unavailable ASPIRATION resource: {}',
                    active_whim_msg.whimset_guid)
            else:
                goal_seed = GoalSeedling.deserialize_from_proto(
                    active_whim_msg.goal_data)
                if goal_seed is None:
                    continue
                target_sim_info = None
                if goal_seed.target_id:
                    target_sim_info = sim_info_manager.get(goal_seed.target_id)
                    if target_sim_info is None:
                        continue
                else:
                    secondary_sim_info = None
                    if goal_seed.secondary_target_id:
                        secondary_sim_info = sim_info_manager.get(
                            goal_seed.secondary_target_id)
                        if secondary_sim_info is None:
                            continue
                    else:
                        whim_index = active_whim_msg.index
                        goal = goal_seed.goal_type(
                            sim_info=self._sim_info,
                            goal_id=self._goal_id_generator(),
                            inherited_target_sim_info=target_sim_info,
                            secondary_sim_info=secondary_sim_info,
                            count=goal_seed.count,
                            reader=goal_seed.reader,
                            locked=goal_seed.locked)
                        goal.setup()
                        goal.register_for_on_goal_completed_callback(
                            self._on_goal_completed)
                        whim_data = self._active_whims[whim_index]
                        whim_data.whim = goal
                        whim_data.whimset = whimset
                        self._create_anti_thrashing_cooldown(whim_data)
                        self._goals_dirty = True
                        logger.info('Whim {} loaded.', goal_seed.goal_type)
        self._whim_goal_proto = None
        self._send_goals_update()

    def save_whims_info_to_proto(self, whim_tracker_proto, copy_existing=True):
        if self._sim_info.is_npc:
            return
        if copy_existing and self._whim_goal_proto is not None:
            whim_tracker_proto.CopyFrom(self._whim_goal_proto)
            return
        for (index, active_whim_data) in enumerate(self._active_whims):
            active_whim = active_whim_data.whim
            if active_whim is None:
                continue
            with ProtocolBufferRollback(
                    whim_tracker_proto.active_whims) as active_whim_msg:
                active_whim_msg.whimset_guid = active_whim_data.whimset.guid64
                active_whim_msg.index = index
                goal_seed = active_whim.create_seedling()
                goal_seed.finalize_creation_for_save()
                goal_seed.serialize_to_proto(active_whim_msg.goal_data)

    def debug_activate_whimset(self, whimset, chained):
        if not whimset.update_on_load:
            return
        self._activate_whimset(whimset)
        self._try_and_thrash_whims(whimset.activated_priority)

    def debug_activate_whim(self, whim):
        whim_data = self._active_whims[0]
        if whim_data.whim is not None:
            self._remove_whim(whim_data.whim, TelemetryWhimEvents.CANCELED)
        goal = whim(sim_info=self._sim_info, goal_id=self._goal_id_generator())
        goal.setup()
        goal.register_for_on_goal_completed_callback(self._on_goal_completed)
        goal.show_goal_awarded_notification()
        whim_data.whim = goal
        whim_data.whimset = next(iter(self._active_whimsets_data.keys()))
        self._create_anti_thrashing_cooldown(whim_data)
        self._goals_dirty = True
        self._send_goals_update()

    def debug_offer_whim_from_whimset(self, whimset):
        if whimset.update_on_load:
            self._activate_whimset(whimset)
        whim_data = self._active_whims[0]
        if whim_data.whim is not None:
            self._remove_whim(whim_data.whim, TelemetryWhimEvents.CANCELED)
        goal = self._create_whim(whimset, set())
        goal.setup()
        goal.register_for_on_goal_completed_callback(self._on_goal_completed)
        goal.show_goal_awarded_notification()
        whim_data.whim = goal
        whim_data.whimset = whimset
        self._create_anti_thrashing_cooldown(whim_data)
        self._goals_dirty = True
        self._send_goals_update()

    @property
    def _whims_needed(self):
        return self.max_whims - sum(1 for whim_info in self._active_whims
                                    if whim_info.whim is not None)

    @property
    def _sim_mood(self):
        return self._sim_info.get_mood()

    def _get_currently_active_whim_types(self):
        return {
            type(whim_data.whim)
            for whim_data in self._active_whims if whim_data.whim is not None
        }

    def _get_currently_used_whimsets(self):
        return {
            whim_data.whimset
            for whim_data in self._active_whims
            if whim_data.whimset is not None
        }

    def _get_whimsets_on_cooldown(self):
        return set(self._cooldown_alarms.keys())

    def _get_whim_data(self, whim):
        for whim_data in self._active_whims:
            if whim is whim_data.whim:
                return whim_data

    def _get_whim_by_whim_type(self, whim_type):
        for whim_data in self._active_whims:
            if isinstance(whim_data.whim, whim_type):
                return whim_data.whim

    def _get_target_for_whimset(self, whimset):
        if whimset.force_target is None:
            whimset_data = self._active_whimsets_data.get(whimset)
            if whimset_data is not None:
                return whimset_data.target
            return
        else:
            return whimset.force_target(self._sim_info)

    def _deactivate_whimset(self, whimset):
        if whimset not in self._active_whimsets_data:
            return
        logger.info('Deactivating Whimset {}', whimset)
        if whimset.cooldown_timer > 0:

            def _cooldown_ended(_):
                if whimset in self._cooldown_alarms:
                    del self._cooldown_alarms[whimset]

            self._cooldown_alarms[whimset] = alarms.add_alarm(
                self, create_time_span(minutes=whimset.cooldown_timer),
                _cooldown_ended)
        if whimset.timeout_retest is not None:
            resolver = event_testing.resolver.SingleSimResolver(self._sim_info)
            if resolver(whimset.timeout_retest.objective_test):
                self._activate_whimset(whimset)
                return
        del self._active_whimsets_data[whimset]
        if self._sim_info.aspiration_tracker is not None:
            self._sim_info.aspiration_tracker.reset_milestone(whimset)
        self._sim_info.remove_statistic(whimset.priority_commodity)

    def _activate_whimset(self, whimset, target=None, chained=False):
        if chained:
            new_priority = whimset.chained_priority
        else:
            new_priority = whimset.activated_priority
        if new_priority == 0:
            return
        self._sim_info.set_stat_value(whimset.priority_commodity,
                                      new_priority,
                                      add=True)
        whimset_data = self._active_whimsets_data.get(whimset)
        if whimset_data is None:
            stat = self._sim_info.get_stat_instance(whimset.priority_commodity)
            threshold = Threshold(whimset.priority_commodity.convergence_value,
                                  operator.le)

            def remove_active_whimset(_):
                self._deactivate_whimset(whimset)

            callback_data = stat.create_and_add_callback_listener(
                threshold, remove_active_whimset)
            self._active_whimsets_data[whimset] = _ActiveWhimsetData(
                target, callback_data)
            stat.decay_enabled = True
            logger.info('Setting whimset {} to active at priority {}.',
                        whimset, new_priority)
        else:
            logger.info(
                'Setting whimset {} which is already active to new priority value {}.',
                whimset, new_priority)

    def _remove_whim(self, whim, telemetry_event):
        whim.decommision()
        whim_data = self._get_whim_data(whim)
        whim_data.whim = None
        whim_data.whimset = None
        if whim_data.anti_thrashing_alarm_handle is not None:
            alarms.cancel_alarm(whim_data.anti_thrashing_alarm_handle)
            whim_data.anti_thrashing_alarm_handle = None
        if telemetry_event is not None:
            with telemetry_helper.begin_hook(writer,
                                             TELEMETRY_HOOK_WHIM_EVENT,
                                             sim_info=self._sim_info) as hook:
                hook.write_int(TELEMETRY_WHIM_EVENT_TYPE, telemetry_event)
                hook.write_guid(TELEMETRY_WHIM_GUID, whim.guid64)
        logger.info('Whim {} removed from whims tracker.', whim)
        self._goals_dirty = True

    def _on_goal_completed(self, whim, whim_completed):
        if not whim_completed:
            self._goals_dirty = True
            self._send_goals_update()
            return
        whim_data = self._get_whim_data(whim)
        parent_whimset = whim_data.whimset
        whim_type = type(whim)
        self._completed_goals[whim_type] = (whim, parent_whimset)
        inherited_target_sim_info = whim.get_actual_target_sim_info()
        self._remove_whim(whim, TelemetryWhimEvents.COMPLETED)
        services.get_event_manager().process_event(
            test_events.TestEvent.WhimCompleted,
            sim_info=self._sim_info,
            whim_completed=whim)
        should_deactivate_parent_whimset = parent_whimset.deactivate_on_completion
        highest_chained_priority = 0
        for set_to_chain in parent_whimset.connected_whim_sets:
            if set_to_chain is parent_whimset:
                should_deactivate_parent_whimset = False
            if set_to_chain.chained_priority > highest_chained_priority:
                highest_chained_priority = set_to_chain.chained_priority
            self._activate_whimset(set_to_chain,
                                   target=inherited_target_sim_info,
                                   chained=True)
        connected_whimsets = parent_whimset.connected_whims.get(whim)
        if connected_whimsets is not None:
            for set_to_chain in connected_whimsets:
                if set_to_chain is parent_whimset:
                    should_deactivate_parent_whimset = False
                if set_to_chain.chained_priority > highest_chained_priority:
                    highest_chained_priority = set_to_chain.chained_priority
                self._activate_whimset(set_to_chain,
                                       target=inherited_target_sim_info,
                                       chained=True)
        if should_deactivate_parent_whimset:
            self._deactivate_whimset(parent_whimset)
        op = distributor.ops.SetWhimComplete(whim_type.guid64)
        Distributor.instance().add_op(self._sim_info, op)
        score = self.get_score_for_whim(whim.score)
        if score > 0:
            self._sim_info.add_whim_bucks(score,
                                          SetWhimBucks.WHIM,
                                          source=whim.guid64)
        logger.info('Goal completed: {}, from Whim Set: {}', whim,
                    parent_whimset)
        thrashed = False
        if highest_chained_priority > 0:
            thrashed = self._try_and_thrash_whims(
                highest_chained_priority, extra_prohibited_whims={whim_type})
        if not thrashed:
            self._offer_whims(prohibited_whims={whim_type})

    def _create_whim(self, whimset, prohibited_whims):
        potential_target = self._get_target_for_whimset(whimset)
        if potential_target is None and whimset.force_target is not None:
            return
        if whimset.secondary_target is not None:
            secondary_target = whimset.secondary_target(self._sim_info)
            if secondary_target is None:
                return
        else:
            secondary_target = None
        sim = self._sim_info.get_sim_instance(
            allow_hidden_flags=ALL_HIDDEN_REASONS_EXCEPT_UNINITIALIZED)
        disallowed_whims = self._get_currently_active_whim_types(
        ) | prohibited_whims
        weighted_whims = [(possible_whim.weight, possible_whim.goal)
                          for possible_whim in whimset.whims
                          if possible_whim.goal not in disallowed_whims]
        while weighted_whims:
            selected_whim = sims4.random.pop_weighted(weighted_whims)
            old_whim_instance_and_whimset = self._completed_goals.get(
                selected_whim)
            if old_whim_instance_and_whimset is not None and old_whim_instance_and_whimset[
                    0].is_on_cooldown():
                continue
            pretest = selected_whim.can_be_given_as_goal(
                sim, None, inherited_target_sim_info=potential_target)
            if pretest:
                whim = selected_whim(
                    sim_info=self._sim_info,
                    goal_id=self._goal_id_generator(),
                    inherited_target_sim_info=potential_target,
                    secondary_sim_info=secondary_target)
                return whim

    def _create_anti_thrashing_cooldown(self, whim_data):
        def end_cooldown(_):
            whim_data.anti_thrashing_alarm_handle = None

        whim_data.anti_thrashing_alarm_handle = alarms.add_alarm(
            self,
            create_time_span(minutes=WhimsTracker.WHIM_ANTI_THRASHING_TIME),
            end_cooldown)

    def _offer_whims(self,
                     prohibited_whimsets=EMPTY_SET,
                     prohibited_whims=EMPTY_SET):
        if self._whims_needed == 0:
            return
        if self._sim_info.is_npc:
            return
        if not self._sim_info.is_instanced(
                allow_hidden_flags=ALL_HIDDEN_REASONS_EXCEPT_UNINITIALIZED):
            return
        if services.current_zone().is_zone_shutting_down:
            return
        whimsets_on_cooldown = self._get_whimsets_on_cooldown()
        for (index, whim_data) in enumerate(self._active_whims):
            if whim_data.whim is not None:
                continue
            if index == self.emotional_whim_index:
                emotional_whimset = self.get_emotional_whimset()
                if emotional_whimset is None:
                    logger.info('No emotional whimset found for mood {}.',
                                self._sim_mood)
                else:
                    possible_whimsets = {emotional_whimset}
                    possible_whimsets -= self._get_currently_used_whimsets()
                    possible_whimsets -= prohibited_whimsets
                    possible_whimsets -= whimsets_on_cooldown
                    prioritized_whimsets = [(self.get_priority(whimset),
                                             whimset)
                                            for whimset in possible_whimsets]
                    while prioritized_whimsets:
                        whimset = sims4.random.pop_weighted(
                            prioritized_whimsets)
                        if whimset is None:
                            break
                        goal = self._create_whim(whimset, prohibited_whims)
                        if goal is None:
                            continue
                        goal.setup()
                        goal.register_for_on_goal_completed_callback(
                            self._on_goal_completed)
                        goal.show_goal_awarded_notification()
                        whim_data.whim = goal
                        whim_data.whimset = whimset
                        self._create_anti_thrashing_cooldown(whim_data)
                        with telemetry_helper.begin_hook(
                                writer,
                                TELEMETRY_HOOK_WHIM_EVENT,
                                sim_info=self._sim_info) as hook:
                            hook.write_int(TELEMETRY_WHIM_EVENT_TYPE,
                                           TelemetryWhimEvents.ADDED)
                            hook.write_guid(TELEMETRY_WHIM_GUID, goal.guid64)
                        self._goals_dirty = True
                        break
            else:
                possible_whimsets = self.get_active_whimsets()
            possible_whimsets -= self._get_currently_used_whimsets()
            possible_whimsets -= prohibited_whimsets
            possible_whimsets -= whimsets_on_cooldown
            prioritized_whimsets = [(self.get_priority(whimset), whimset)
                                    for whimset in possible_whimsets]
            while prioritized_whimsets:
                whimset = sims4.random.pop_weighted(prioritized_whimsets)
                if whimset is None:
                    break
                goal = self._create_whim(whimset, prohibited_whims)
                if goal is None:
                    continue
                goal.setup()
                goal.register_for_on_goal_completed_callback(
                    self._on_goal_completed)
                goal.show_goal_awarded_notification()
                whim_data.whim = goal
                whim_data.whimset = whimset
                self._create_anti_thrashing_cooldown(whim_data)
                with telemetry_helper.begin_hook(
                        writer, TELEMETRY_HOOK_WHIM_EVENT,
                        sim_info=self._sim_info) as hook:
                    hook.write_int(TELEMETRY_WHIM_EVENT_TYPE,
                                   TelemetryWhimEvents.ADDED)
                    hook.write_guid(TELEMETRY_WHIM_GUID, goal.guid64)
                self._goals_dirty = True
                break
        self._send_goals_update()

    def _try_and_thrash_whims(self,
                              priority,
                              extra_prohibited_whims=EMPTY_SET):
        whims_thrashed = set()
        for (index, whim_data) in enumerate(self._active_whims):
            if index == self.emotional_whim_index:
                continue
            if whim_data.whim is None:
                continue
            if not whim_data.anti_thrashing_alarm_handle is not None:
                if whim_data.whim.locked:
                    continue
                if self.get_priority(whim_data.whimset) >= priority:
                    continue
                if not sims4.random.random_chance(
                        WhimsTracker.WHIM_THRASHING_CHANCE * 100):
                    continue
                whims_thrashed.add(type(whim_data.whim))
                self._remove_whim(whim_data.whim, TelemetryWhimEvents.CANCELED)
        if not whims_thrashed:
            return False
        prohibited_whims = whims_thrashed | extra_prohibited_whims
        self._offer_whims(prohibited_whims=prohibited_whims)
        return True

    def _send_goals_update(self):
        if not self._goals_dirty:
            return
        logger.debug('Sending whims update for {}.  Current active whims: {}',
                     self._sim_info,
                     self._active_whims,
                     owner='jjacobson')
        current_whims = []
        for (index, whim_data) in enumerate(self._active_whims):
            whim = whim_data.whim
            if whim is None or self._hidden:
                whim_goal = DistributorOps_pb2.WhimGoal()
                current_whims.append(whim_goal)
            else:
                goal_target_id = 0
                goal_whimset = whim_data.whimset
                goal_target = whim.get_required_target_sim_info()
                goal_target_id = goal_target.id if goal_target is not None else 0
                whim_goal = DistributorOps_pb2.WhimGoal()
                whim_goal.whim_guid64 = whim.guid64
                whim_name = whim.get_display_name()
                if whim_name is not None:
                    whim_goal.whim_name = whim_name
                whim_goal.whim_score = self.get_score_for_whim(whim.score)
                whim_goal.whim_noncancel = whim.noncancelable
                whim_display_icon = whim.display_icon
                if whim_display_icon is not None:
                    whim_goal.whim_icon_key.type = whim_display_icon.type
                    whim_goal.whim_icon_key.group = whim_display_icon.group
                    whim_goal.whim_icon_key.instance = whim_display_icon.instance
                whim_goal.whim_goal_count = whim.max_iterations
                whim_goal.whim_current_count = whim.completed_iterations
                whim_goal.whim_target_sim = goal_target_id
                whim_tooltip = whim.get_display_tooltip()
                if whim_tooltip is not None:
                    whim_goal.whim_tooltip = whim_tooltip
                if index == self.emotional_whim_index:
                    whim_goal.whim_mood_guid64 = self._sim_mood().guid64
                else:
                    whim_goal.whim_mood_guid64 = 0
                whim_goal.whim_tooltip_reason = goal_whimset.whim_reason(
                    *whim.get_localization_tokens())
                whim_goal.whim_locked = whim.locked
                current_whims.append(whim_goal)
        if self._goals_dirty:
            self._sim_info.current_whims = current_whims
            self._goals_dirty = False

    @classproperty
    def _tracker_lod_threshold(cls):
        return SimInfoLODLevel.FULL

    def on_lod_update(self, old_lod, new_lod):
        if new_lod < self._tracker_lod_threshold:
            self.clean_up()
        elif old_lod < self._tracker_lod_threshold:
            sim_msg = services.get_persistence_service().get_sim_proto_buff(
                self._sim_info.id)
            if sim_msg is not None:
                self._sim_info.set_whim_bucks(sim_msg.gameplay_data.whim_bucks,
                                              SetWhimBucks.LOAD)
                self.cache_whim_goal_proto(sim_msg.gameplay_data.whim_tracker)
class AffordanceReferenceScoringModifier(BaseGameEffectModifier):
    FACTORY_TUNABLES = {
        'content_score_bonus':
        Tunable(
            description=
            '\n            When determine content score for affordances and afforance matches\n            tuned here, content score is increased by this amount.\n            ',
            tunable_type=int,
            default=0),
        'success_modifier':
        TunablePercent(
            description=
            '\n            Amount to adjust percent success chance. For example, tuning 10%\n            will increase success chance by 10% over the base success chance.\n            Additive with other buffs.\n            ',
            default=0,
            minimum=-100),
        'affordances':
        TunableList(
            description=
            '\n            A list of affordances that will be compared against.\n            ',
            tunable=TunableReference(manager=services.affordance_manager())),
        'affordance_lists':
        TunableList(
            description=
            '\n            A list of affordance snippets that will be compared against.\n            ',
            tunable=snippets.TunableAffordanceListReference()),
        'interaction_category_tags':
        TunableSet(
            description=
            '\n            This attribute is used to test for affordances that contain any of the tags in this set.\n            ',
            tunable=TunableEnumEntry(
                description=
                '\n                These tag values are used for testing interactions.\n                ',
                tunable_type=Tag,
                default=Tag.INVALID)),
        'interaction_category_blacklist_tags':
        TunableSet(
            description=
            '\n            Any interaction with a tag in this set will NOT be modiified.\n            Affects display name on a per interaction basis.\n            ',
            tunable=TunableEnumEntry(
                description=
                '\n                These tag values are used for testing interactions.\n                ',
                tunable_type=Tag,
                default=Tag.INVALID)),
        'pie_menu_parent_name':
        OptionalTunable(
            description=
            '\n            If enabled, we will insert the name into this parent string\n            in the pie menu.  Only affected by test and blacklist tags\n            (for performance reasons)\n            ',
            tunable=TunableLocalizedStringFactory(
                description=
                '\n                A string to wrap the normal interaction name.  Token 0 is actor,\n                Token 1 is the normal name.\n                '
            )),
        'new_pie_menu_icon':
        TunableIconAllPacks(
            description=
            "\n            Icon to put on interactions that pass test (interaction resolver)\n            and don't match blacklist tags.\n            ",
            allow_none=True),
        'basic_extras':
        TunableBasicExtras(
            description=
            '\n            Basic extras to add to interactions that match. \n            '
        ),
        'test':
        event_testing.tests.TunableTestSet(
            description=
            '\n            The test to run to see if the display_name should be\n            overridden. Ors of Ands.\n            '
        )
    }

    def __init__(self,
                 content_score_bonus=0,
                 success_modifier=0,
                 affordances=(),
                 affordance_lists=(),
                 interaction_category_tags=set(),
                 interaction_category_blacklist_tags=set(),
                 pie_menu_parent_name=None,
                 new_pie_menu_icon=None,
                 basic_extras=(),
                 test=None):
        super().__init__(GameEffectType.AFFORDANCE_MODIFIER)
        self._score_bonus = content_score_bonus
        self._success_modifier = success_modifier
        self._affordances = affordances
        self._affordance_lists = affordance_lists
        self._interaction_category_tags = interaction_category_tags
        self._interaction_category_blacklist_tags = interaction_category_blacklist_tags
        self._pie_menu_parent_name = pie_menu_parent_name
        self._new_pie_menu_icon = new_pie_menu_icon
        self._basic_extras = basic_extras
        self._test = test

    def is_type(self, affordance, resolver):
        if affordance is not None:
            if affordance.interaction_category_tags & self._interaction_category_blacklist_tags:
                return False
            if affordance in self._affordances:
                return True
            for affordances in self._affordance_lists:
                if affordance in affordances:
                    return True
            if affordance.interaction_category_tags & self._interaction_category_tags:
                return True
            elif self._test:
                result = False
                try:
                    result = self._test.run_tests(resolver)
                except:
                    pass
                if result:
                    return True
        if self._test:
            result = False
            try:
                result = self._test.run_tests(resolver)
            except:
                pass
            if result:
                return True
        return False

    def get_score_for_type(self, affordance, resolver):
        if self.is_type(affordance, resolver):
            return self._score_bonus
        return 0

    def get_success_for_type(self, affordance, resolver):
        if self.is_type(affordance, resolver):
            return self._success_modifier
        return 0

    def get_new_pie_menu_icon_and_parent_name_for_type(self, affordance,
                                                       resolver):
        if self.is_type(affordance, resolver):
            return (self._new_pie_menu_icon, self._pie_menu_parent_name,
                    self._interaction_category_blacklist_tags)
        return (None, None, None)

    def get_basic_extras_for_type(self, affordance, resolver):
        if self.is_type(affordance, resolver):
            return self._basic_extras
        return []

    def debug_affordances_gen(self):
        for affordance in self._affordances:
            yield affordance.__name__
        for affordnace_snippet in self._affordance_lists:
            yield affordnace_snippet.__name__
class RabbitholeGig(Gig):
    INSTANCE_TUNABLES = {
        'negative_mood_tuning':
        TunableTuple(
            description=
            '\n            Tuning for the negative mood test.  If the Sim has the any of the \n            negative mood buffs (the Buff test passes), the failure chance \n            tunable will be used to determine whether or not to apply the \n            FAILURE outcome.\n            ',
            negative_mood_test=sims.sim_info_tests.BuffTest.TunableFactory(),
            failure_chance=TunablePercent(
                description=
                '\n                Chance of a FAILURE outcome if the negative mood test passes.\n                ',
                default=0.0)),
        'recommended_skill_tuning':
        OptionalTunable(
            description=
            "\n            Tuning for the (optional) recommended skill.  If the Sim has this\n            skill, the outcome will depend on the Sim's skill level relative \n            to the recommended skill level.\n            ",
            tunable=TunableTuple(
                recommended_skill_test=statistics.skill_tests.SkillRangeTest.
                TunableFactory(
                    description=
                    '\n                    The recommended skill test for this gig.  For Home \n                    Assignment gigs, the skill range min and max should be the \n                    same.\n                    '
                ),
                great_success_chance_multiplier=Tunable(
                    description=
                    '\n                    The multiplier for determining the chance the Sim will\n                    receive the GREAT_SUCCESS outcome.\n                    ',
                    tunable_type=float,
                    default=0.0),
                failure_chance_multiplier=Tunable(
                    description=
                    '\n                    The multiplier for determining the chance the Sim will\n                    receive the FAILURE outcome.\n                    ',
                    tunable_type=float,
                    default=0.0),
                critical_failure_skill_level_delta=Tunable(
                    description=
                    '\n                    The difference in skill levels lower than the recommended\n                    skill level for a Sim to qualify for a CRITICAL FAILURE \n                    outcome.\n                    ',
                    tunable_type=int,
                    default=0))),
        'gig_picker_localization_format':
        TunableLocalizedStringFactory(
            description=
            '\n            String used to format the description in the gig picker. Currently\n            has tokens for name, payout, gig time, tip title, and tip text.\n            '
        )
    }

    @classmethod
    def _verify_tuning_callback(cls):
        if not cls.tip:
            logger.error(
                'No tip tuned for Rabbithole Gig {}. Rabbithole Gigs must have a tip.',
                cls)

    def _determine_gig_outcome(self):
        if not self.has_attended_gig():
            self._gig_result = GigResult.CRITICAL_FAILURE
            self._send_gig_telemetry(TELEMETRY_GIG_PROGRESS_TIMEOUT)
            return
        if self._gig_result == GigResult.CANCELED:
            self._gig_result = GigResult.FAILURE
            return
        self._send_gig_telemetry(TELEMETRY_GIG_PROGRESS_COMPLETE)
        resolver = self.get_resolver_for_gig()
        if resolver(
                self.negative_mood_tuning.negative_mood_test
        ) and random.random() <= self.negative_mood_tuning.failure_chance:
            self._gig_result = GigResult.FAILURE
            return
        if self.recommended_skill_tuning:
            skill = self._owner.get_statistic(
                self.recommended_skill_tuning.recommended_skill_test.skill,
                add=False)
            sim_skill_level = 0
            if skill:
                sim_skill_level = skill.get_user_value()
            recommended_level = self.recommended_skill_tuning.recommended_skill_test.skill_range_max
            if sim_skill_level > recommended_level:
                chance = (
                    sim_skill_level - recommended_level
                ) * self.recommended_skill_tuning.great_success_chance_multiplier
                if random.random() <= chance:
                    self._gig_result = GigResult.GREAT_SUCCESS
                else:
                    self._gig_result = GigResult.SUCCESS
            elif sim_skill_level == recommended_level:
                self._gig_result = GigResult.SUCCESS
            else:
                skill_level_difference = recommended_level - sim_skill_level
                if skill_level_difference >= self.recommended_skill_tuning.critical_failure_skill_level_delta:
                    self._gig_result = GigResult.CRITICAL_FAILURE
                else:
                    chance = skill_level_difference * self.recommended_skill_tuning.failure_chance_multiplier
                    if random.random() <= chance:
                        self._gig_result = GigResult.FAILURE
                    else:
                        self._gig_result = GigResult.CRITICAL_FAILURE
        else:
            self._gig_result = GigResult.SUCCESS

    @classmethod
    def create_picker_row(cls,
                          description=None,
                          scheduled_time=None,
                          owner=None,
                          gig_customer=None,
                          enabled=True,
                          **kwargs):
        tip = cls.tip
        duration = TimeSpan.ONE
        finishing_time = None
        if scheduled_time is None:
            logger.error('Rabbit Hole Gig {} : Not a valid scheduled_time.',
                         cls)
            return
        for (start_time, end_time) in cls.gig_time().get_schedule_entries():
            if scheduled_time.day() == start_time.day():
                if scheduled_time.hour() == start_time.hour():
                    if scheduled_time.minute() == start_time.minute():
                        duration = end_time - start_time
                        finishing_time = scheduled_time + duration
                        break
        if finishing_time == None:
            logger.error(
                'Rabbit Hole Gig {} : No gig start_time found for scheduled_time {} ',
                cls, scheduled_time)
            return
        pay_rate = cls.gig_pay.lower_bound / duration.in_hours()
        description = cls.gig_picker_localization_format(
            cls.gig_pay.lower_bound, pay_rate, scheduled_time, finishing_time,
            tip.tip_title(), tip.tip_text(), gig_customer)
        if not enabled and cls.disabled_tooltip is not None:
            row_tooltip = lambda *_: cls.disabled_tooltip(owner)
        elif cls.display_description is None:
            row_tooltip = None
        else:
            row_tooltip = lambda *_: cls.display_description(owner)
        customer_description = cls.odd_job_tuning.customer_description(
            gig_customer)
        row = OddJobPickerRow(customer_id=gig_customer.id,
                              customer_description=customer_description,
                              tip_title=tip.tip_title(),
                              tip_text=tip.tip_text(),
                              tip_icon=tip.tip_icon,
                              name=cls.display_name(owner),
                              icon=cls.display_icon,
                              row_description=description,
                              row_tooltip=row_tooltip,
                              is_enable=enabled)
        return row
 def __init__(self, *args, **kwargs):
     super().__init__(balloon_target=TunableEnumFlags(ParticipantType, ParticipantType.Invalid, description='\n                                                             Who to play balloons over relative to the interaction. \n                                                             Generally, balloon tuning will use either balloon_animation_target \n                                                             or balloon_target.'), balloon_choices=TunableList(description='\n                             A list of the balloons and balloon categories\n                             ', tunable=BalloonVariant.TunableFactory()), balloon_delay=Tunable(float, None, description='\n                             If set, the number of seconds after the start of the animation to \n                             trigger the balloon. A negative number will count backwards from the \n                             end of the animation.'), balloon_delay_random_offset=TunableRange(float, 0, minimum=0, description='\n                             The amount of randomization that is added to balloon requests. \n                             Will always offset the delay time later, and requires the delay \n                             time to be set to a number. A value of 0 has no randomization.'), balloon_chance=TunablePercent(100, description='\n                             The chance that the balloon will play.'), **kwargs)
Beispiel #24
0
class ClientObjectMixin:
    INITIAL_DEPRECIATION = TunablePercent(
        20,
        description=
        'Amount (0%%-100%%) of depreciation to apply to an object after purchase. An item worth 10 in the catalog if tuned at 20%% will be worth 8 after purchase.'
    )
    FADE_DURATION = TunableSimMinute(
        1.2, description='Default fade time (in sim minutes) for objects.')
    VISIBLE_TO_AUTOMATION = True
    _get_next_ui_metadata_handle = uid.UniqueIdGenerator(min_uid=1)
    HOVERTIP_HANDLE = 0
    ui_metadata = distributor.sparse.SparseField(
        ui_protocols.UiObjectMetadata, distributor.ops.SetUiObjectMetadata)
    _generic_ui_metadata_setters = {}
    FORWARD_OFFSET = 0.04

    def __init__(self, definition, **kwargs):
        super().__init__(definition, **kwargs)
        if definition is not None:
            self.apply_definition(definition, **kwargs)
        self._ui_metadata_stack = None
        self._ui_metadata_handles = None
        self._ui_metadata_cache = None
        self.primitives = distributor.ops.DistributionSet(self)
        zone_id = services.current_zone_id()
        self._location = sims4.math.Location(
            sims4.math.Transform(),
            routing.SurfaceIdentifier(zone_id, 0,
                                      routing.SurfaceType.SURFACETYPE_WORLD))
        self._children_objects = None
        self._scale = 1
        self._parent_type = ObjectParentType.PARENT_NONE
        self._parent_location = 0
        self._build_buy_lockout = False
        self._build_buy_lockout_alarm_handler = None
        self._tint = None
        self._opacity = None
        self._censor_state = None
        self._geometry_state = None
        self._geometry_state_overrides = None
        self._standin_model = None
        self._visibility = None
        self._visibility_flags = None
        self._material_state = None
        self._reference_arb = None
        self._audio_effects = None
        self._video_playlist = None
        self._painting_state = None
        self.custom_name = None
        self.custom_description = None
        self._multicolor = None
        self._display_number = None
        self._awareness_scores = None
        self._scratched = False
        self._base_value = definition.price
        self._needs_post_bb_fixup = False
        self._needs_depreciation = False
        self._swapping_to_parent = None
        self._swapping_from_parent = None
        self._on_children_changed = None
        self.allow_opacity_change = True
        self._wind_speed_effect = None

    def get_create_op(self, *args, **kwargs):
        additional_ops = list(self.get_additional_create_ops_gen())
        return distributor.ops.ObjectCreate(self,
                                            *args,
                                            additional_ops=additional_ops,
                                            **kwargs)

    @forward_to_components_gen
    def get_additional_create_ops_gen(self):
        pass

    def get_create_after_objs(self):
        parent = self.parent_object(child_type=ChildrenType.BB_ONLY)
        if parent is not None:
            return (parent, )
        return ()

    def get_delete_op(self, fade_duration=0):
        return distributor.ops.ObjectDelete(fade_duration=fade_duration)

    @forward_to_components
    def apply_definition(self, definition, obj_state=0):
        if not isinstance(definition, objects.definition.Definition):
            definition = services.definition_manager().get(definition)
        self._model = definition.get_model(obj_state)
        self._material_variant = definition.material_variant
        self._rig = definition.get_rig(obj_state)
        self._slot = definition.get_slot(obj_state)
        self._slots_resource = definition.get_slots_resource(obj_state)
        self._state_index = obj_state

    def set_definition(self, definition_id, ignore_rig_footprint=False):
        new_definition = services.definition_manager().get(definition_id)
        (result, error) = self.definition.is_similar(
            new_definition, ignore_rig_footprint=ignore_rig_footprint)
        if not result:
            logger.error(
                'Trying to set the definition {} to an incompatible definition {}.\n {}',
                self.definition.id,
                definition_id,
                error,
                owner='nbaker')
            return False
        services.definition_manager().unregister_definition(
            self.definition.id, self)
        self.apply_definition(new_definition, self._state_index)
        self.definition = new_definition
        services.definition_manager().register_definition(
            new_definition.id, self)
        self.resend_model_with_material_variant()
        self.resend_slot()
        self.resend_state_index()
        op = distributor.ops.SetObjectDefinitionId(definition_id)
        distributor.system.Distributor.instance().add_op(self, op)
        return True

    @property
    def hover_tip(self):
        if self._ui_metadata_stack is None or ClientObjectMixin.HOVERTIP_HANDLE not in self._ui_metadata_handles:
            return
        (_, _,
         value) = self._ui_metadata_handles[ClientObjectMixin.HOVERTIP_HANDLE]
        return value

    @hover_tip.setter
    def hover_tip(self, value):
        if value is not None:
            if self._ui_metadata_stack is None:
                self._ui_metadata_stack = []
                self._ui_metadata_handles = {}
                self._ui_metadata_cache = {}
            data = self._ui_metadata_handles.get(
                ClientObjectMixin.HOVERTIP_HANDLE)
            if data is not None:
                self._ui_metadata_stack.remove(data)
            data = (ClientObjectMixin.HOVERTIP_HANDLE, 'hover_tip', value)
            self._ui_metadata_stack.append(data)
            self._ui_metadata_handles[ClientObjectMixin.HOVERTIP_HANDLE] = data

    def add_ui_metadata(self, name, value):
        if self._ui_metadata_stack is None:
            self._ui_metadata_stack = []
            self._ui_metadata_handles = {}
            self._ui_metadata_cache = {}
        if name not in self._ui_metadata_cache:
            default_value = type(self).ui_metadata.generic_getter(name)(self)
            self._ui_metadata_cache[name] = default_value
        handle = self._get_next_ui_metadata_handle()
        data = (handle, name, value)
        self._ui_metadata_stack.append(data)
        self._ui_metadata_handles[handle] = data
        return handle

    def get_ui_metadata(self, handle):
        return self._ui_metadata_handles[handle]

    def remove_ui_metadata(self, handle):
        if self._ui_metadata_stack is not None:
            self._ui_metadata_stack.remove(self._ui_metadata_handles[handle])

    def update_ui_metadata(self, use_cache=True):
        if self._ui_metadata_stack is None:
            return
        ui_metadata = {}
        for (_, name, value) in self._ui_metadata_stack:
            ui_metadata[name] = value
        for (name, value) in ui_metadata.items():
            if name in self._ui_metadata_cache and self._ui_metadata_cache[
                    name] == value and use_cache:
                continue
            if name in self._generic_ui_metadata_setters:
                setter = self._generic_ui_metadata_setters[name]
            else:
                setter = type(self).ui_metadata.generic_setter(name,
                                                               auto_reset=True)
                self._generic_ui_metadata_setters[name] = setter
            try:
                setter(self, value)
            except (ValueError, TypeError):
                logger.error(
                    'Error trying to set field {} to value {} in object {}.',
                    name,
                    value,
                    self,
                    owner='camilogarcia')
        for name in self._ui_metadata_cache.keys() - ui_metadata.keys():
            try:
                if name in self._generic_ui_metadata_setters:
                    self._generic_ui_metadata_setters[name](self, None)
            except (ValueError, TypeError):
                logger.error(
                    'Error trying to set field {} to default in object {}.',
                    name,
                    self,
                    owner='nabaker')
        self._ui_metadata_cache = ui_metadata

    @property
    def swapping_to_parent(self):
        return self._swapping_to_parent

    @property
    def swapping_from_parent(self):
        return self._swapping_from_parent

    @contextmanager
    def _swapping_parents(self, old_parent, new_parent):
        self._swapping_from_parent = old_parent
        self._swapping_to_parent = new_parent
        try:
            yield None
        finally:
            self._swapping_from_parent = None
            self._swapping_to_parent = None

    @property
    def location(self):
        return self._location

    @distributor.fields.Field(op=distributor.ops.SetLocation)
    def _location_field_internal(self):
        return self

    resend_location = _location_field_internal.get_resend()

    @location.setter
    def location(self, new_location):
        self.set_location_without_distribution(new_location)
        self.resend_location()

    def set_location_without_distribution(self, new_location):
        if not isinstance(new_location, sims4.math.Location):
            raise TypeError()
        if not (new_location == self._location and
                (self.parts is not None and new_location.parent is not None)
                and new_location.parent.parts is not None):
            return
        old_location = self._location
        events = [(self, old_location)]
        for child in self.get_all_children_recursive_gen():
            events.append((child, child._location))
        if new_location.parent != old_location.parent:
            self.pre_parent_change(new_location.parent)
            with self._swapping_parents(old_location.parent,
                                        new_location.parent):
                if old_location.parent is not None:
                    old_location.parent._remove_child(
                        self, new_parent=new_location.parent)
                if new_location.parent is not None:
                    new_location.parent._add_child(self, new_location)
            visibility_state = self.visibility or VisibilityState()
            if new_location.parent is not None and new_location.parent._disable_child_footprint_and_shadow:
                visibility_state.enable_drop_shadow = False
            else:
                visibility_state.enable_drop_shadow = True
            self.visibility = visibility_state
        if new_location.parent is not None:
            current_inventory = self.get_inventory()
            if current_inventory is not None and not current_inventory.try_remove_object_by_id(
                    self.id):
                raise RuntimeError(
                    'Unable to remove object: {} from the inventory: {}, parenting request will be ignored.'
                    .format(self, current_inventory))
        posture_graph_service = services.current_zone().posture_graph_service
        with posture_graph_service.object_moving(self):
            self._location = new_location
            if self.parts:
                for part in self.parts:
                    part.on_owner_location_changed()
        if new_location.parent != old_location.parent:
            self.on_parent_change(new_location.parent)
        for (obj, old_value) in events:
            if obj is not self:
                new_location = obj.location.clone()
                obj._location = new_location
            obj.on_location_changed(old_value)

    def set_location(self, location):
        self.location = location

    def move_to(self, **overrides):
        self.location = self._location.clone(**overrides)

    @distributor.fields.Field(op=distributor.ops.SetAudioEffects)
    def audio_effects(self):
        return self._audio_effects

    resend_audio_effects = audio_effects.get_resend()

    def append_audio_effect(self, key, audio_effect_data):
        if self._audio_effects is None:
            self._audio_effects = {}
        self._audio_effects[key] = audio_effect_data
        self.resend_audio_effects()

    def remove_audio_effect(self, key):
        if self._audio_effects is None:
            logger.error(
                'Found audio effects is None while trying to remove audio effect with key {} on {}',
                key,
                self,
                owner='jdimailig')
            return
        if key in self._audio_effects:
            del self._audio_effects[key]
        if not self._audio_effects:
            self._audio_effects = None
        self.resend_audio_effects()

    @forward_to_components
    def on_location_changed(self, old_location):
        pass

    @property
    def transform(self):
        return self._location.world_transform

    @transform.setter
    def transform(self, transform):
        if self.parent is not None:
            self.move_to(transform=transform,
                         parent=None,
                         routing_surface=self.parent.routing_surface)
            return
        self.move_to(transform=transform)

    @property
    def position(self):
        return self.transform.translation

    @property
    def position_with_forward_offset(self):
        return self.position + self.forward * ClientObjectMixin.FORWARD_OFFSET

    @property
    def intended_position_with_forward_offset(self):
        return self.intended_position + self.intended_forward * ClientObjectMixin.FORWARD_OFFSET

    @property
    def orientation(self):
        return self.transform.orientation

    @property
    def forward(self):
        return self.orientation.transform_vector(
            self.forward_direction_for_picking)

    @property
    def routing_surface(self):
        return self._location.world_routing_surface

    @property
    def level(self):
        routing_surface = self.routing_surface
        if routing_surface is None:
            return
        return routing_surface.secondary_id

    @property
    def routing_location(self):
        return self.get_routing_location_for_transform(self.transform)

    def get_routing_location_for_transform(self,
                                           transform,
                                           routing_surface=DEFAULT):
        routing_surface = self.routing_surface if routing_surface is DEFAULT else routing_surface
        return routing.Location(transform.translation, transform.orientation,
                                routing_surface)

    @property
    def intended_transform(self):
        return self.transform

    @property
    def intended_position(self):
        return self.intended_transform.translation

    @property
    def intended_forward(self):
        return self.intended_transform.orientation.transform_vector(
            self.forward_direction_for_picking)

    @property
    def intended_routing_surface(self):
        return self.routing_surface

    @property
    def parent(self):
        parent = self._location.parent
        parent = self.attempt_to_remap_parent(parent)
        if parent is not None:
            children = parent.children
            if not (self not in children and self.is_part
                    and self.part_owner in children):
                return
        return parent

    @property
    def bb_parent(self):
        return self._location.parent

    def attempt_to_remap_parent(self, parent):
        if self.is_part and parent is not None and parent.parts is not None:
            distance = None
            found_part = None
            for part in parent.parts:
                dot = sims4.math.vector_dot(self.forward, part.forward)
                if dot < -0.98:
                    new_distance = (part.position -
                                    self.position).magnitude_squared()
                    if not distance is None:
                        if new_distance <= distance:
                            if new_distance == distance and found_part.subroot_index is not None:
                                continue
                            distance = new_distance
                            found_part = part
                    if new_distance == distance and found_part.subroot_index is not None:
                        continue
                    distance = new_distance
                    found_part = part
            return found_part
        return parent

    def parent_object(self, child_type=ChildrenType.DEFAULT):
        if child_type is ChildrenType.BB_ONLY:
            parent = self.bb_parent
        else:
            parent = self.parent
        if parent is not None:
            if parent.is_part:
                parent = parent.part_owner
        return parent

    @property
    def parent_slot(self):
        parent = self._location.parent
        if parent is None:
            return
        bone_name_hash = self._location.joint_name_or_hash or self._location.slot_hash
        result = None
        for runtime_slot in parent.get_runtime_slots_gen(
                bone_name_hash=bone_name_hash):
            assert not result is not None
            result = runtime_slot
        if result is None:
            result = RuntimeSlot(parent, bone_name_hash, frozenset())
        return result

    def get_parenting_root(self):
        result = self
        next_parent = result.parent
        while next_parent is not None:
            result = next_parent
            next_parent = result.parent
        return result

    @property
    def children(self):
        if self._children_objects is not None:
            return self._children_objects[ChildrenType.DEFAULT]
        return ()

    def children_recursive_gen(self, include_self=False):
        if include_self:
            yield self
        for child in self.children:
            yield child
            for grandchild in child.children_recursive_gen():
                yield grandchild

    @assertions.hot_path
    def _children_recursive_fast_gen(self):
        yield self
        for child in self.children:
            yield from child._children_recursive_fast_gen()

    def get_all_children_gen(self):
        if self._children_objects is not None:
            for children in self._children_objects.values():
                yield from children

    def get_all_children_recursive_gen(self):
        for child in self.get_all_children_gen():
            yield child
            yield from child.get_all_children_recursive_gen()

    def clear_default_children(self):
        if self._children_objects is not None:
            self._children_objects[ChildrenType.DEFAULT].clear()

    @assertions.hot_path
    def parenting_hierarchy_gen(self):
        self_parent = self.parent
        if self_parent is not None:
            master_parent = self_parent
            master_parent_parent = master_parent.parent
            while master_parent_parent is not None:
                master_parent = master_parent_parent
                master_parent_parent = master_parent.parent
            yield from master_parent._children_recursive_fast_gen()
        else:
            yield from self._children_recursive_fast_gen()

    def on_reset_send_op(self, reset_reason):
        super().on_reset_send_op(reset_reason)
        if self.valid_for_distribution:
            if reset_reason != ResetReason.BEING_DESTROYED or self.vehicle_component is not None:
                try:
                    reset_op = distributor.ops.ResetObject(self.id)
                    dist = Distributor.instance()
                    dist.add_op(self, reset_op)
                except:
                    logger.exception(
                        'Exception thrown sending reset op for {}', self)

    def on_reset_internal_state(self, reset_reason):
        if self.valid_for_distribution and reset_reason != ResetReason.BEING_DESTROYED:
            self.geometry_state = None
            self.material_state = None
            self.resend_location()
        self._reset_reference_arb()
        super().on_reset_internal_state(reset_reason)

    def on_reset_get_interdependent_reset_records(self, reset_reason,
                                                  reset_records):
        super().on_reset_get_interdependent_reset_records(
            reset_reason, reset_records)
        for child in set(self.get_all_children_gen()):
            reset_records.append(
                ResetRecord(child, ResetReason.RESET_EXPECTED, self, 'Child'))

    @property
    def slot_hash(self):
        return self._location.slot_hash

    @slot_hash.setter
    def slot_hash(self, value):
        if self._location.slot_hash != value:
            self.location = self._location.clone(slot_hash=value)

    @property
    def bone_name_hash(self):
        return self._location.joint_name_or_hash or self._location.slot_hash

    @property
    def part_suffix(self) -> str:
        pass

    @distributor.fields.Field(op=distributor.ops.SetModel)
    def model_with_material_variant(self):
        return (self._model, self._material_variant)

    resend_model_with_material_variant = model_with_material_variant.get_resend(
    )

    @model_with_material_variant.setter
    def model_with_material_variant(self, value):
        (self._model, self._material_variant) = value

    @property
    def model(self):
        return self._model

    @model.setter
    def model(self, value):
        model_res_key = None
        if isinstance(value, sims4.resources.Key):
            model_res_key = value
        elif isinstance(value, Definition):
            model_res_key = value.get_model(index=0)
            self.set_definition(value.id, ignore_rig_footprint=True)
        else:
            if value is not None:
                logger.error(
                    'Trying to set the model of object {} to the invalid value of {}.                                The object will revert to its default model instead.',
                    self,
                    value,
                    owner='tastle')
            model_res_key = self.definition.get_model(self._state_index)
        self.model_with_material_variant = (model_res_key,
                                            self._material_variant)

    @property
    def material_variant(self):
        return self._material_variant

    @material_variant.setter
    def material_variant(self, value):
        if value is None:
            self.model_with_material_variant = (self._model, None)
        else:
            if not isinstance(value, str):
                raise TypeError('Model variant value must be a string')
            if not value:
                self.model_with_material_variant = (self._model, None)
            else:
                try:
                    variant_value = int(value)
                except ValueError:
                    variant_value = sims4.hash_util.hash32(value)
                self.model_with_material_variant = (self._model, variant_value)

    @distributor.fields.Field(op=distributor.ops.SetStandInModel)
    def standin_model(self):
        return self._standin_model

    @standin_model.setter
    def standin_model(self, value):
        self._standin_model = value

    @distributor.fields.Field(op=distributor.ops.SetObjectDefStateIndex,
                              default=0)
    def state_index(self):
        return self._state_index

    resend_state_index = state_index.get_resend()

    @distributor.fields.Field(op=distributor.ops.SetRig,
                              priority=distributor.fields.Field.Priority.HIGH)
    def rig(self):
        return self._rig

    @rig.setter
    def rig(self, value):
        if not isinstance(value, sims4.resources.Key):
            raise TypeError
        self._rig = value

    @distributor.fields.Field(op=distributor.ops.SetSlot)
    def slot(self):
        return self._slot

    resend_slot = slot.get_resend()

    @property
    def slots_resource(self):
        return self._slots_resource

    @distributor.fields.Field(op=distributor.ops.SetScale, default=1)
    def _client_scale(self):
        scale_value = self.scale
        for modifier in self.scale_modifiers_gen():
            scale_value *= modifier
        return scale_value

    _resend_client_scale = _client_scale.get_resend()

    @property
    def scale(self):
        return self._scale

    @forward_to_components_gen
    def scale_modifiers_gen(self):
        pass

    @scale.setter
    def scale(self, value):
        if self._scale != value:
            self._scale = value
            self.on_location_changed(self._location)
        self._resend_client_scale()

    @property
    def parent_type(self):
        return self._parent_type

    @parent_type.setter
    def parent_type(self, value):
        self._parent_type = value
        self._resend_parent_type_info()

    @distributor.fields.Field(op=distributor.ops.SetParentType, default=None)
    def parent_type_info(self):
        return (self._parent_type, self._parent_location)

    @parent_type_info.setter
    def parent_type_info(self, value):
        (self._parent_type, self._parent_location) = value

    _resend_parent_type_info = parent_type_info.get_resend()

    @property
    def build_buy_lockout(self):
        return self._build_buy_lockout

    @distributor.fields.Field(op=distributor.ops.SetTint, default=None)
    def tint(self):
        if self.build_buy_lockout and lockout_visualization:
            return sims4.color.ColorARGB32(23782)
        return self._tint

    @tint.setter
    def tint(self, tint_color):
        value = getattr(tint_color, 'value', tint_color)
        if value and not isinstance(value, sims4.color.ColorARGB32):
            raise TypeError('Tint value must be a Color')
        if value == sims4.color.Color.WHITE:
            self._tint = None
        else:
            self._tint = value

    resend_tint = tint.get_resend()

    @distributor.fields.Field(op=distributor.ops.SetMulticolor, default=None)
    def multicolor(self):
        return self._multicolor

    @multicolor.setter
    def multicolor(self, value):
        self._multicolor = value

    resend_multicolor = multicolor.get_resend()

    @distributor.fields.Field(op=distributor.ops.SetDisplayNumber,
                              default=None)
    def display_number(self):
        return self._display_number

    @display_number.setter
    def display_number(self, value):
        self._display_number = value

    resend_display_number = display_number.get_resend()

    def update_display_number(self, display_number=None):
        if display_number is not None:
            self.display_number = display_number
            return
        if hasattr(self, 'get_display_number'):
            self.display_number = self.get_display_number()

    @distributor.fields.Field(op=distributor.ops.SetOpacity, default=None)
    def opacity(self):
        return self._opacity

    @opacity.setter
    def opacity(self, value):
        if self.allow_opacity_change:
            self._opacity = self._clamp_opacity(value)

    def _clamp_opacity(self, value):
        if value is None:
            return
        try:
            value = float(value)
        except:
            raise TypeError('Opacity value must be a float')
        return sims4.math.clamp(0.0, value, 1.0)

    @distributor.fields.Field(op=SetAwarenessSourceOp)
    def awareness_scores(self):
        return self._awareness_scores

    resend_awareness_scores = awareness_scores.get_resend()

    def add_awareness_scores(self, awareness_sources):
        if self._awareness_scores is None:
            self._awareness_scores = Counter()
        self._awareness_scores.update(awareness_sources)
        self.resend_awareness_scores()

    def remove_awareness_scores(self, awareness_sources):
        if self._awareness_scores is None:
            return
        self._awareness_scores.subtract(awareness_sources)
        for awareness_channel in tuple(self.awareness_scores):
            if not self._awareness_scores[awareness_channel]:
                del self._awareness_scores[awareness_channel]
        if not self._awareness_scores:
            self._awareness_scores = None
        self.resend_awareness_scores()

    def add_geometry_state_override(self, original_geometry_state,
                                    override_geometry_state):
        if self._geometry_state_overrides is None:
            self._geometry_state_overrides = {}
        original_state_hash = sims4.hash_util.hash32(original_geometry_state)
        override_state_hash = sims4.hash_util.hash32(override_geometry_state)
        logger.assert_raise(
            original_state_hash not in self._geometry_state_overrides,
            'add_geometry_state_override does not support multiple overrides per state'
        )
        self._geometry_state_overrides[
            original_state_hash] = override_state_hash
        self.geometry_state = self.geometry_state

    def remove_geometry_state_override(self, original_geometry_state):
        state_hash = sims4.hash_util.hash32(original_geometry_state)
        if state_hash in self._geometry_state_overrides:
            del self._geometry_state_overrides[state_hash]
        if not self._geometry_state_overrides:
            self._geometry_state_overrides = None

    @distributor.fields.Field(op=distributor.ops.SetGeometryState,
                              default=None)
    def geometry_state(self):
        return self._geometry_state

    @geometry_state.setter
    def geometry_state(self, value):
        self._geometry_state = self._get_geometry_state_for_value(value)

    def _get_geometry_state_for_value(self, value):
        if not value:
            return
        if isinstance(value, str):
            state_hash = sims4.hash_util.hash32(value)
        elif isinstance(value, int):
            state_hash = value
        if self._geometry_state_overrides is not None:
            if state_hash in self._geometry_state_overrides:
                state_hash = self._geometry_state_overrides[state_hash]
        return state_hash

    @distributor.fields.Field(op=distributor.ops.SetCensorState, default=None)
    def censor_state(self):
        return self._censor_state

    @censor_state.setter
    def censor_state(self, value):
        try:
            value = CensorState(value)
        except:
            raise TypeError('Censor State value must be an int')
        self._censor_state = value

    @distributor.fields.Field(op=distributor.ops.SetVisibility, default=None)
    def visibility(self):
        return self._visibility

    @visibility.setter
    def visibility(self, value):
        if not isinstance(value, VisibilityState):
            raise TypeError(
                'Visibility must be set to value of type VisibilityState')
        self._visibility = value
        if value is not None:
            if value.visibility is True:
                if value.inherits is False:
                    if value.enable_drop_shadow is False:
                        self._visibility = None

    @distributor.fields.Field(op=distributor.ops.SetVisibilityFlags)
    def visibility_flags(self):
        return self._visibility_flags

    @visibility_flags.setter
    def visibility_flags(self, value):
        self._visibility_flags = value

    @distributor.fields.Field(op=distributor.ops.SetMaterialState,
                              default=None)
    def material_state(self):
        return self._material_state

    @material_state.setter
    def material_state(self, value):
        if value is None:
            self._material_state = None
        else:
            if not isinstance(value, MaterialState):
                raise TypeError(
                    'Material State must be set to value of type MaterialState'
                )
            if value.state_name_hash == 0:
                self._material_state = None
            else:
                self._material_state = value

    @property
    def material_hash(self):
        if self.material_state is None:
            return 0
        else:
            return self.material_state.state_name_hash

    @distributor.fields.Field(op=distributor.ops.StartArb, default=None)
    def reference_arb(self):
        return self._reference_arb

    def update_reference_arb(self, arb):
        if self._reference_arb is None:
            self._reference_arb = animation.arb.Arb()
        native.animation.update_post_condition_arb(self._reference_arb, arb)

    def _reset_reference_arb(self):
        if self._reference_arb is not None:
            reset_arb_element = ArbElement(animation.arb.Arb())
            reset_arb_element.add_object_to_reset(self)
            reset_arb_element.distribute()
            reset_arb_element.cleanup()
        self._reference_arb = None

    _NO_SLOTS = EMPTY_SET

    @property
    def deco_slot_size(self):
        return get_object_decosize(self.definition.id)

    @property
    def deco_slot_types(self):
        return DecorativeSlotTuning.get_slot_types_for_object(
            self.deco_slot_size)

    @property
    def slot_type_set(self):
        key = get_object_slotset(self.definition.id)
        return get_slot_type_set_from_key(key)

    @property
    def slot_types(self):
        slot_type_set = self.slot_type_set
        if slot_type_set is not None:
            return slot_type_set.slot_types
        return self._NO_SLOTS

    @property
    def ideal_slot_types(self):
        carryable = self.get_component(CARRYABLE_COMPONENT)
        if carryable is not None:
            slot_type_set = carryable.ideal_slot_type_set
            if slot_type_set is not None:
                return slot_type_set.slot_types & (self.slot_types
                                                   | self.deco_slot_types)
        return self._NO_SLOTS

    @property
    def all_valid_slot_types(self):
        return self.deco_slot_types | self.slot_types

    def _add_child(self, child, location):
        if self._children_objects is None:
            self._children_objects = defaultdict(WeakSet)
        if not isinstance(self.children, (WeakSet, set)):
            raise TypeError(
                "self.children is not a WeakSet or a set, it's {}".format(
                    self.children))
        bone_name_hash = location.joint_name_or_hash or location.slot_hash
        found_runtime_slot = None
        for runtime_slot in location.parent.get_runtime_slots_gen(
                bone_name_hash=bone_name_hash):
            assert not found_runtime_slot is not None
            found_runtime_slot = runtime_slot
        if found_runtime_slot is not None:
            for slot_type in found_runtime_slot.slot_types:
                if not slot_type.bb_only:
                    self._children_objects[ChildrenType.DEFAULT].add(child)
                    break
            else:
                self._children_objects[ChildrenType.BB_ONLY].add(child)
        else:
            self._children_objects[ChildrenType.DEFAULT].add(child)
        if self.parts:
            for part in self.parts:
                part.on_children_changed()
        self.on_child_added(child, location)

    def _remove_child(self, child, new_parent=None):
        if not isinstance(self.children, (WeakSet, set)):
            raise TypeError(
                "self.children is not a WeakSet or a set, it's {}".format(
                    self.children))
        for (_, weak_obj_set) in self._children_objects.items():
            if child in weak_obj_set:
                weak_obj_set.discard(child)
                break
        if self.parts:
            for part in self.parts:
                part.on_children_changed()
        self.on_child_removed(child, new_parent=new_parent)

    @forward_to_components
    def on_remove_from_client(self):
        super().on_remove_from_client()
        for primitive in tuple(self.primitives):
            primitive.detach(self)

    def post_remove(self):
        super().post_remove()
        for primitive in tuple(self.primitives):
            primitive.detach(self)
        self.primitives = None

    @forward_to_components
    def on_child_added(self, child, location):
        if self._on_children_changed is None:
            return
        self._on_children_changed(child, location=location)

    @forward_to_components
    def on_child_removed(self, child, new_parent=None):
        if self._on_children_changed is None:
            return
        self._on_children_changed(child, new_parent=new_parent)

    @forward_to_components
    def pre_parent_change(self, parent):
        pass

    @forward_to_components
    def on_parent_change(self, parent):
        caches.clear_all_caches()
        if parent is None:
            self.parent_type = ObjectParentType.PARENT_NONE
        else:
            self.parent_type = ObjectParentType.PARENT_OBJECT

    def create_parent_location(self,
                               parent,
                               transform=sims4.math.Transform.IDENTITY(),
                               joint_name_or_hash=None,
                               slot_hash=0,
                               routing_surface=None):
        if parent is not None:
            if self in parent.ancestry_gen():
                raise ValueError(
                    'Invalid parent value (parent chain is circular)')
            if joint_name_or_hash:
                native.animation.get_joint_transform_from_rig(
                    parent.rig, joint_name_or_hash)
            if slot_hash and not parent.has_slot(slot_hash):
                raise KeyError(
                    'Could not slot {}/{} in slot {} on {}/{}'.format(
                        self, self.definition, hex(slot_hash), parent,
                        parent.definition))
        part_joint_name = joint_name_or_hash or slot_hash
        if parent is not None:
            if part_joint_name is not None:
                if not parent.is_part:
                    if parent.parts:
                        for part in parent.parts:
                            if part.has_slot(part_joint_name):
                                parent = part
                                break
        new_location = self._location.clone(
            transform=transform,
            joint_name_or_hash=joint_name_or_hash,
            slot_hash=slot_hash,
            parent=parent,
            routing_surface=routing_surface)
        return new_location

    def set_parent(self, *args, **kwargs):
        new_location = self.create_parent_location(*args, **kwargs)
        self.location = new_location

    def clear_parent(self, transform, routing_surface):
        return self.set_parent(None,
                               transform=transform,
                               routing_surface=routing_surface)

    def remove_reference_from_parent(self):
        parent = self.bb_parent
        if parent is not None:
            parent._remove_child(self, new_parent=UNSET)

    @distributor.fields.Field(op=distributor.ops.VideoSetPlaylistOp,
                              default=None)
    def video_playlist(self):
        return self._video_playlist

    @video_playlist.setter
    def video_playlist(self, playlist):
        self._video_playlist = playlist

    _resend_video_playlist = video_playlist.get_resend()

    def fade_opacity(self,
                     opacity: float,
                     duration: float,
                     immediate=False,
                     additional_channels=None):
        if self.allow_opacity_change:
            opacity = self._clamp_opacity(opacity)
            if opacity != self._opacity:
                self._opacity = opacity
                fade_op = distributor.ops.FadeOpacity(opacity,
                                                      duration,
                                                      immediate=immediate)
                if additional_channels:
                    for channel in additional_channels:
                        fade_op.add_additional_channel(*channel)
                distributor.ops.record(self, fade_op)

    def fade_in(self,
                fade_duration=None,
                immediate=False,
                additional_channels=None):
        if self.allow_opacity_change:
            if fade_duration is None:
                fade_duration = ClientObjectMixin.FADE_DURATION
            if self.visibility is not None:
                if not self.visibility.visibility:
                    self.visibility = VisibilityState()
                    self.opacity = 0
            self.fade_opacity(1,
                              fade_duration,
                              immediate=immediate,
                              additional_channels=additional_channels)

    def fade_out(self,
                 fade_duration=None,
                 immediate=False,
                 additional_channels=None):
        if self.allow_opacity_change:
            if fade_duration is None:
                fade_duration = ClientObjectMixin.FADE_DURATION
            self.fade_opacity(0,
                              fade_duration,
                              immediate=immediate,
                              additional_channels=additional_channels)

    @distributor.fields.Field(op=distributor.ops.SetValue, default=None)
    def current_value(self):
        new_value = self._base_value
        statistic_component = self.statistic_component
        if statistic_component is not None:
            new_value += statistic_component.get_added_monetary_value()
        state_component = self.state_component
        if state_component is not None:
            return max(
                round(new_value * state_component.state_based_value_mod), 0)
        return max(new_value, 0)

    @current_value.setter
    def current_value(self, value):
        state_component = self.state_component
        if state_component is not None:
            self.base_value = value / state_component.state_based_value_mod
        else:
            self.base_value = value

    _resend_current_value = current_value.get_resend()

    def update_current_value(self, update_tooltip=True):
        self._resend_current_value()
        if update_tooltip:
            self.update_tooltip_field(TooltipFieldsComplete.simoleon_value,
                                      self.current_value)

    @property
    def base_value(self):
        return self._base_value

    @base_value.setter
    def base_value(self, value):
        self._base_value = round(max(value, 0))
        update_tooltip = self.get_tooltip_field(
            TooltipFieldsComplete.simoleon_value) is not None
        self.update_current_value(update_tooltip=update_tooltip)

    @property
    def depreciated_value(self):
        if not self.definition.get_can_depreciate():
            return self.catalog_value
        return self.catalog_value * (1 - self.INITIAL_DEPRECIATION)

    @property
    def catalog_value(self):
        return self.get_object_property(GameObjectProperty.CATALOG_PRICE)

    @property
    def depreciated(self):
        return not self._needs_depreciation

    def set_post_bb_fixup_needed(self):
        self._needs_post_bb_fixup = True
        self._needs_depreciation = True

    def try_post_bb_fixup(self, force_fixup=False, active_household_id=0):
        if force_fixup or self._needs_depreciation:
            if force_fixup:
                self._needs_depreciation = True
            self._on_try_depreciation(active_household_id=active_household_id)
        if force_fixup or self._needs_post_bb_fixup:
            self._needs_post_bb_fixup = False
            self.on_post_bb_fixup()

    @forward_to_components
    def on_post_bb_fixup(self):
        services.get_event_manager().process_events_for_household(
            test_events.TestEvent.ObjectAdd,
            services.household_manager().get(self._household_owner_id),
            obj=self)

    def _on_try_depreciation(self, active_household_id=0):
        if self._household_owner_id != active_household_id:
            return
        self._needs_depreciation = False
        if not self.definition.get_can_depreciate():
            return
        self.base_value = floor(self._base_value *
                                (1 - self.INITIAL_DEPRECIATION))

    def register_for_on_children_changed_callback(self, callback):
        if self._on_children_changed is None:
            self._on_children_changed = CallableList()
        if callback not in self._on_children_changed:
            self._on_children_changed.append(callback)

    def unregister_for_on_children_changed_callback(self, callback):
        if self._on_children_changed is not None:
            if callback in self._on_children_changed:
                self._on_children_changed.remove(callback)
            if not self._on_children_changed:
                self._on_children_changed = None

    @distributor.fields.Field(op=distributor.ops.SetScratched, default=False)
    def scratched(self):
        return self._scratched

    @scratched.setter
    def scratched(self, scratched):
        self._scratched = scratched

    @distributor.fields.Field(op=distributor.ops.SetWindSpeedEffect,
                              default=None)
    def wind_speed_level(self):
        return self._wind_speed_effect

    @wind_speed_level.setter
    def wind_speed_level(self, value):
        self._wind_speed_effect = value.wind_speed
Beispiel #25
0
class LaundryTuning:
    GENERATE_CLOTHING_PILE = TunableTuple(description='\n        The tunable to generate clothing pile on the lot. This will be called\n        when we find laundry hero objects on the lot and there is no hamper\n        available.\n        ', loot_to_apply=TunableReference(description='\n            Loot to apply for generating clothing pile.\n            ', manager=services.get_instance_manager(sims4.resources.Types.ACTION), class_restrictions=('LootActions',), pack_safe=True), naked_outfit_category=TunableSet(description="\n            Set of outfits categories which is considered naked.\n            When Sim switches FROM these outfits, it won't generate the pile.\n            When Sim switches TO these outfits, it won't apply laundry reward\n            or punishment.\n            ", tunable=TunableEnumEntry(tunable_type=OutfitCategory, default=OutfitCategory.EVERYDAY, invalid_enums=(OutfitCategory.CURRENT_OUTFIT,))), no_pile_outfit_category=TunableSet(description="\n            Set of outfits categories which will never generate the pile.\n            When Sim switches FROM or TO these outfits, it won't generate the\n            pile.\n            \n            Laundry reward or punishment will still be applied to the Sim when \n            switching FROM or TO these outfits.\n            ", tunable=TunableEnumEntry(tunable_type=OutfitCategory, default=OutfitCategory.EVERYDAY, invalid_enums=(OutfitCategory.CURRENT_OUTFIT,))), no_pile_interaction_tag=TunableEnumWithFilter(description='\n            If interaction does spin clothing change and has this tag, it will\n            generate no clothing pile.\n            ', tunable_type=Tag, default=Tag.INVALID, filter_prefixes=('interaction',)))
    HAMPER_OBJECT_TAGS = TunableTags(description='\n        Tags that considered hamper objects.\n        ', filter_prefixes=('func',))
    LAUNDRY_HERO_OBJECT_TAGS = TunableTags(description='\n        Tags of laundry hero objects. Placing any of these objects on the lot\n        will cause the service to generate clothing pile for each Sims on the\n        household after spin clothing change.\n        ', filter_prefixes=('func',))
    NOT_DOING_LAUNDRY_PUNISHMENT = TunableTuple(description='\n        If no Sim in the household unload completed laundry in specific\n        amount of time, the negative loot will be applied to Sim household \n        on spin clothing change to engage them doing laundry.\n        ', timeout=TunableSimMinute(description="\n            The amount of time in Sim minutes, since the last time they're \n            finishing laundry, before applying the loot.\n            ", default=2880, minimum=1), loot_to_apply=TunableReference(description='\n            Loot defined here will be applied to the Sim in the household\n            on spin clothing change if they are not doing laundry for \n            a while.\n            ', manager=services.get_instance_manager(sims4.resources.Types.ACTION), class_restrictions=('LootActions',), pack_safe=True))
    PUT_AWAY_FINISHED_LAUNDRY = TunableTuple(description='\n        The tunable to update laundry service on Put Away finished laundry\n        interaction.\n        ', interaction_tag=TunableEnumWithFilter(description='\n            Tag that represent the put away finished laundry interaction which \n            will update Laundry Service data.\n            ', tunable_type=Tag, default=Tag.INVALID, filter_prefixes=('interaction',)), laundry_condition_states=TunableTuple(description='\n            This is the state type of completed laundry object condition \n            which will aggregate the data to the laundry service.\n            ', condition_states=TunableList(description='\n                A list of state types to be stored on laundry service.\n                ', tunable=TunableStateTypeReference(pack_safe=True), unique_entries=True), excluded_states=TunableList(description='\n                A list of state values of Condition States which will not \n                be added to the laundry service.\n                ', tunable=TunableStateValueReference(pack_safe=True), unique_entries=True)), laundry_condition_timeout=TunableSimMinute(description='\n            The amount of time in Sim minutes that the individual laundry\n            finished conditions will be kept in the laundry conditions \n            aggregate data.\n            ', default=1440, minimum=0), conditions_and_rewards_map=TunableMapping(description='\n            Mapping of laundry conditions and loot rewards.\n            ', key_type=TunableReference(manager=services.get_instance_manager(sims4.resources.Types.OBJECT_STATE), pack_safe=True), value_type=TunableReference(manager=services.get_instance_manager(sims4.resources.Types.ACTION), class_restrictions=('LootActions',), pack_safe=True)))
    PUT_CLOTHING_PILE_ON_HAMPER = TunableTuple(description='\n        The Tunable to directly put generated clothing pile in the hamper.\n        ', chance=TunablePercent(description='\n            The chance that a clothing pile will be put directly in the hamper. \n            Tune the value in case putting clothing pile in hamper every \n            spin-outfit-change feeling excessive.\n            ', default=100), clothing_pile=TunableTuple(description="\n            Clothing pile object that will be created and put into the hamper \n            automatically. \n            \n            You won't see the object on the lot since it will go directly to \n            the hamper. We create it because we need to transfer all of the \n            commodities data and average the values into the hamper precisely.\n            ", definition=TunablePackSafeReference(description='\n                Reference to clothing pile object definition.\n                ', manager=services.definition_manager()), initial_states=TunableList(description='\n                A list of states to apply to the clothing pile as soon as it \n                is created.\n                ', tunable=TunableTuple(description='\n                    The state to apply and optional to decide if the state \n                    should be applied.\n                    ', state=TunableStateValueReference(pack_safe=True), tests=TunableTestSet()))), full_hamper_state=TunableStateValueReference(description='\n            The state of full hamper which make the hamper is unavailable to \n            add new clothing pile in it.\n            ', pack_safe=True), loots_to_apply=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)), tests=TunableTestSet(description='\n            The test to run on the Sim that must pass in order for putting\n            clothing pile automatically to the hamper. These tests will only \n            be run when we have available hamper on the lot.\n            '))
Beispiel #26
0
 def __init__(self, **kwargs):
     super().__init__(
         gender=TunableEnumEntry(
             description=
             "\n                The Sim's gender.\n                ",
             tunable_type=Gender,
             default=None),
         species=TunableEnumEntry(
             description=
             "\n                The Sim's species.\n                ",
             tunable_type=SpeciesExtended,
             default=SpeciesExtended.HUMAN,
             invalid_enums=(SpeciesExtended.INVALID, )),
         age_variant=TunableVariant(
             description=
             "\n                The sim's age for creation. Can be a literal age or random\n                between two ages.\n                ",
             literal=LiteralAge.TunableFactory(),
             random=RandomAge.TunableFactory()),
         resource_key=OptionalTunable(
             description=
             '\n                If enabled, the Sim will be created using a saved SimInfo file.\n                ',
             tunable=TunableResourceKey(
                 description=
                 '\n                    The SimInfo file to use.\n                    ',
                 default=None,
                 resource_types=(sims4.resources.Types.SIMINFO, ))),
         full_name=TunableVariant(
             description=
             '\n                If specified, then defines how the Sims name will be determined.\n                ',
             enabled=TunableLocalizedString(
                 description=
                 "\n                    The Sim's name will be determined by this localized string. \n                    Their first, last and full name will all be set to this.                \n                    "
             ),
             name_type=TunableEnumEntry(
                 description=
                 '\n                    The sim name type to use when generating the Sims name\n                    randomly.\n                    ',
                 tunable_type=SimNameType,
                 default=SimNameType.DEFAULT),
             locked_args={'disabled': None},
             default='disabled'),
         tunable_tag_set=TunableReference(
             description=
             '\n                The set of tags that this template uses for CAS creation.\n                ',
             manager=services.get_instance_manager(
                 sims4.resources.Types.TAG_SET),
             allow_none=True,
             class_restrictions=('TunableTagSet', )),
         weighted_tag_lists=TunableList(
             description=
             '\n                A list of weighted tag lists. Each weighted tag list adds\n                a single tag to the set of tags to use for Sim creation.\n                ',
             tunable=TunableReference(
                 description=
                 '\n                    A weighted tag list. A single tag is added to the set of\n                    tags for Sim creation from this list based on the weights.\n                    ',
                 manager=services.get_instance_manager(
                     sims4.resources.Types.TAG_SET),
                 class_restrictions=('TunableWeightedTagList', ))),
         filter_flag=TunableEnumFlags(
             description=
             '\n                Define how to handle part randomization for the generated outfit.\n                ',
             enum_type=OutfitFilterFlag,
             default=OutfitFilterFlag.USE_EXISTING_IF_APPROPRIATE
             | OutfitFilterFlag.USE_VALID_FOR_LIVE_RANDOM,
             allow_no_flags=True),
         body_type_chance_overrides=TunableMapping(
             description=
             '\n                Define body type chance overrides for the generate outfit. For\n                example, if BODYTYPE_HAT is mapped to 100%, then the outfit is\n                guaranteed to have a hat if any hat matches the specified tags.\n                ',
             key_type=BodyType,
             value_type=TunablePercent(
                 description=
                 '\n                    The chance that a part is applied to the corresponding body\n                    type.\n                    ',
                 default=100)),
         body_type_match_not_found_policy=TunableMapping(
             description=
             '\n                The policy we should take for a body type that we fail to find a\n                match for. Primary example is to use MATCH_NOT_FOUND_KEEP_EXISTING\n                for generating a tshirt and making sure a sim wearing full body has\n                a lower body cas part.\n                ',
             key_type=BodyType,
             value_type=MatchNotFoundPolicy),
         **kwargs)
class RebateManager:
    __qualname__ = 'RebateManager'
    TRAIT_REBATE_MAP = TunableMapping(
        description=
        '\n        A mapping of traits and the tags of objects which provide a rebate for\n        the given trait.\n        ',
        key_type=TunableReference(
            description=
            '\n            If the Sim has this trait, any objects purchased with the given\n            tag(s) below will provide a rebate.\n            ',
            manager=services.trait_manager()),
        value_type=TunableTuple(
            description=
            '\n            The information about the rebates the player should get for having\n            the mapped trait.\n            ',
            valid_objects=TunableVariant(
                description=
                '\n                The items to which the rebate will be applied.\n                ',
                by_tag=TunableSet(
                    description=
                    '\n                    The rebate will only be applied to objects purchased with the\n                    tags in this list.\n                    ',
                    tunable=TunableEnumEntry(tag.Tag, tag.Tag.INVALID)),
                locked_args={'all_purchases': None}),
            rebate_percentage=TunablePercent(
                description=
                '\n                The percentage of the catalog price that the player will get\n                back in the rebate.\n                ',
                default=10)))
    REBATE_PAYMENT_SCHEDULE = TunableWeeklyScheduleFactory(
        description=
        '\n        The schedule when accrued rebates will be paid out.\n        '
    )
    REBATE_NOTIFICATION = UiDialogNotification.TunableFactory(
        description=
        '\n        The notification that will show when the player receives their rebate money.\n        ',
        locked_args={'text': None})
    REBATE_NOTIFICATION_HEADER = TunableLocalizedString(
        description=
        '\n        The header for the rebate notification that displays when the households\n        gets their rebate payout.\n        '
    )
    REBATE_NOTIFICATION_LINE_ITEM = TunableLocalizedStringFactory(
        description=
        '\n        Each trait that gave rebates will generate a new line item on the notification.\n        {0.String} = trait name\n        {1.Money} = amount of rebate money received from the trait.\n        '
    )

    def __init__(self, household):
        self._household = household
        self._rebates = Counter()
        self._schedule = None

    def add_rebate_for_object(self, obj):
        for (trait, rebate_info) in self.TRAIT_REBATE_MAP.items():
            rebate_percentage = rebate_info.rebate_percentage
            valid_objects = rebate_info.valid_objects
            while self._sim_in_household_has_trait(trait):
                if valid_objects is None or self._object_has_required_tags(
                        obj, valid_objects):
                    self._rebates[
                        trait] += obj.catalog_value * rebate_percentage
        if self._rebates:
            self.start_rebate_schedule()

    def _sim_in_household_has_trait(self, trait):
        return any(
            s.trait_tracker.has_trait(trait)
            for s in self._household.sim_info_gen())

    @staticmethod
    def _object_has_required_tags(obj, valid_tags):
        return set(obj.tags) & set(valid_tags)

    def clear_rebates(self):
        self._rebates.clear()

    def start_rebate_schedule(self):
        if self._schedule is None:
            self._schedule = self.REBATE_PAYMENT_SCHEDULE(
                start_callback=self._payout_rebates, schedule_immediate=False)

    def _payout_rebates(self, *_):
        if not self._rebates:
            return
        active_sim = self._household.client.active_sim
        line_item_text = LocalizationHelperTuning.get_new_line_separated_strings(
            *(self.REBATE_NOTIFICATION_LINE_ITEM(t.display_name(active_sim), a)
              for (t, a) in self._rebates.items()))
        notification_text = LocalizationHelperTuning.get_new_line_separated_strings(
            self.REBATE_NOTIFICATION_HEADER, line_item_text)
        dialog = self.REBATE_NOTIFICATION(
            active_sim, text=lambda *_, **__: notification_text)
        dialog.show_dialog()
        total_rebate_amount = sum(self._rebates.values())
        self._household.funds.add(
            total_rebate_amount,
            reason=Consts_pb2.TELEMETRY_MONEY_ASPIRATION_REWARD,
            sim=active_sim)
        self.clear_rebates()
class ObjectTeleportationComponent(Component, HasTunableFactory, AutoFactoryInit, component_name=types.OBJECT_TELEPORTATION_COMPONENT):
    ON_CLIENT_CONNECT = 0
    FACTORY_TUNABLES = {'when_to_teleport': TunableVariant(description='\n            When this object should teleport around.\n            ', locked_args={'on_client_connect': ON_CLIENT_CONNECT}, default='on_client_connect'), 'chance_to_teleport': TunablePercent(description='\n            A percent chance that this object will teleport when the\n            appropriate situation arises.\n            ', default=100), 'required_states': OptionalTunable(TunableList(description='\n            The states this object is required to be in in order to teleport.\n            ', tunable=TunableStateValueReference())), 'objects_to_teleport_near': TunableList(description='\n            A tunable list of static commodities, weights and behavior.  When\n            choosing where to teleport, objects with higher weights have a\n            greater chance of being chosen.\n            \n            If we fail to find a valid location near an object advertising the\n            chosen static commodity, we will search try again with a new object\n            until the list has been exhausted.\n            ', tunable=TunableTuple(description='\n                A static commodity and weight.\n                ', weight=TunableRange(description='\n                    A weight, between 0 and 1, that determines how likely this\n                    static commodity is to be chosen over the others listed.\n                    ', tunable_type=float, minimum=0, maximum=1, default=1), static_commodity=TunableReference(description='\n                    Reference to a type of static commodity.\n                    ', manager=services.static_commodity_manager()), state_change=OptionalTunable(TunableStateValueReference(description='\n                    A state value to apply to the object advertising this\n                    commodity if the teleport succeeds.\n                    '))))}

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        zone = services.current_zone()
        if self.when_to_teleport == self.ON_CLIENT_CONNECT and not zone.is_zone_running:
            zone.register_callback(zone_types.ZoneState.CLIENT_CONNECTED, self.teleport)

    def teleport(self):
        if random.random() > self.chance_to_teleport:
            return
        if self.required_states is not None:
            for state in self.required_states:
                if not self.owner.state_value_active(state):
                    return
        weights_and_commodities = [(obj_dict.weight, obj_dict.static_commodity, obj_dict.state_change) for obj_dict in self.objects_to_teleport_near]
        while weights_and_commodities:
            index = sims4.random._weighted(weights_and_commodities)
            (_, static_commodity, state_change) = weights_and_commodities.pop(index)
            motives = set()
            motives.add(static_commodity)
            all_objects = list(services.object_manager().valid_objects())
            random.shuffle(all_objects)
            for obj in all_objects:
                if obj is self.owner:
                    continue
                if obj.commodity_flags & motives:
                    starting_location = placement.create_starting_location(position=obj.position)
                    if self.owner.is_sim:
                        fgl_context = placement.create_fgl_context_for_sim(starting_location, self.owner)
                    else:
                        fgl_context = placement.create_fgl_context_for_object(starting_location, self.owner)
                    (position, orientation) = placement.find_good_location(fgl_context)
                    if position is not None and orientation is not None:
                        self.owner.transform = sims4.math.Transform(position, orientation)
                        if state_change is not None:
                            obj.set_state(state_change.state, state_change)
                        break
Beispiel #29
0
class RestaurantTuning:
    MENU_PRESETS = TunableMapping(
        description=
        '\n        The map to tune preset of menus that player to select to use in\n        restaurant customization.\n        ',
        key_type=TunableEnumEntry(tunable_type=MenuPresets,
                                  default=MenuPresets.CUSTOMIZE,
                                  binary_type=EnumBinaryExportType.EnumUint32),
        value_type=TunableTuple(
            description='\n            Menu preset contents.\n            ',
            preset_name=TunableLocalizedString(
                description=
                '\n                Menu preset name that appear in both menu customize UI and in\n                game menu UI.\n                '
            ),
            recipe_map=TunableMapping(
                description=
                "\n                The map that represent a menu preset. It's organized with courses\n                like drink, appetizer, entree etc, and in each course there are\n                options of recipes.\n                ",
                key_type=TunableEnumWithFilter(
                    tunable_type=Tag,
                    filter_prefixes=['recipe_course'],
                    default=Tag.INVALID,
                    invalid_enums=(Tag.INVALID, ),
                    pack_safe=True,
                    binary_type=EnumBinaryExportType.EnumUint32),
                value_type=TunableSet(
                    tunable=TunableReference(manager=services.recipe_manager(),
                                             class_restrictions=('Recipe', ),
                                             pack_safe=True)),
                key_name='course_tags',
                value_name='recipes',
                tuple_name='MenuCourseMappingTuple'),
            show_in_restaurant_menu=Tunable(
                description=
                "\n                If this is enabled, this menu preset will show up on restaurant\n                menus. If not, it won't. Currently, only home-chef menus\n                shouldn't show up on restaurant menus.\n                ",
                tunable_type=bool,
                default=True),
            export_class_name='MenuPresetContentTuple'),
        key_name='preset_enum',
        value_name='preset_contents',
        tuple_name='MenuPresetMappingTuple',
        export_modes=ExportModes.All)
    MENU_TAG_DISPLAY_CONTENTS = TunableMapping(
        description=
        '\n        The map to tune menu tags to display contents.\n        ',
        key_type=TunableEnumWithFilter(
            tunable_type=Tag,
            filter_prefixes=['recipe'],
            default=Tag.INVALID,
            invalid_enums=(Tag.INVALID, ),
            pack_safe=True,
            binary_type=EnumBinaryExportType.EnumUint32),
        value_type=TunableTuple(
            description=
            '\n            menu tag display contents.\n            ',
            menu_tag_name=TunableLocalizedString(),
            menu_tag_icon=TunableResourceKey(
                description=
                '\n                This will display as the filter icon in the course recipe picker UI.\n                ',
                resource_types=sims4.resources.CompoundTypes.IMAGE),
            export_class_name='MenuTagDisplayTuple'),
        key_name='menu_tags',
        value_name='menu_tag_display_contents',
        tuple_name='MenuTagDisplayMappingTuple',
        export_modes=ExportModes.ClientBinary)
    COURSE_SORTING_SEQUENCE = TunableSet(
        description=
        '\n        This set determines the sorting sequence for courses in both menu\n        customize UI and in game menu UI.\n        ',
        tunable=TunableEnumWithFilter(
            tunable_type=Tag,
            filter_prefixes=['recipe_course'],
            default=Tag.INVALID,
            invalid_enums=(Tag.INVALID, ),
            pack_safe=True,
            binary_type=EnumBinaryExportType.EnumUint32),
        export_modes=ExportModes.ClientBinary)
    DAILY_SPECIAL_DISCOUNT = TunablePercent(
        description=
        '\n        The percentage of the base price when an item is the daily special.\n        For example, if the base price is $10 and this is tuned to 80%, the\n        discounted price will be $10 x 80% = $8\n        ',
        default=80)
    INVALID_DAILY_SPECIAL_RECIPES = TunableList(
        description=
        '\n        A list of recipes that should not be considered for daily specials.\n        i.e. Glass of water.\n        ',
        tunable=TunableReference(
            description=
            '\n            The recipe to disallow from being a daily special.\n            ',
            manager=services.recipe_manager(),
            class_restrictions=('Recipe', ),
            pack_safe=True))
    COURSE_TO_FILTER_TAGS_MAPPING = TunableMapping(
        description=
        '\n        Mapping from course to filter tags for food picker UI.\n        ',
        key_type=TunableEnumWithFilter(
            description=
            '\n            The course associated with the list of filters.\n            ',
            tunable_type=Tag,
            filter_prefixes=['recipe_course'],
            default=Tag.INVALID,
            invalid_enums=(Tag.INVALID, ),
            pack_safe=True,
            binary_type=EnumBinaryExportType.EnumUint32),
        value_type=TunableList(
            description=
            '\n            This list of filter tags for the food picker UI for the course\n            specified.\n            ',
            tunable=TunableEnumWithFilter(
                tunable_type=Tag,
                filter_prefixes=['recipe_category'],
                default=Tag.INVALID,
                invalid_enums=(Tag.INVALID, ),
                pack_safe=True,
                binary_type=EnumBinaryExportType.EnumUint32)),
        key_name='course_key',
        value_name='course_filter_tags',
        tuple_name='CourseToFilterTuple',
        export_modes=ExportModes.ClientBinary)
    CUSTOMER_QUALITY_STAT = TunablePackSafeReference(
        description=
        '\n        The Customer Quality stat applied to food/drink the restaurant customer\n        eats/drinks. This is how we apply buffs to the Sim at the time they\n        consume the food/drink.\n        \n        The Customer Quality value is determined by multiplying the Final\n        Quality To Customer Quality Multiplier (found in Final Quality State\n        Data Mapping) by the Food Difficulty To Customer Quality Multiplier\n        (found in the Ingredient Quality State Data Mapping).\n        ',
        manager=services.get_instance_manager(sims4.resources.Types.STATISTIC))
    CUSTOMER_VALUE_STAT = TunablePackSafeReference(
        description=
        '\n        The Customer Value stat applied to food/drink the restaurant customer\n        eats/drinks. This is how we apply buffs to the Sim at the time they\n        consume the food/drink.\n        \n        The Customer Value value is determined by multiplying the Final Quality\n        To Customer Value Multiplier (found in Final Quality State Data Mapping)\n        by the Markup To Customer Value Multiplier (found in the Markup Data\n        Mapping).\n        ',
        manager=services.get_instance_manager(sims4.resources.Types.STATISTIC))
    RECIPE_DIFFICULTY_DATA_MAPPING = TunableMapping(
        description=
        '\n        A mapping of the recipe difficulty for restaurants to the appropriate\n        data.\n        ',
        key_name='recipe_difficulty',
        key_type=TunableEnumEntry(
            description=
            "\n            The recipe difficulty for chef's at a restaurant.\n            ",
            tunable_type=RecipeDifficulty,
            default=RecipeDifficulty.NORMAL),
        value_name='recipe_difficulty_data',
        value_type=TunableTuple(
            description=
            '\n            The tuning associated with the provided recipe difficulty.\n            ',
            recipe_difficulty_to_final_quality_adder=Tunable(
                description=
                '\n                This value is added to the Ingredient Quality To Final Quality Adder\n                and the Cooking Speed To Final Quality Adder to determine the player-\n                facing recipe quality.\n                ',
                tunable_type=float,
                default=0),
            recipe_difficulty_to_customer_quality_multiplier=Tunable(
                description=
                "\n                This value is multiplied by the Final Quality To Customer\n                Quality Multiplier to determine the customer's perceived quality\n                of the recipe.\n                ",
                tunable_type=float,
                default=1)))
    DEFAULT_INGREDIENT_QUALITY = TunableEnumEntry(
        description=
        '\n        The default ingredient quality for a restaurant.\n        ',
        tunable_type=RestaurantIngredientQualityType,
        default=RestaurantIngredientQualityType.INVALID,
        invalid_enums=(RestaurantIngredientQualityType.INVALID, ))
    INGREDIENT_QUALITY_DATA_MAPPING = TunableMapping(
        description=
        '\n        The mapping between ingredient enum and the ingredient data for\n        that type.\n        ',
        key_type=TunableEnumEntry(
            description=
            '\n            The ingredient type. Organic, normal, lousy, etc...\n            ',
            tunable_type=RestaurantIngredientQualityType,
            default=RestaurantIngredientQualityType.INVALID,
            invalid_enums=(RestaurantIngredientQualityType.INVALID, ),
            binary_type=EnumBinaryExportType.EnumUint32),
        value_type=TunableTuple(
            description=
            '\n            Data associated with this type of ingredient.\n            ',
            ingredient_quality_type_name=TunableLocalizedString(
                description=
                '\n                The localized name of this ingredient used in various places in\n                the UI.\n                '
            ),
            ingredient_quality_to_final_quality_adder=Tunable(
                description=
                '\n                This value is added to the Recipe Difficulty To Final Quality\n                Adder and the Cooking Speed To Final Quality Adder to determine\n                the player-facing recipe quality.\n                ',
                tunable_type=float,
                default=0),
            ingredient_quality_to_restaurant_expense_multiplier=TunableRange(
                description=
                '\n                This value is multiplied by the Base Restaurant Price (found in\n                the Recipe tuning) for each recipe served to determine what the\n                cost is to the restaurant for preparing that recipe.\n                ',
                tunable_type=float,
                default=0.5,
                minimum=0),
            export_class_name='IngredientDataTuple'),
        key_name='ingredient_enum',
        value_name='ingredient_data',
        tuple_name='IngredientEnumDataMappingTuple',
        export_modes=ExportModes.All)
    COOKING_SPEED_DATA_MAPPING = TunableMapping(
        description=
        '\n        A mapping from chef cooking speed to the data associated with that\n        cooking speed.\n        ',
        key_name='cooking_speed_buff',
        key_type=TunableReference(
            description=
            '\n            The cooking speed buff that is applied to the chef.\n            ',
            manager=services.get_instance_manager(sims4.resources.Types.BUFF),
            pack_safe=True),
        value_name='cooking_speed_data',
        value_type=TunableTuple(
            description=
            '\n            The data associated with the tuned cooking speed.\n            ',
            cooking_speed_to_final_quality_adder=Tunable(
                description=
                '\n                This value is added to the Recipe Difficulty To Final Quality\n                Adder and the Ingredient Quality To Final Quality Adder to\n                determine the player-facing recipe quality.\n                ',
                tunable_type=float,
                default=0),
            active_cooking_states_delta=Tunable(
                description=
                '\n                The amount by which to adjust the number of active cooking\n                states the chef must complete before completing the order. For\n                instance, if a -1 is tuned here, the chef will have to complete\n                one less state than normal. Regardless of how the buffs are\n                tuned, the chef will always run at least one state before\n                completing the order.\n                ',
                tunable_type=int,
                default=-1)))
    CHEF_SKILL_TO_FOOD_FINAL_QUALITY_ADDER_DATA = TunableTuple(
        description=
        '\n        Pairs a skill with a curve to determine the additional value to add to\n        the final quality of a food made at an owned restaurant.\n        ',
        skill=TunablePackSafeReference(
            description=
            '\n            The skill used to determine the adder for the final quality of food.\n            ',
            manager=services.get_instance_manager(
                sims4.resources.Types.STATISTIC),
            class_restrictions=('Skill', )),
        final_quality_adder_curve=TunableCurve(
            description=
            "\n            Maps the chef's current level of the tuned skill to a value that\n            will be added to the final quality statistic for food recipes cooked\n            at an owned restaurant.\n            ",
            x_axis_name='Skill Level',
            y_axis_name='Food Final Quality Adder'))
    CHEF_SKILL_TO_DRINK_FINAL_QUALITY_ADDER_DATA = TunableTuple(
        description=
        '\n        Pairs a skill with a curve to determine the additional value to add to\n        the final quality of a drink made at an owned restaurant.\n        ',
        skill=TunablePackSafeReference(
            description=
            '\n            The skill used to determine the adder for the final quality of drinks.\n            ',
            manager=services.get_instance_manager(
                sims4.resources.Types.STATISTIC),
            class_restrictions=('Skill', )),
        final_quality_adder_curve=TunableCurve(
            description=
            "\n            Maps the chef's current level of the tuned skill to a value that\n            will be added to the final quality statistic for drink recipes\n            cooked at an owned restaurant.\n            ",
            x_axis_name='Skill Level',
            y_axis_name='Food Final Quality Adder'))
    FINAL_QUALITY_STATE_DATA_MAPPING = TunableMapping(
        description=
        '\n        A mapping of final quality recipe states (Poor, Normal, Outstanding) to\n        the data associated with that recipe quality.\n        ',
        key_name='recipe_quality_state',
        key_type=TunableReference(
            description=
            '\n            The recipe quality state value.\n            ',
            manager=services.get_instance_manager(
                sims4.resources.Types.OBJECT_STATE),
            class_restrictions='ObjectStateValue',
            pack_safe=True),
        value_name='recipe_quality_state_value_data',
        value_type=TunableTuple(
            description=
            '\n            The data associated with the tuned recipe quality state value.\n            ',
            final_quality_to_customer_quality_multiplier=Tunable(
                description=
                '\n                This value is multiplied by the Recipe Difficulty To Customer\n                Quality Multiplier to determine the Customer Quality State value\n                of the recipe.\n                ',
                tunable_type=float,
                default=1),
            final_quality_to_customer_value_multiplier=Tunable(
                description=
                '\n                This value is multiplied by the Markup To Customer Value\n                Multiplier to determine the value of the Customer Value Stat\n                value of the recipe.\n                ',
                tunable_type=float,
                default=1)))
    PRICE_MARKUP_DATA_MAPPING = TunableMapping(
        description=
        '\n        A mapping of the current price markup of the restaurant to the data\n        associated with that markup.\n        ',
        key_name='markup_multiplier',
        key_type=Tunable(
            description=
            '\n            The markup multiplier. this needs to be in line with the available\n            markups tuned on the restaurant business.\n            ',
            tunable_type=float,
            default=1.5),
        value_name='markup_multiplier_data',
        value_type=TunableTuple(
            description=
            '\n            The data associated with the tuned markup multiplier.\n            ',
            markup_to_customer_value_multiplier=Tunable(
                description='\n                ',
                tunable_type=float,
                default=1)))
    BUSINESS_FUNDS_CATEGORY_FOR_COST_OF_INGREDIENTS = TunableEnumEntry(
        description=
        '\n        When a Chef cooks an order, the restaurant has to pay for the\n        ingredients. This is the category for those expenses.\n        ',
        tunable_type=BusinessFundsCategory,
        default=BusinessFundsCategory.NONE,
        invalid_enums=(BusinessFundsCategory.NONE, ))
    ATTIRE = TunableList(
        description=
        '\n        List of attires player can select to apply to the restaurant.\n        ',
        tunable=TunableEnumEntry(tunable_type=OutfitCategory,
                                 default=OutfitCategory.EVERYDAY,
                                 binary_type=EnumBinaryExportType.EnumUint32),
        export_modes=ExportModes.All)
    UNIFORM_CHEF_MALE = TunablePackSafeResourceKey(
        description=
        '\n        The SimInfo file to use to edit male chef uniforms.\n        ',
        default=None,
        resource_types=(sims4.resources.Types.SIMINFO, ),
        export_modes=ExportModes.All)
    UNIFORM_CHEF_FEMALE = TunablePackSafeResourceKey(
        description=
        '\n        The SimInfo file to use to edit female chef uniforms.\n        ',
        default=None,
        resource_types=(sims4.resources.Types.SIMINFO, ),
        export_modes=ExportModes.All)
    UNIFORM_WAITSTAFF_MALE = TunablePackSafeResourceKey(
        description=
        '\n        The SimInfo file to use to edit waiter uniforms.\n        ',
        default=None,
        resource_types=(sims4.resources.Types.SIMINFO, ),
        export_modes=ExportModes.All)
    UNIFORM_WAITSTAFF_FEMALE = TunablePackSafeResourceKey(
        description=
        '\n        The SimInfo file to use to edit waitress uniforms.\n        ',
        default=None,
        resource_types=(sims4.resources.Types.SIMINFO, ),
        export_modes=ExportModes.All)
    UNIFORM_HOST_MALE = TunablePackSafeResourceKey(
        description=
        '\n        The SimInfo file to use to edit male host uniforms.\n        ',
        default=None,
        resource_types=(sims4.resources.Types.SIMINFO, ),
        export_modes=ExportModes.All)
    UNIFORM_HOST_FEMALE = TunablePackSafeResourceKey(
        description=
        '\n        The SimInfo file to use to edit female host uniforms.\n        ',
        default=None,
        resource_types=(sims4.resources.Types.SIMINFO, ),
        export_modes=ExportModes.All)
    RESTAURANT_VENUE = TunablePackSafeReference(
        description=
        '\n        This is a tunable reference to the type of Venue that will describe\n        a Restaurant. To be used for code references to restaurant venue types\n        in code.\n        ',
        manager=services.get_instance_manager(sims4.resources.Types.VENUE))
    HOST_SITUATION = TunablePackSafeReference(
        description=
        '\n        The situation that Sims working as a Host will have.\n        ',
        manager=services.get_instance_manager(sims4.resources.Types.SITUATION))
    WAITSTAFF_SITUATION = TunablePackSafeReference(
        description=
        '\n        The situation that Sims working as a Waiter will have.\n        ',
        manager=services.get_instance_manager(sims4.resources.Types.SITUATION))
    CHEF_SITUATION = TunablePackSafeReference(
        description=
        '\n        The situation that Sims working as a Chef will have.\n        ',
        manager=services.get_instance_manager(sims4.resources.Types.SITUATION))
    HOME_CHEF_SITUATION_TAG = TunableEnumWithFilter(
        description=
        '\n        Tag that we use on all the home chef situations.\n        ',
        tunable_type=Tag,
        filter_prefixes=['situation'],
        default=Tag.INVALID,
        invalid_enums=(Tag.INVALID, ),
        pack_safe=True)
    DINING_SITUATION_TAG = TunableEnumWithFilter(
        description=
        "\n        The tag used to find dining situations. \n        \n        This shouldn't need to be re-tuned after being set initially. If you\n        need to re-tune this you should probably talk to a GPE first.\n        ",
        tunable_type=Tag,
        filter_prefixes=['situation'],
        default=Tag.INVALID,
        pack_safe=True)
    TABLE_FOOD_SLOT_TYPE = TunableReference(
        description=
        '\n        The slot type of the food slot on the dining table.\n        ',
        manager=services.get_instance_manager(sims4.resources.Types.SLOT_TYPE))
    TABLE_DRINK_SLOT_TYPE = TunableReference(
        description=
        '\n        The slot type of the drink slot on the dining table.\n        ',
        manager=services.get_instance_manager(sims4.resources.Types.SLOT_TYPE))
    FOOD_AUTONOMY_PREFERENCE = TunableAutonomyPreference(
        description=
        '\n        The Autonomy Preference for the delivered food items.\n        ',
        is_scoring=False)
    DRINK_AUTONOMY_PREFERENCE = TunableAutonomyPreference(
        description=
        '\n        The Autonomy Preference for the delivered drink items.\n        ',
        is_scoring=False)
    CONSUMABLE_FULL_STATE_VALUE = TunableReference(
        description=
        '\n        The Consumable_Full state value. Food in restaurants will be set to\n        this value instead of defaulting to Consumable_Untouched to avoid other\n        Sims from eating your own food.\n        ',
        manager=services.get_instance_manager(
            sims4.resources.Types.OBJECT_STATE),
        class_restrictions=('ObjectStateValue', ))
    CONSUMABLE_EMPTY_STATE_VALUE = TunableReference(
        description=
        "\n        The Consumable_Empty state value. This is the state we'll use to\n        determine if food/drink is empty or not.\n        ",
        manager=services.get_instance_manager(
            sims4.resources.Types.OBJECT_STATE),
        class_restrictions=('ObjectStateValue', ))
    FOOD_DELIVERED_TO_TABLE_NOTIFICATION = TunableUiDialogNotificationSnippet(
        description=
        "\n        The notification shown when the food is delivered to the player's table.\n        "
    )
    FOOD_STILL_ON_TABLE_NOTIFICATION = TunableUiDialogNotificationSnippet(
        description=
        "\n        The notification that the player will see if the waitstaff try and\n        deliver food but there's still food on the table.\n        "
    )
    STAND_UP_INTERACTION = TunableReference(
        description=
        '\n        A reference to sim-stand so that sim-stand can be pushed on every sim\n        that is sitting at a table that is abandoned.\n        ',
        manager=services.get_instance_manager(
            sims4.resources.Types.INTERACTION))
    DEFAULT_MENU = TunableEnumEntry(
        description=
        '\n        The default menu setting for a brand new restaurant.\n        ',
        tunable_type=MenuPresets,
        default=MenuPresets.CUSTOMIZE,
        export_modes=ExportModes.All,
        binary_type=EnumBinaryExportType.EnumUint32)
    SWITCH_SEAT_INTERACTION = TunableReference(
        description=
        '\n        This is a reference to the interaction that gets pushed on whichever Sim\n        is sitting in the seat that the Actor is switching to. The interaction \n        will be pushed onto the sseated Sim and will target the Actor Sims \n        current seat before the switch.\n        ',
        manager=services.get_instance_manager(
            sims4.resources.Types.INTERACTION))
    RECOMMENDED_ORDER_INTERACTION = TunableReference(
        description=
        '\n        This is a reference to the interaction that will get pushed on the active Sim\n        to recommend orders to the Sim AFTER the having gone through the Menu UI.\n        \n        It will continue to retain the previous target.\n        ',
        manager=services.get_instance_manager(
            sims4.resources.Types.INTERACTION),
        pack_safe=True)
    INGREDIENT_PRICE_PERK_MAP = TunableMapping(
        description=
        '\n        Maps the various ingredient price perks with their corresponding\n        discount.\n        ',
        key_name='Ingredient Price Perk',
        key_type=TunableReference(
            description=
            '\n            A perk that gives a tunable multiplier to the price of ingredients\n            for restaurants.\n            ',
            manager=services.get_instance_manager(
                sims4.resources.Types.BUCKS_PERK),
            pack_safe=True),
        value_name='Ingredient Price Multiplier',
        value_type=TunableRange(
            description=
            '\n            If the household has the corresponding perk, this value will be\n            multiplied by the final cost of each recipe to the restaurant.\n            ',
            tunable_type=float,
            default=1,
            minimum=0))
    CUSTOMERS_ORDER_EXPENSIVE_FOOD_PERK_DATA = TunableTuple(
        description=
        '\n        The perk that makes customers order more expensive food, and the off-lot\n        multiplier for that perk.\n        ',
        perk=TunablePackSafeReference(
            description=
            '\n            If the owning household has this perk, customers will pick two dishes to\n            order and then pick the most expensive of the two.\n            ',
            manager=services.get_instance_manager(
                sims4.resources.Types.BUCKS_PERK)),
        off_lot_multiplier=TunableRange(
            description=
            '\n            When calculating off-lot profits, this is applied if the household\n            has this perk.\n            ',
            tunable_type=float,
            default=1.1,
            minimum=1))
    UNOWNED_RESTAURANT_PRICE_MULTIPLIER = TunableRange(
        description=
        '\n        The amount each item in the menu will be multiplied by on unowned\n        restaurant lots.\n        ',
        tunable_type=float,
        default=1.2,
        minimum=0,
        export_modes=ExportModes.All)
    CHEF_NOT_SKILLED_ENOUGH_THRESHOLD = Tunable(
        description=
        '\n        This is the value that a chef must reach when preparing a meal for a\n        customer without displaying the "Chef isn\'t skilled enough to make \n        receiver X" \n        \n        The number that must reach this value is the skill adder\n        of the chef and recipe difficulty adder.\n        ',
        tunable_type=int,
        default=-30)
    CHEF_NOT_SKILLED_ENOUGH_NOTIFICATION = TunableUiDialogNotificationSnippet(
        description=
        '\n        The notification shown when the chef is working on a recipe that is \n        too difficult for their skill.\n        '
    )
    DEFAULT_PROFIT_PER_MEAL_FOR_OFF_LOT_SIMULATION = TunableRange(
        description=
        '\n        This is used as the default profit for a meal for off-lot simulation. Once\n        enough actual meals have been sold, this value becomes irrelevant and\n        the MEAL_COUNT_FOR_OFF_LOT_PROFIT_PER_MEAL tunable comes into use.\n        ',
        tunable_type=int,
        default=20,
        minimum=1)
    MEAL_COUNT_FOR_OFF_LOT_PROFIT_PER_MEAL = TunableRange(
        description=
        '\n        The number of meals to keep a running average of for the profit per meal\n        calculations during off lot simulations.\n        ',
        tunable_type=int,
        default=10,
        minimum=2)
    ADVERTISING_DATA_MAP = TunableMapping(
        description=
        '\n        The mapping between advertising type and the data for that type.\n        ',
        key_name='Advertising_Type',
        key_type=TunableEnumEntry(
            description='\n            The Advertising Type .\n            ',
            tunable_type=BusinessAdvertisingType,
            default=BusinessAdvertisingType.INVALID,
            invalid_enums=(BusinessAdvertisingType.INVALID, ),
            binary_type=EnumBinaryExportType.EnumUint32),
        value_name='Advertising_Data',
        value_type=TunableTuple(
            description=
            '\n            Data associated with this advertising type.\n            ',
            cost_per_hour=TunableRange(
                description=
                '\n                How much, per hour, it costs to use this advertising type.\n                ',
                tunable_type=int,
                default=10,
                minimum=0),
            customer_count_multiplier=TunableRange(
                description=
                '\n                This amount is multiplied by the ideal customer count for owned\n                restaurants.\n                ',
                tunable_type=float,
                default=0.8,
                minimum=0),
            ui_sort_order=TunableRange(
                description=
                '\n                Value representing how map entries will be sorted in the UI.\n                1 represents the first entry.  Avoid duplicate values\n                within the map.\n                ',
                tunable_type=int,
                minimum=1,
                default=1),
            export_class_name='RestaurantAdvertisingData'),
        tuple_name='RestaurantAdvertisingDataMapping',
        export_modes=ExportModes.All)
    TODDLER_SENT_TO_DAYCARE_FOR_RESTAURANTS = TunableUiDialogNotificationSnippet(
        description=
        '\n        The notification shown when a toddler is sent to daycare upon traveling\n        to a restaurant venue.\n        '
    )
    TIME_OF_DAY_TO_CUSTOMER_COUNT_MULTIPLIER_CURVE = TunableCurve(
        description=
        '\n        A curve that lets you tune a specific customer count multiplier\n        based on the time of day. Time of day should range between 0 and 23,\n        0 being midnight.\n        ',
        x_axis_name='time_of_day',
        y_axis_name='customer_count_multiplier')
class ModifyAllLotItems(HasTunableFactory, AutoFactoryInit):
    DESTROY_OBJECT = 0
    SET_STATE = 1
    INVENTORY_TRANSFER = 2
    DELIVER_BILLS = 3
    SET_ON_FIRE = 4
    CLEANUP_VEHICLE = 5
    LOOT = 6
    FACTORY_TUNABLES = {
        'description':
        '\n        Tune modifications to apply to all objects on a lot.\n        Can do state changes, destroy certain items, etc.\n        \n        EX: for auto cleaning, tune to have objects with Dirtiness state that\n        equals dirty to be set to the clean state and tune to have dirty dishes\n        and spoiled food to be deleted\n        ',
        'modifications':
        TunableList(
            description=
            "\n            A list of where the elements define how to modify objects on the\n            lot. Each entry is a triplet of an object modification action\n            (currently either destroy the object or set its state), a list of\n            tests to run on the object to determine if we should actually apply\n            the modification, and a priority in case some modifications should\n            take precedence over other ones when both of their tests pass.\n            \n            EX: test list: object's dirtiness state != dirtiness clean\n            action: set state to Dirtiness_clean\n            \n            So dirty objects will become clean\n            ",
            tunable=TunableTuple(
                action=TunableVariant(
                    set_state=TunableTuple(
                        action_value=TunableStateValueReference(
                            description='An object state to set the object to',
                            pack_safe=True),
                        locked_args={'action_type': SET_STATE}),
                    destroy_object=TunableTuple(
                        locked_args={'action_type': DESTROY_OBJECT}),
                    inventory_transfer=TunableTuple(
                        action_value=InventoryTransferFakePerform.
                        TunableFactory(),
                        locked_args={'action_type': INVENTORY_TRANSFER}),
                    deliver_bills=TunableTuple(
                        action_value=DeliverBillFakePerform.TunableFactory(),
                        locked_args={'action_type': DELIVER_BILLS}),
                    set_on_fire=TunableTuple(
                        locked_args={'action_type': SET_ON_FIRE}),
                    cleanup_vehicle=TunableTuple(
                        description=
                        '\n                        Cleanup vehicles that are left around.\n                        ',
                        locked_args={'action_type': CLEANUP_VEHICLE}),
                    loot=TunableTuple(
                        description=
                        '\n                        Apply loots to the object.\n                        ',
                        loot_actions=TunableSet(
                            description=
                            '\n                            Loot(s) to apply.\n                            ',
                            tunable=TunableReference(
                                manager=services
                                .get_instance_manager(
                                    Types.ACTION),
                                pack_safe
                                =True)),
                        locked_args={'action_type': LOOT})),
                chance=TunablePercent(
                    description=
                    '\n                    Chance this modification will occur.\n                    ',
                    default=100,
                    minimum=1),
                global_tests=TunableObjectModifyGlobalTestList(
                    description=
                    "\n                    Non object-related tests that gate this modification from occurring.  Use this for any global\n                    tests that don't require the object, such as zone/location/time-elapsed tests.  These tests\n                    will run only ONCE for this action, unlike 'Tests', which runs PER OBJECT. \n                    "
                ),
                tests=TunableObjectModifyTestSet(
                    description=
                    '\n                    All least one subtest group (AKA one list item) must pass\n                    within this list before the action associated with this\n                    tuning will be run.\n                    ',
                    additional_tests={
                        'elapsed_time':
                        TimeElapsedZoneTest.TunableFactory(
                            locked_args={'tooltip': None}),
                        'statistic':
                        StatThresholdTest.TunableFactory(
                            locked_args={'tooltip': None})
                    }),
                weighted_tests=TunableList(
                    description=
                    '\n                    Weighted tests for the individual object. One is chosen \n                    based on weight, and all objects are run against that chosen\n                    test set.\n                    ',
                    tunable=TunableTuple(
                        tests=TunableObjectModifyTestSet(
                            description=
                            '\n                            All least one subtest group (AKA one list item) must pass\n                            within this list before the action associated with this\n                            tuning will be run.\n                            ',
                            additional_tests={
                                'elapsed_time':
                                TimeElapsedZoneTest.TunableFactory(
                                    locked_args={'tooltip': None}),
                                'statistic':
                                StatThresholdTest.TunableFactory(
                                    locked_args={'tooltip': None})
                            }),
                        weight=TunableRange(
                            description=
                            '\n                            Weight to use.\n                            ',
                            tunable_type=int,
                            default=1,
                            minimum=1)))))
    }

    def modify_objects(self, object_criteria=None):
        objects_to_destroy = []
        num_modified = 0
        modifications = defaultdict(CompoundTestList)
        for mod in self.modifications:
            if not random_chance(mod.chance * 100):
                continue
            if mod.global_tests and not mod.global_tests.run_tests(
                    GlobalResolver()):
                continue
            if mod.tests:
                modifications[mod.action].extend(mod.tests)
            if mod.weighted_tests:
                weighted_tests = []
                for test_weight_pair in mod.weighted_tests:
                    weighted_tests.append(
                        (test_weight_pair.weight, test_weight_pair.tests))
                modifications[mod.action].extend(
                    weighted_random_item(weighted_tests))
        if not modifications:
            return num_modified
        all_objects = list(services.object_manager().values())
        for obj in all_objects:
            if obj.is_sim:
                continue
            if object_criteria is not None and not object_criteria(obj):
                continue
            resolver = SingleObjectResolver(obj)
            modified = False
            for (action, tests) in modifications.items():
                if not tests.run_tests(resolver):
                    continue
                modified = True
                action_type = action.action_type
                if action_type == ModifyAllLotItems.DESTROY_OBJECT:
                    objects_to_destroy.append(obj)
                    break
                elif action_type == ModifyAllLotItems.SET_STATE:
                    new_state_value = action.action_value
                    if obj.state_component and obj.has_state(
                            new_state_value.state):
                        obj.set_state(new_state_value.state,
                                      new_state_value,
                                      immediate=True)
                        if action_type in (
                                ModifyAllLotItems.INVENTORY_TRANSFER,
                                ModifyAllLotItems.DELIVER_BILLS):
                            element = action.action_value()
                            element._do_behavior()
                        elif action_type == ModifyAllLotItems.SET_ON_FIRE:
                            fire_service = services.get_fire_service()
                            fire_service.spawn_fire_at_object(obj)
                        elif action_type == ModifyAllLotItems.CLEANUP_VEHICLE:
                            if self._should_cleanup_vehicle(obj):
                                objects_to_destroy.append(obj)
                                if action_type == ModifyAllLotItems.LOOT:
                                    for loot_action in action.loot_actions:
                                        loot_action.apply_to_resolver(resolver)
                                    else:
                                        raise NotImplementedError
                                else:
                                    raise NotImplementedError
                        elif action_type == ModifyAllLotItems.LOOT:
                            for loot_action in action.loot_actions:
                                loot_action.apply_to_resolver(resolver)
                            else:
                                raise NotImplementedError
                        else:
                            raise NotImplementedError
                elif action_type in (ModifyAllLotItems.INVENTORY_TRANSFER,
                                     ModifyAllLotItems.DELIVER_BILLS):
                    element = action.action_value()
                    element._do_behavior()
                elif action_type == ModifyAllLotItems.SET_ON_FIRE:
                    fire_service = services.get_fire_service()
                    fire_service.spawn_fire_at_object(obj)
                elif action_type == ModifyAllLotItems.CLEANUP_VEHICLE:
                    if self._should_cleanup_vehicle(obj):
                        objects_to_destroy.append(obj)
                        if action_type == ModifyAllLotItems.LOOT:
                            for loot_action in action.loot_actions:
                                loot_action.apply_to_resolver(resolver)
                            else:
                                raise NotImplementedError
                        else:
                            raise NotImplementedError
                elif action_type == ModifyAllLotItems.LOOT:
                    for loot_action in action.loot_actions:
                        loot_action.apply_to_resolver(resolver)
                    else:
                        raise NotImplementedError
                else:
                    raise NotImplementedError
            if modified:
                num_modified += 1
        for obj in objects_to_destroy:
            obj.destroy(source=self,
                        cause='Destruction requested by modify lot tuning')
        objects_to_destroy = None
        return num_modified

    def _should_cleanup_vehicle(self, obj):
        vehicle_component = obj.get_component(VEHICLE_COMPONENT)
        if vehicle_component is None:
            return False
        household_owner_id = obj.get_household_owner_id()
        if household_owner_id is not None and household_owner_id != 0:
            return False
        elif obj.interaction_refs:
            return False
        return True