Exemple #1
0
class Mood(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.mood_manager()):
    __qualname__ = 'Mood'
    INSTANCE_TUNABLES = {'mood_asm_param': OptionalTunable(description='\n            If set, then this mood will specify an asm parameter to affect\n            animations. If not set, then the ASM parameter will be determined by\n            the second most prevalent mood.\n            ', tunable=Tunable(description="\n                The asm parameter for Sim's mood, if not set, will use 'xxx'\n                from instance name pattern with 'mood_xxx'.\n                ", tunable_type=str, default='', source_query=SourceQueries.SwingEnumNamePattern.format('mood')), enabled_name='Specify', disabled_name='Determined_By_Other_Moods'), 'intensity_thresholds': TunableList(int, description='\n                List of thresholds at which the intensity of this mood levels up.\n                If empty, this mood has a single threshold and all mood tuning lists should\n                have a single item in them.\n                For each threshold added, you may add a new item to the Buffs, Mood Names,\n                Portrait Pose Indexes and Portrait Frames lists.'), 'buffs': TunableList(TunableBuffReference(reload_dependent=True), description='\n                A list of buffs that will be added while this mood is the active mood\n                on a Sim. \n                The first item is applied for the initial intensity, and each\n                subsequent item replaces the previous buff as the intensity levels up.'), 'mood_names': TunableList(TunableLocalizedString(), description='\n                A list of localized names of this mood.\n                The first item is applied for the initial intensity, and each\n                subsequent item replaces the name as the intensity levels up.', export_modes=(ExportModes.ServerXML, ExportModes.ClientBinary)), 'portrait_pose_indexes': TunableList(Tunable(tunable_type=int, default=0), description='\n                A list of the indexes of the pose passed to thumbnail generation on the\n                client to pose the Sim portrait when they have this mood.\n                You can find the list of poses in tuning\n                (Client_ThumnailPoses)\n                The first item is applied for the initial intensity, and each\n                subsequent item replaces the pose as the intensity levels up.', export_modes=(ExportModes.ClientBinary,)), 'portrait_frames': TunableList(Tunable(tunable_type=str, default=''), description='\n                A list of the frame labels (NOT numbers!) from the UI .fla file that the\n                portrait should be set to when this mood is active. Determines\n                background color, font color, etc.\n                The first item is applied for the initial intensity, and each\n                subsequent item replaces the pose as the intensity levels up.', export_modes=(ExportModes.ClientBinary,)), 'environment_scoring_commodity': Commodity.TunableReference(description="\n                Defines the ranges and corresponding buffs to apply for this\n                mood's environmental contribution.\n                \n                Be sure to tune min, max, and the different states. The\n                convergence value is what will remove the buff. Suggested to be\n                0.\n                "), 'descriptions': TunableList(TunableLocalizedString(), description='\n                Description for the UI tooltip, per intensity.', export_modes=(ExportModes.ClientBinary,)), 'icons': TunableList(TunableResourceKey(None, resource_types=sims4.resources.CompoundTypes.IMAGE), description='\n                Icon for the UI tooltip, per intensity.', export_modes=(ExportModes.ClientBinary,)), 'descriptions_age_override': TunableMapping(description='\n                Mapping of age to descriptions text for mood.  If age does not\n                exist in mapping will use default description text.\n                ', key_type=TunableEnumEntry(sim_info_types.Age, sim_info_types.Age.CHILD), value_type=TunableList(description='\n                    Description for the UI tooltip, per intensity.\n                    ', tunable=TunableLocalizedString()), key_name='Age', value_name='description_text', export_modes=(ExportModes.ClientBinary,)), 'descriptions_trait_override': TunableMoodDescriptionTraitOverride(description='\n                Trait override for mood descriptions.  If a Sim has this trait\n                and there is not a valid age override for the Sim, this\n                description text will be used.\n                ', export_modes=(ExportModes.ClientBinary,)), 'audio_stings_on_add': TunableList(description="\n                The audio to play when a mood or it's intensity changes. Tune one for each intensity on the mood.\n                ", tunable=TunableResourceKey(description='\n                    The sound to play.\n                    ', default=None, resource_types=(sims4.resources.Types.PROPX,), export_modes=ExportModes.ClientBinary)), 'mood_colors': TunableList(description='\n                A list of the colors displayed on the steel series mouse when the active Sim has this mood.  The first item is applied for the initial intensity, and each  subsequent item replaces the color as the intensity levels up.\n                ', tunable=TunableVector3(description='\n                    Color.\n                    ', default=sims4.math.Vector3.ZERO(), export_modes=ExportModes.ClientBinary)), 'mood_frequencies': TunableList(description='\n                A list of the flash frequencies on the steel series mouse when the active Sim has this mood.   The first item is applied for the initial intensity, and each  subsequent item replaces the value as the intensity levels up.  0 => solid color, otherwise, value => value hertz.\n                  ', tunable=Tunable(tunable_type=float, default=0.0, description=',\n                    Hertz.\n                    ', export_modes=ExportModes.ClientBinary)), 'buff_polarity': TunableEnumEntry(description='\n                Setting the polarity will determine how up/down arrows\n                appear for any buff that provides this mood.\n                ', tunable_type=BuffPolarity, default=BuffPolarity.NEUTRAL, tuning_group=GroupNames.UI, needs_tuning=True, export_modes=ExportModes.All), 'is_changeable': Tunable(description='\n                If this is checked, any buff with this mood will change to\n                the highest current mood of the same polarity.  If there is no mood\n                with the same polarity it will default to use this mood type\n                ', tunable_type=bool, default=False, needs_tuning=True)}
    _asm_param_name = None
    excluding_traits = None

    @classmethod
    def _tuning_loaded_callback(cls):
        cls._asm_param_name = cls.mood_asm_param
        if not cls._asm_param_name:
            name_list = cls.__name__.split('_', 1)
            if len(name_list) <= 1:
                logger.error("Mood {} has an invalid name for asm parameter, please either set 'mood_asm_param' or change the tuning file name to match the format 'mood_xxx'.", cls.__name__)
            cls._asm_param_name = name_list[1]
        cls._asm_param_name = cls._asm_param_name.lower()
        for buff_ref in cls.buffs:
            my_buff = buff_ref.buff_type
            while my_buff is not None:
                if my_buff.mood_type is not None:
                    logger.error('Mood {} will apply a buff ({}) that affects mood. This can cause mood calculation errors. Please select a different buff or remove the mood change.', cls.__name__, my_buff.mood_type.__name__)
                my_buff.is_mood_buff = True
        prev_threshold = 0
        for threshold in cls.intensity_thresholds:
            if threshold <= prev_threshold:
                logger.error('Mood {} has Intensity Thresholds in non-ascending order.')
                break
            prev_threshold = threshold

    @classmethod
    def _verify_tuning_callback(cls):
        num_thresholds = len(cls.intensity_thresholds) + 1
        if len(cls.buffs) != num_thresholds:
            logger.error('Mood {} does not have the correct number of Buffs tuned. It has {} thresholds, but {} buffs.', cls.__name__, num_thresholds, len(cls.buffs))
        if len(cls.mood_names) != num_thresholds:
            logger.error('Mood {} does not have the correct number of Mood Names tuned. It has {} thresholds, but {} names.', cls.__name__, num_thresholds, len(cls.mood_names))
        if len(cls.portrait_pose_indexes) != num_thresholds:
            logger.error('Mood {} does not have the correct number of Portrait Pose Indexes tuned. It has {} thresholds, but {} poses.', cls.__name__, num_thresholds, len(cls.portrait_pose_indexes))
        if len(cls.portrait_frames) != num_thresholds:
            logger.error('Mood {} does not have the correct number of Portrait Frames tuned. It has {} thresholds, but {} frames.', cls.__name__, num_thresholds, len(cls.portrait_frames))
        for (age, descriptions) in cls.descriptions_age_override.items():
            while len(descriptions) != num_thresholds:
                logger.error('Mood {} does not have the correct number of descriptions age override tuned. For age:({}) It has {} thresholds, but {} descriptions.', cls.__name__, age, num_thresholds, len(descriptions))
        if cls.descriptions_trait_override.trait is not None and len(cls.descriptions_trait_override.descriptions) != num_thresholds:
            logger.error('Mood {} does not have the correct number of trait override descriptions tuned. For trait:({}) It has {} thresholds, but {} descriptions.', cls.__name__, cls.descriptions_trait_override.trait.__name__, num_thresholds, len(cls.descriptions_trait_override.descriptions))

    @classproperty
    def asm_param_name(cls):
        return cls._asm_param_name
Exemple #2
0
class MusicTrack(metaclass=HashedTunedInstanceMetaclass,
                 manager=services.get_instance_manager(
                     sims4.resources.Types.RECIPE)):
    INSTANCE_TUNABLES = {
        'music_clip':
        OptionalTunable(
            description=
            '\n            If enabled, the music clip for music interactions. If disabled,\n            make sure you have vocals tuned.\n            ',
            tunable=TunableResourceKey(
                description=
                '\n                The propx file of the music clip to play.\n                ',
                needs_tuning=False,
                resource_types=(sims4.resources.Types.PROPX, ))),
        'length':
        TunableRealSecond(
            description=
            "\n            The length of the clip in real seconds.  This should be a part of\n            the propx's file name.\n            ",
            needs_tuning=False,
            default=30,
            minimum=0),
        'buffer':
        TunableRealSecond(
            description=
            "\n            A buffer added to the track length.  This is used to prevent the\n            audio from stopping before it's finished.\n            ",
            needs_tuning=False,
            default=0),
        'check_for_unlock':
        Tunable(
            description=
            "\n            Whether or not to check the Sim's Unlock Component to determine if\n            they can play the song.  Currently, only clips that are meant to be\n            unlocked by the Write Song interaction should have this set to true.\n            ",
            needs_tuning=False,
            tunable_type=bool,
            default=False),
        'music_track_name':
        OptionalTunable(
            description=
            "\n            If the clip is of a song, this is its name. The name is shown in the\n            Pie Menu when picking specific songs to play.\n            \n            If the clip isn't a song, like clips used for the Practice or Write\n            Song interactions, this does not need to be tuned.\n            ",
            tunable=TunableLocalizedStringFactory(
                description=
                "\n                The track's name.\n                "),
            enabled_by_default=True),
        'tests':
        TunableTestSet(
            description=
            '\n            Tests to verify if this song is available for the Sim to play.\n            '
        ),
        'moods':
        TunableList(
            description=
            "\n            A list of moods that will be used to determine which song a Sim will\n            play autonomously.  If a Sim doesn't know any songs that their\n            current mood, they'll play anything.\n            ",
            tunable=TunableReference(manager=services.mood_manager()),
            needs_tuning=True),
        'vocals':
        TunableMapping(
            description=
            "\n            A mapping of participants and their potential vocal tracks. Each\n            participant that has a vocal track that tests successfully will\n            sing when the music starts.\n            \n            Note: The interaction's resolver will be passed into the vocal\n            track tests, so use the same participant in those tests.\n            ",
            key_name='participant',
            value_name='vocal_tracks',
            key_type=TunableEnumEntry(
                description=
                '\n                The participant who should sing vocals when the music starts.\n                ',
                tunable_type=ParticipantTypeSim,
                default=ParticipantTypeSim.Actor),
            value_type=TunableList(
                description=
                '\n                If this music track has vocals, add them here.  The first track that\n                passes its test will be used.  If no tracks pass their test, none\n                will be used.\n                ',
                tunable=VocalTrack.TunableReference()))
    }

    @classmethod
    def _verify_tuning_callback(cls):
        if cls.music_clip is None and not cls.vocals:
            logger.error('{} does not have music or vocals tuned.',
                         cls,
                         owner='rmccord')

    @classproperty
    def tuning_tags(cls):
        return EMPTY_SET
class AwayAction(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(sims4.resources.Types.AWAY_ACTION)):
    __qualname__ = 'AwayAction'
    INSTANCE_TUNABLES = {'_exit_conditions': TunableList(description='\n                A list of exit conditions for this away action. When exit\n                conditions are met then the away action ends and the default\n                away action is reapplied.\n                ', tunable=TunableTuple(conditions=TunableList(description='\n                        A list of conditions that all must be satisfied for the\n                        group to be considered satisfied.\n                        ', tunable=TunableAwayActionCondition(description='\n                            A condition for an away action.\n                            ')))), '_periodic_stat_changes': PeriodicStatisticChange.TunableFactory(description='\n                Periodic stat changes that this away action applies while it\n                is active.\n                '), 'icon': TunableResourceKey(description='\n                Icon that represents the away action in on the sim skewer.\n                ', default=None, resource_types=sims4.resources.CompoundTypes.IMAGE, tuning_group=GroupNames.UI), 'tooltip': TunableLocalizedStringFactory(description='\n                The tooltip shown on the icon that represents the away action.\n                ', tuning_group=GroupNames.UI), 'pie_menu_tooltip': TunableLocalizedStringFactory(description='\n                The tooltip shown in the pie menu for this away action.\n                ', tuning_group=GroupNames.UI), '_tests': TunableTestSet(description='\n                Tests that determine if this away action is applicable.  These\n                tests do not ensure that the conditions are still met\n                throughout the duration that the away action is applied.\n                '), '_display_name': TunableLocalizedStringFactory(description='\n                The name given to the away action when the user sees it in the\n                pie menu.\n                ', tuning_group=GroupNames.UI), '_display_name_text_tokens': LocalizationTokens.TunableFactory(description="\n                Localization tokens to be passed into 'display_name'.\n                For example, you could use a participant or you could also pass\n                in statistic and commodity values\n                ", tuning_group=GroupNames.UI), '_available_when_instanced': Tunable(description="\n                If this away action is able to be applied when the sim is still\n                instanced.  If the sim becomes instanced while the away action\n                is running we will not stop running it.\n                \n                This should only be true in special cases such as with careers.\n                \n                PLEASE ASK A GPE ABOUT MAKING THIS TRUE BEFORE DOING SO.  YOU\n                PROBABLY DON'T WANT THIS.\n                ", tunable_type=bool, default=False), '_preroll_commodities': TunableList(description='\n                A list of commodities that will be used to run preroll\n                if the sim loaded with this away action.\n                ', tunable=TunableReference(description='\n                    The commodity that is used to solve for preroll if the\n                    sim had this away action on them when they are being loaded.\n                    \n                    This is used to help preserve the fiction of what that sim was\n                    doing when the player returns to the lot.  EX: make the sim\n                    garden if they were using the gardening away action. \n                    ', manager=services.get_instance_manager(sims4.resources.Types.STATISTIC))), '_preroll_static_commodities': TunableList(description='\n                A list of static commodities that will be used to run preroll\n                if the sim loaded with this away action.\n                ', tunable=StaticCommodity.TunableReference(description='\n                    The static commodity that is used to solve for preroll if the\n                    sim had this away action on them when they are being loaded.\n                    \n                    This is used to help preserve the fiction of what that sim was\n                    doing when the player returns to the lot.  EX: make the sim\n                    garden if they were using the gardening away action. \n                    ')), '_apply_on_load_tags': TunableSet(description='\n                A set of tags that are are compared to interaction tags that\n                the sim was running when they became uninstantiated.  If there\n                are any matching tags then this away action will be applied\n                automatically to that sim rather than the default away action.\n                ', tunable=TunableEnumEntry(description='\n                    A single tag that will be compared to the interaction tags.\n                    ', tunable_type=tag.Tag, default=tag.Tag.INVALID)), '_disabled_when_running': OptionalTunable(description='\n                The availability of this away action when it is already the\n                active away action on the sim.\n                ', tunable=TunableLocalizedStringFactory(description='\n                    The text that displays in the tooltip string when this\n                    away action is not available because it is already the\n                    active away action.\n                    '), disabled_name='available_when_running', enabled_name='disabled_when_running'), 'mood_list': TunableList(description='\n                A list of possible moods this AwayAction may associate with.\n                ', tunable=TunableReference(description='\n                    A mood associated with this AwayAction.\n                    ', manager=services.mood_manager()))}

    def __init__(self, tracker, target=None):
        self._tracker = tracker
        self._target = target
        self._conditional_actions_manager = ConditionalActionManager()
        self._periodic_stat_changes_instance = self._periodic_stat_changes(self)
        self._state = AwayActionState.INITIALIZED

    @classmethod
    def should_run_on_load(cls, sim_info):
        for interaction_data in sim_info.si_state.interactions:
            interaction = services.get_instance_manager(sims4.resources.Types.INTERACTION).get(interaction_data.interaction)
            if interaction is None:
                pass
            while len(interaction.get_category_tags() & cls._apply_on_load_tags) > 0:
                return True
        return False

    @classmethod
    def get_commodity_preroll_list(cls):
        if cls._preroll_commodities:
            return cls._preroll_commodities

    @classmethod
    def get_static_commodity_preroll_list(cls):
        if cls._preroll_static_commodities:
            return cls._preroll_static_commodities

    @property
    def sim_info(self):
        return self._tracker.sim_info

    @property
    def sim(self):
        return self.sim_info

    @property
    def target(self):
        return self._target

    @classproperty
    def available_when_instanced(cls):
        return cls._available_when_instanced

    @property
    def is_running(self):
        return self._state == AwayActionState.RUNNING

    def run(self, callback):
        if self._state == AwayActionState.RUNNING:
            logger.callstack('Attempting to start away action that is already running.', owner='jjacobson')
            return
        self._periodic_stat_changes_instance.run()
        if self._exit_conditions:
            self._conditional_actions_manager.attach_conditions(self, self._exit_conditions, callback)
        self._state = AwayActionState.RUNNING

    def stop(self):
        if self._state == AwayActionState.STOPPED:
            logger.callstack('Attempting to stop away action that is already stopped.', owner='jjacobson')
            return
        self._periodic_stat_changes_instance.stop()
        if self._exit_conditions:
            self._conditional_actions_manager.detach_conditions(self)
        self._state = AwayActionState.STOPPED

    @flexmethod
    def get_participant(cls, inst, participant_type=ParticipantType.Actor, **kwargs):
        inst_or_cl = inst if inst is not None else cls
        participants = inst_or_cl.get_participants(participant_type=participant_type, **kwargs)
        if not participants:
            return
        if len(participants) > 1:
            raise ValueError('Too many participants returned for {}!'.format(participant_type))
        return next(iter(participants))

    @flexmethod
    def get_participants(cls, inst, participant_type, sim_info=DEFAULT, target=DEFAULT) -> set:
        inst_or_cls = inst if inst is not None else cls
        sim_info = inst.sim_info if sim_info is DEFAULT else sim_info
        target = inst.target if target is DEFAULT else target
        if sim_info is None:
            logger.error('Sim info is None when trying to get participants for Away Action {}.', inst_or_cls, owner='jjacobson')
            return ()
        results = set()
        participant_type = int(participant_type)
        if participant_type & ParticipantType.Actor:
            results.add(sim_info)
        if participant_type & ParticipantType.Lot:
            zone = services.get_zone(sim_info.zone_id, allow_uninstantiated_zones=True)
            results.add(zone.lot)
        if participant_type & ParticipantType.TargetSim and target is not None:
            results.add(target)
        return tuple(results)

    @flexmethod
    def get_resolver(cls, inst, **away_action_parameters):
        inst_or_cls = inst if inst is not None else cls
        return event_testing.resolver.AwayActionResolver(inst_or_cls, **away_action_parameters)

    @flexmethod
    def get_localization_tokens(cls, inst, **away_action_parameters):
        inst_or_cls = inst if inst is not None else cls
        tokens = inst_or_cls._display_name_text_tokens.get_tokens(inst_or_cls.get_resolver(**away_action_parameters))
        return tokens

    @flexmethod
    def test(cls, inst, sim_info=DEFAULT, **away_action_parameters):
        inst_or_cls = inst if inst is not None else cls
        sim_info = inst.sim_info if sim_info is DEFAULT else sim_info
        current_away_action = sim_info.current_away_action
        if inst_or_cls._disabled_when_running and current_away_action is not None and isinstance(current_away_action, cls):
            return TestResult(False, 'Cannot run away action when it is already running', tooltip=inst_or_cls._disabled_when_running)
        resolver = inst_or_cls.get_resolver(sim_info=sim_info, **away_action_parameters)
        if inst is None:
            condition_actions_manager = ConditionalActionManager()
        else:
            condition_actions_manager = inst._conditional_actions_manager
        if inst_or_cls._exit_conditions and condition_actions_manager.callback_will_trigger_immediately(resolver, inst_or_cls._exit_conditions):
            return TestResult(False, 'Away Action cannot run since exit conditions will satisfy immediately.')
        return inst_or_cls._tests.run_tests(resolver)

    @flexmethod
    def get_display_name(cls, inst, *tokens, **away_action_parameters):
        inst_or_cls = inst if inst is not None else cls
        localization_tokens = inst_or_cls.get_localization_tokens(**away_action_parameters)
        return inst_or_cls._display_name(*localization_tokens + tokens)
Exemple #4
0
class Photography:
    SMALL_PORTRAIT_OBJ_DEF = TunablePackSafeReference(
        description=
        '\n        Object definition for a small portrait photo.\n        ',
        manager=services.definition_manager())
    SMALL_LANDSCAPE_OBJ_DEF = TunablePackSafeReference(
        description=
        '\n        Object definition for a small landscape photo.\n        ',
        manager=services.definition_manager())
    MEDIUM_PORTRAIT_OBJ_DEF = TunablePackSafeReference(
        description=
        '\n        Object definition for a medium portrait photo.\n        ',
        manager=services.definition_manager())
    MEDIUM_LANDSCAPE_OBJ_DEF = TunablePackSafeReference(
        description=
        '\n        Object definition for a medium landscape photo.\n        ',
        manager=services.definition_manager())
    LARGE_PORTRAIT_OBJ_DEF = TunablePackSafeReference(
        description=
        '\n        Object definition for a large portrait photo.\n        ',
        manager=services.definition_manager())
    LARGE_LANDSCAPE_OBJ_DEF = TunablePackSafeReference(
        description=
        '\n        Object definition for a large landscape photo.\n        ',
        manager=services.definition_manager())
    PAINTING_INTERACTION_TAG = TunableEnumEntry(
        description=
        '\n        Tag to specify a painting interaction.\n        ',
        tunable_type=tag.Tag,
        default=tag.Tag.INVALID)
    PHOTOGRAPHY_LOOT_LIST = TunableList(
        description=
        '\n        A list of loot operations to apply to the photographer when photo mode exits.\n        ',
        tunable=TunableReference(manager=services.get_instance_manager(
            sims4.resources.Types.ACTION),
                                 class_restrictions=('LootActions', ),
                                 pack_safe=True))
    FAIL_PHOTO_QUALITY_RANGE = TunableInterval(
        description=
        '\n        The random quality statistic value that a failure photo will be\n        given between the min and max tuned values.\n        ',
        tunable_type=int,
        default_lower=0,
        default_upper=100)
    BASE_PHOTO_QUALITY_MAP = TunableMapping(
        description=
        '\n        The mapping of CameraQuality value to an interval of quality values\n        that will be used to asign a random base quality value to a photo\n        as it is created.\n        ',
        key_type=TunableEnumEntry(
            description=
            '\n            The CameraQuality value. If this photo has this CameraQuality,\n            value, then a random quality between the min value and max value\n            will be assigned to the photo.\n            ',
            tunable_type=CameraQuality,
            default=CameraQuality.CHEAP),
        value_type=TunableInterval(
            description=
            '\n            The range of base quality values from which a random value will be\n            given to the photo.\n            ',
            tunable_type=int,
            default_lower=1,
            default_upper=100))
    QUALITY_MODIFIER_PER_SKILL_LEVEL = Tunable(
        description=
        '\n        For each level of skill in Photography, this amount will be added to\n        the quality statistic.\n        ',
        tunable_type=float,
        default=0)
    PHOTO_VALUE_MODIFIER_MAP = TunableMapping(
        description=
        '\n        The mapping of state values to Simoleon value modifiers.\n        The final value of a photo is decided based on its\n        current value multiplied by the sum of all modifiers for\n        states that apply to the photo. All modifiers are\n        added together first, then the sum will be multiplied by\n        the current price.\n        ',
        key_type=TunableStateValueReference(
            description=
            '\n            The quality state values. If this photo has this state,\n            then a random modifier between min_value and max_value\n            will be multiplied to the current price.'
        ),
        value_type=TunableInterval(
            description=
            '\n            The maximum modifier multiplied to the current price based on the provided state value\n            ',
            tunable_type=float,
            default_lower=1,
            default_upper=1))
    PHOTO_VALUE_SKILL_CURVE = TunableStatisticModifierCurve.TunableFactory(
        description=
        "\n        Allows you to adjust the final value of the photo based on the Sim's\n        level of a given skill.\n        ",
        axis_name_overrides=('Skill Level', 'Simoleon Multiplier'),
        locked_args={'subject': ParticipantType.Actor})
    PHOTOGRAPHY_SKILL = Skill.TunablePackSafeReference(
        description='\n        A reference to the photography skill.\n        '
    )
    EMOTION_STATE_MAP = TunableMapping(
        description=
        "\n        The mapping of moods to states, used to give photo objects a mood\n        based state. These states are then used by the tooltip component to\n        display emotional content on the photo's tooltip.\n        ",
        key_type=TunableReference(
            description=
            '\n            The mood to associate with a state.\n            ',
            manager=services.mood_manager()),
        value_type=TunableStateValueReference(
            description=
            '\n            The state that represents the mood for the purpose of displaying\n            emotional content in a tooltip.\n            '
        ))
    PHOTO_OBJECT_LOOT_PER_TARGET = TunableList(
        description=
        '\n        A list of loots which will be applied once PER target. The participants\n        for each application will be Actor: photographer, Target: photograph\n        target and Object: the Photograph itself. If a photo interaction has 2\n        target sims, this loot will be applied twice.\n        ',
        tunable=TunableReference(manager=services.get_instance_manager(
            sims4.resources.Types.ACTION),
                                 pack_safe=True))
    MOOD_PARAM_TO_MOOD_CATEGORY_STATE = TunableMapping(
        description=
        '\n        If the player took a picture in a photo mode that supports mood\n        categories, we will perform a state change to the corresponding state\n        based on the mood that each picture was taken in.\n        ',
        key_type=Tunable(
            description=
            '\n            The mood ASM parameter value to associate with a state.\n            ',
            tunable_type=str,
            default=None),
        value_type=TunableStateValueReference(
            description=
            '\n            The state that represents the mood category.\n            '
        ))
    GROUP_PHOTO_X_ACTOR_TAG = TunableEnumEntry(
        description=
        '\n        Tag to specify the photo studio interaction that the photo target sim\n        who should be considered the x actor will run.\n        ',
        tunable_type=tag.Tag,
        default=tag.Tag.INVALID,
        invalid_enums=(tag.Tag.INVALID, ))
    GROUP_PHOTO_Y_ACTOR_TAG = TunableEnumEntry(
        description=
        '\n        Tag to specify the photo studio interaction that the photo target sim\n        who should be considered the y actor will run.\n        ',
        tunable_type=tag.Tag,
        default=tag.Tag.INVALID,
        invalid_enums=(tag.Tag.INVALID, ))
    GROUP_PHOTO_Z_ACTOR_TAG = TunableEnumEntry(
        description=
        '\n        Tag to specify the photo studio interaction that the photo target sim\n        who should be considered the z actor will run.\n        ',
        tunable_type=tag.Tag,
        default=tag.Tag.INVALID,
        invalid_enums=(tag.Tag.INVALID, ))
    NUM_PHOTOS_PER_SESSION = Tunable(
        description=
        '\n        Max possible photos that can be taken during one photo session. Once\n        this number has been reached, the photo session will exit.\n        ',
        tunable_type=int,
        default=5)

    @classmethod
    def _is_fail_photo(cls, photo_style_type):
        if photo_style_type == PhotoStyleType.EFFECT_GRAINY or (
                photo_style_type == PhotoStyleType.EFFECT_OVERSATURATED or
            (photo_style_type == PhotoStyleType.EFFECT_UNDERSATURATED or
             (photo_style_type == PhotoStyleType.PHOTO_FAIL_BLURRY or
              (photo_style_type == PhotoStyleType.PHOTO_FAIL_FINGER
               or photo_style_type == PhotoStyleType.PHOTO_FAIL_GNOME)))
        ) or photo_style_type == PhotoStyleType.PHOTO_FAIL_NOISE:
            return True
        return False

    @classmethod
    def _apply_quality_and_value_to_photo(cls, photographer_sim, photo_obj,
                                          photo_style, camera_quality):
        quality_stat = CraftingTuning.QUALITY_STATISTIC
        quality_stat_tracker = photo_obj.get_tracker(quality_stat)
        if cls._is_fail_photo(photo_style):
            final_quality = cls.FAIL_PHOTO_QUALITY_RANGE.random_int()
        else:
            quality_range = cls.BASE_PHOTO_QUALITY_MAP.get(
                camera_quality, None)
            if quality_range is None:
                logger.error(
                    'Photography tuning BASE_PHOTO_QUALITY_MAP does not have an expected quality value: []',
                    str(camera_quality))
                return
            base_quality = quality_range.random_int()
            skill_quality_modifier = 0
            if cls.PHOTOGRAPHY_SKILL is not None:
                effective_skill_level = photographer_sim.get_effective_skill_level(
                    cls.PHOTOGRAPHY_SKILL)
                if effective_skill_level:
                    skill_quality_modifier = effective_skill_level * cls.QUALITY_MODIFIER_PER_SKILL_LEVEL
            final_quality = base_quality + skill_quality_modifier
        quality_stat_tracker.set_value(quality_stat, final_quality)
        value_multiplier = 1
        for (state_value, value_mods) in cls.PHOTO_VALUE_MODIFIER_MAP.items():
            if photo_obj.has_state(state_value.state):
                actual_state_value = photo_obj.get_state(state_value.state)
                if state_value is actual_state_value:
                    value_multiplier *= value_mods.random_float()
                    break
        value_multiplier *= cls.PHOTO_VALUE_SKILL_CURVE.get_multiplier(
            SingleSimResolver(photographer_sim), photographer_sim)
        photo_obj.base_value = int(photo_obj.base_value * value_multiplier)

    @classmethod
    def _get_mood_sim_info_if_exists(cls, photographer_sim_info,
                                     target_sim_ids, camera_mode):
        if camera_mode is CameraMode.SELFIE_PHOTO:
            return photographer_sim_info
        else:
            num_target_sims = len(target_sim_ids)
            if num_target_sims == 1:
                sim_info_manager = services.sim_info_manager()
                target_sim_info = sim_info_manager.get(target_sim_ids[0])
                return target_sim_info

    @classmethod
    def _apply_mood_state_if_appropriate(cls, photographer_sim_info,
                                         target_sim_ids, camera_mode,
                                         photo_object):
        mood_sim_info = cls._get_mood_sim_info_if_exists(
            photographer_sim_info, target_sim_ids, camera_mode)
        if mood_sim_info:
            mood = mood_sim_info.get_mood()
            mood_state = cls.EMOTION_STATE_MAP.get(mood, None)
            if mood_state:
                photo_object.set_state(mood_state.state, mood_state)

    @classmethod
    def _apply_mood_category_state_if_appropriate(cls, selected_mood_param,
                                                  camera_mode, photo_object):
        if camera_mode in (CameraMode.TRIPOD, CameraMode.SIM_PHOTO,
                           CameraMode.PHOTO_STUDIO_PHOTO):
            mood_category_state = cls.MOOD_PARAM_TO_MOOD_CATEGORY_STATE.get(
                selected_mood_param, None)
            if mood_category_state:
                photo_object.set_state(mood_category_state.state,
                                       mood_category_state)

    @classmethod
    def create_photo_from_photo_data(cls, camera_mode, camera_quality,
                                     photographer_sim_id, target_obj_id,
                                     target_sim_ids, res_key, photo_style,
                                     photo_size, photo_orientation,
                                     photographer_sim_info, photographer_sim,
                                     time_stamp, selected_mood_param):
        photo_object = None
        is_paint_by_reference = camera_mode is CameraMode.PAINT_BY_REFERENCE
        if is_paint_by_reference:
            current_zone = services.current_zone()
            photo_object = current_zone.object_manager.get(target_obj_id)
            if photo_object is None:
                photo_object = current_zone.inventory_manager.get(
                    target_obj_id)
        else:
            if photo_orientation == PhotoOrientation.LANDSCAPE:
                if photo_size == PhotoSize.LARGE:
                    photo_object_def = cls.LARGE_LANDSCAPE_OBJ_DEF
                elif photo_size == PhotoSize.MEDIUM:
                    photo_object_def = cls.MEDIUM_LANDSCAPE_OBJ_DEF
                elif photo_size == PhotoSize.SMALL:
                    photo_object_def = cls.SMALL_LANDSCAPE_OBJ_DEF
            elif photo_orientation == PhotoOrientation.PORTRAIT:
                if photo_size == PhotoSize.LARGE:
                    photo_object_def = cls.LARGE_PORTRAIT_OBJ_DEF
                elif photo_size == PhotoSize.MEDIUM:
                    photo_object_def = cls.MEDIUM_PORTRAIT_OBJ_DEF
                elif photo_size == PhotoSize.SMALL:
                    photo_object_def = cls.SMALL_PORTRAIT_OBJ_DEF
                else:
                    photo_object_def = cls.SMALL_LANDSCAPE_OBJ_DEF
            if photo_object_def is None:
                return
            photo_object = create_object(photo_object_def)
        if photo_object is None:
            logger.error('photo object could not be found.')
            return
        for target_sim_id in target_sim_ids:
            target_sim_info = services.sim_info_manager().get(target_sim_id)
            target_sim = target_sim_info.get_sim_instance()
            resolver = DoubleSimAndObjectResolver(photographer_sim,
                                                  target_sim,
                                                  photo_object,
                                                  source=cls)
            for loot in cls.PHOTO_OBJECT_LOOT_PER_TARGET:
                loot.apply_to_resolver(resolver)
        photography_service = services.get_photography_service()
        loots = photography_service.get_loots_for_photo()
        for photoloot in loots:
            if photoloot._AUTO_FACTORY.FACTORY_TYPE is RotateTargetPhotoLoot:
                photographer_sim = photoloot.photographer
                photographer_sim_info = photographer_sim.sim_info
                break
        reveal_level = PaintingState.REVEAL_LEVEL_MIN if is_paint_by_reference else PaintingState.REVEAL_LEVEL_MAX
        painting_state = PaintingState.from_key(res_key, reveal_level, False,
                                                photo_style)
        photo_object.canvas_component.painting_state = painting_state
        photo_object.canvas_component.time_stamp = time_stamp
        photo_object.set_household_owner_id(photographer_sim.household_id)
        if selected_mood_param:
            cls._apply_mood_category_state_if_appropriate(
                selected_mood_param, camera_mode, photo_object)
        if not is_paint_by_reference:
            cls._apply_quality_and_value_to_photo(photographer_sim,
                                                  photo_object, photo_style,
                                                  camera_quality)
            cls._apply_mood_state_if_appropriate(photographer_sim_info,
                                                 target_sim_ids, camera_mode,
                                                 photo_object)
            photo_object.add_dynamic_component(STORED_SIM_INFO_COMPONENT,
                                               sim_id=photographer_sim.id)
            photo_object.update_object_tooltip()
            if not (photographer_sim.inventory_component.can_add(photo_object)
                    and photographer_sim.inventory_component.
                    player_try_add_object(photo_object)):
                logger.error(
                    "photo object could not be put in the sim's inventory, deleting photo."
                )
                photo_object.destroy()
        photo_targets = [
            services.sim_info_manager().get(sim_id)
            for sim_id in target_sim_ids
        ]
        if camera_mode == CameraMode.TWO_SIM_SELFIE_PHOTO:
            photo_targets.append(photographer_sim_info)
        photo_targets = frozenset(photo_targets)
        services.get_event_manager().process_event(
            test_events.TestEvent.PhotoTaken,
            sim_info=photographer_sim_info,
            photo_object=photo_object,
            photo_targets=photo_targets)
Exemple #5
0
class Skill(HasTunableReference, ProgressiveStatisticCallbackMixin, statistics.continuous_statistic_tuning.TunedContinuousStatistic, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(sims4.resources.Types.STATISTIC)):
    SKILL_LEVEL_LIST = TunableMapping(description='\n        A mapping defining the level boundaries for each skill type.\n        ', key_type=SkillLevelType, value_type=TunableList(description='\n            The level boundaries for skill type, specified as a delta from the\n            previous value.\n            ', tunable=Tunable(tunable_type=int, default=0)), tuple_name='SkillLevelListMappingTuple', export_modes=ExportModes.All)
    SKILL_EFFECTIVENESS_GAIN = TunableMapping(description='\n        Skill gain points based on skill effectiveness.\n        ', key_type=SkillEffectiveness, value_type=TunableCurve())
    DYNAMIC_SKILL_INTERVAL = TunableRange(description='\n        Interval used when dynamic loot is used in a\n        PeriodicStatisticChangeElement.\n        ', tunable_type=float, default=1, minimum=1)
    INSTANCE_TUNABLES = {'stat_name': TunableLocalizedString(description='\n            The name of this skill.\n            ', export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'skill_description': TunableLocalizedString(description="\n            The skill's normal description.\n            ", export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'locked_description': TunableLocalizedString(description="\n            The skill description when it's locked.\n            ", allow_none=True, export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'icon': TunableIcon(description='\n            Icon to be displayed for the Skill.\n            ', export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'tooltip_icon_list': TunableList(description='\n            A list of icons to show in the tooltip of this\n            skill.\n            ', tunable=TunableIcon(description='\n                Icon that is displayed what types of objects help\n                improve this skill.\n                '), export_modes=(ExportModes.ClientBinary,), tuning_group=GroupNames.UI), 'tutorial': TunableReference(description='\n            Tutorial instance for this skill. This will be used to bring up the\n            skill lesson from the first notification for Sim to know this skill.\n            ', manager=services.get_instance_manager(sims4.resources.Types.TUTORIAL), allow_none=True, class_restrictions=('Tutorial',), tuning_group=GroupNames.UI), 'priority': Tunable(description="\n            Skill priority.  Higher priority skills will trump other skills when\n            being displayed on the UI. When a Sim gains multiple skills at the\n            same time, only the highest priority one will display a progress bar\n            over the Sim's head.\n            ", tunable_type=int, default=1, export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'next_level_teaser': TunableList(description='\n            Tooltip which describes what the next level entails.\n            ', tunable=TunableLocalizedString(), export_modes=(ExportModes.ClientBinary,), tuning_group=GroupNames.UI), 'mood_id': TunableReference(description='\n            When this mood is set and active sim matches mood, the UI will\n            display a special effect on the skill bar to represent that this\n            skill is getting a bonus because of the mood.\n            ', manager=services.mood_manager(), allow_none=True, export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'stat_asm_param': TunableStatAsmParam.TunableFactory(tuning_group=GroupNames.ANIMATION), 'hidden': Tunable(description='\n            If checked, this skill will be hidden.\n            ', tunable_type=bool, default=False, export_modes=ExportModes.All, tuning_group=GroupNames.AVAILABILITY), 'update_client_for_npcs': Tunable(description="\n            Whether this skill will send update messages to the client\n            for non-active household sims (NPCs).\n            \n            e.g. A toddler's communication skill determines the VOX they use, so\n            the client needs to know the skill level for all toddlers in order\n            for this work properly.\n            ", tunable_type=bool, default=False, tuning_group=GroupNames.UI), 'is_default': Tunable(description='\n            Whether Sim will default has this skill.\n            ', tunable_type=bool, default=False, tuning_group=GroupNames.AVAILABILITY), 'ages': TunableSet(description='\n            Allowed ages for this skill.\n            ', tunable=TunableEnumEntry(tunable_type=Age, default=Age.ADULT, export_modes=ExportModes.All), tuning_group=GroupNames.AVAILABILITY), 'ad_data': TunableList(description='\n            A list of Vector2 points that define the desire curve for this\n            commodity.\n            ', tunable=TunableVector2(description='\n                Point on a Curve\n                ', default=sims4.math.Vector2(0, 0)), tuning_group=GroupNames.AUTONOMY), 'weight': Tunable(description="\n            The weight of the Skill with regards to autonomy.  It's ignored for\n            the purposes of sorting stats, but it's applied when scoring the\n            actual statistic operation for the SI.\n            ", tunable_type=float, default=0.5, tuning_group=GroupNames.AUTONOMY), 'statistic_multipliers': TunableMapping(description='\n            Multipliers this skill applies to other statistics based on its\n            value.\n            ', key_type=TunableReference(description='\n                The statistic this multiplier will be applied to.\n                ', manager=services.statistic_manager(), reload_dependent=True), value_type=TunableTuple(curve=TunableCurve(description='\n                    Tunable curve where the X-axis defines the skill level, and\n                    the Y-axis defines the associated multiplier.\n                    ', x_axis_name='Skill Level', y_axis_name='Multiplier'), direction=TunableEnumEntry(description="\n                    Direction where the multiplier should work on the\n                    statistic.  For example, a tuned decrease for an object's\n                    brokenness rate will not also increase the time it takes to\n                    repair it.\n                    ", tunable_type=StatisticChangeDirection, default=StatisticChangeDirection.INCREASE), use_effective_skill=Tunable(description='\n                    If checked, this modifier will look at the current\n                    effective skill value.  If unchecked, this modifier will\n                    look at the actual skill value.\n                    ', tunable_type=bool, needs_tuning=True, default=True)), tuning_group=GroupNames.MULTIPLIERS), 'success_chance_multipliers': TunableList(description='\n            Multipliers this skill applies to the success chance of\n            affordances.\n            ', tunable=TunableSkillMultiplier(), tuning_group=GroupNames.MULTIPLIERS), 'monetary_payout_multipliers': TunableList(description='\n            Multipliers this skill applies to the monetary payout amount of\n            affordances.\n            ', tunable=TunableSkillMultiplier(), tuning_group=GroupNames.MULTIPLIERS), 'tags': TunableList(description='\n            The associated categories of the skill\n            ', tunable=TunableEnumEntry(tunable_type=tag.Tag, default=tag.Tag.INVALID, pack_safe=True), tuning_group=GroupNames.CORE), 'skill_level_type': TunableEnumEntry(description='\n            Skill level list to use.\n            ', tunable_type=SkillLevelType, default=SkillLevelType.MAJOR, export_modes=ExportModes.All, tuning_group=GroupNames.CORE), 'level_data': TunableMapping(description='\n            Level-specific information, such as notifications to be displayed to\n            level up.\n            ', key_type=int, value_type=TunableTuple(level_up_notification=UiDialogNotification.TunableFactory(description='\n                    The notification to display when the Sim obtains this level.\n                    The text will be provided two tokens: the Sim owning the\n                    skill and a number representing the 1-based skill level\n                    ', locked_args={'text_tokens': DEFAULT, 'icon': None, 'primary_icon_response': UiDialogResponse(text=None, ui_request=UiDialogResponse.UiDialogUiRequest.SHOW_SKILL_PANEL), 'secondary_icon': None}), level_up_screen_slam=OptionalTunable(description='\n                    Screen slam to show when reaches this skill level.\n                    Localization Tokens: Sim - {0.SimFirstName}, Skill Name - \n                    {1.String}, Skill Number - {2.Number}\n                    ', tunable=ui.screen_slam.TunableScreenSlamSnippet()), skill_level_buff=OptionalTunable(tunable=TunableReference(description='\n                        The buff to place on a Sim when they reach this specific\n                        level of skill.\n                        ', manager=services.buff_manager())), rewards=TunableList(description='\n                    A reward to give for achieving this level.\n                    ', tunable=rewards.reward_tuning.TunableSpecificReward(pack_safe=True)), loot=TunableList(description='\n                    A loot to apply for achieving this level.\n                    ', tunable=TunableReference(manager=services.get_instance_manager(sims4.resources.Types.ACTION), class_restrictions=('LootActions',))), super_affordances=TunableSet(description='\n                    Super affordances this adds to the Sim.\n                    ', tunable=TunableReference(description='\n                        A super affordance added to this Sim.\n                        ', manager=services.get_instance_manager(sims4.resources.Types.INTERACTION), class_restrictions=('SuperInteraction',), pack_safe=True)), target_super_affordances=TunableProvidedAffordances(description='\n                    Super affordances this adds to the target.\n                    ', locked_args={'target': ParticipantType.Object, 'carry_target': ParticipantType.Invalid, 'is_linked': False, 'unlink_if_running': False}), actor_mixers=TunableMapping(description='\n                    Mixers this adds to an associated actor object. (When targeting\n                    something else.)\n                    ', key_type=TunableReference(description='\n                        The super affordance these mixers are associated with.\n                        ', manager=services.get_instance_manager(sims4.resources.Types.INTERACTION), class_restrictions=('SuperInteraction',), pack_safe=True), value_type=TunableSet(description='\n                        Set of mixer affordances associated with the super affordance.\n                        ', tunable=TunableReference(description='\n                            Linked mixer affordance.\n                            ', manager=services.get_instance_manager(sims4.resources.Types.INTERACTION), category='asm', class_restrictions=('MixerInteraction',), pack_safe=True)))), tuning_group=GroupNames.CORE), 'age_up_skill_transition_data': OptionalTunable(description='\n            Data used to modify the value of a new skill based on the level\n            of this skill.\n            \n            e.g. Toddler Communication skill transfers into Child Social skill.\n            ', tunable=TunableTuple(new_skill=TunablePackSafeReference(description='\n                    The new skill.\n                    ', manager=services.get_instance_manager(sims4.resources.Types.STATISTIC)), skill_data=TunableMapping(description="\n                    A mapping between this skill's levels and the\n                    new skill's internal value.\n                    \n                    The keys are user facing skill levels.\n                    \n                    The values are the internal statistic value, not the user\n                    facing skill level.\n                    ", key_type=Tunable(description="\n                        This skill's level.\n                        \n                        This is the actual user facing skill level.\n                        ", tunable_type=int, default=0), value_type=Tunable(description='\n                        The new skill\'s value.\n                        \n                        This is the internal statistic\n                        value, not the user facing skill level."\n                        ', tunable_type=int, default=0))), tuning_group=GroupNames.SPECIAL_CASES), 'skill_unlocks_on_max': TunableList(description='\n            A list of skills that become unlocked when this skill is maxed.\n            ', tunable=TunableReference(description='\n                A skill to unlock.\n                ', manager=services.get_instance_manager(sims4.resources.Types.STATISTIC), class_restrictions=('Skill',), pack_safe=True), tuning_group=GroupNames.SPECIAL_CASES), 'trend_tag': OptionalTunable(description='\n            If enabled, we associate this skill with a particular trend via tag\n            which you can find in trend_tuning.\n            ', tunable=TunableTag(description='\n                The trend tag we associate with this skill\n                ', filter_prefixes=('func_trend',)))}
    REMOVE_INSTANCE_TUNABLES = ('min_value_tuning', 'max_value_tuning', 'decay_rate', '_default_convergence_value')

    def __init__(self, tracker):
        self._skill_level_buff = None
        super().__init__(tracker, self.initial_value)
        self._delta_enabled = True
        self._max_level_update_sent = False

    @classmethod
    def _tuning_loaded_callback(cls):
        super()._tuning_loaded_callback()
        level_list = cls.get_level_list()
        cls.max_level = len(level_list)
        cls.min_value_tuning = 0
        cls.max_value_tuning = sum(level_list)
        cls._default_convergence_value = cls.min_value_tuning
        cls._build_utility_curve_from_tuning_data(cls.ad_data)
        for stat in cls.statistic_multipliers:
            multiplier = cls.statistic_multipliers[stat]
            curve = multiplier.curve
            direction = multiplier.direction
            use_effective_skill = multiplier.use_effective_skill
            stat.add_skill_based_statistic_multiplier(cls, curve, direction, use_effective_skill)
        for multiplier in cls.success_chance_multipliers:
            curve = multiplier.curve
            use_effective_skill = multiplier.use_effective_skill
            for affordance in multiplier.affordance_list:
                affordance.add_skill_multiplier(affordance.success_chance_multipliers, cls, curve, use_effective_skill)
        for multiplier in cls.monetary_payout_multipliers:
            curve = multiplier.curve
            use_effective_skill = multiplier.use_effective_skill
            for affordance in multiplier.affordance_list:
                affordance.add_skill_multiplier(affordance.monetary_payout_multipliers, cls, curve, use_effective_skill)

    @classmethod
    def _verify_tuning_callback(cls):
        success_multiplier_affordances = []
        for multiplier in cls.success_chance_multipliers:
            success_multiplier_affordances.extend(multiplier.affordance_list)
        if len(success_multiplier_affordances) != len(set(success_multiplier_affordances)):
            logger.error("The same affordance has been tuned more than once under {}'s success multipliers, and they will overwrite each other. Please fix in tuning.", cls, owner='tastle')
        monetary_payout_multiplier_affordances = []
        for multiplier in cls.monetary_payout_multipliers:
            monetary_payout_multiplier_affordances.extend(multiplier.affordance_list)
        if len(monetary_payout_multiplier_affordances) != len(set(monetary_payout_multiplier_affordances)):
            logger.error("The same affordance has been tuned more than once under {}'s monetary payout multipliers, and they will overwrite each other. Please fix in tuning.", cls, owner='tastle')

    @classproperty
    def skill_type(cls):
        return cls

    @constproperty
    def is_skill():
        return True

    @classproperty
    def autonomy_weight(cls):
        return cls.weight

    @constproperty
    def remove_on_convergence():
        return False

    @classproperty
    def valid_for_stat_testing(cls):
        return True

    @classmethod
    def can_add(cls, owner, force_add=False, **kwargs):
        if force_add:
            return True
        if owner.age not in cls.ages:
            return False
        return super().can_add(owner, **kwargs)

    @classmethod
    def convert_to_user_value(cls, value):
        level_list = cls.get_level_list()
        if not level_list:
            return 0
        current_value = value
        for (level, level_threshold) in enumerate(level_list):
            current_value -= level_threshold
            if current_value < 0:
                return level
        return level + 1

    @classmethod
    def convert_from_user_value(cls, user_value):
        (level_min, _) = cls._get_level_bounds(user_value)
        return level_min

    @classmethod
    def create_skill_update_msg(cls, sim_id, stat_value):
        skill_msg = Commodities_pb2.Skill_Update()
        skill_msg.skill_id = cls.guid64
        skill_msg.curr_points = int(stat_value)
        skill_msg.sim_id = sim_id
        return skill_msg

    @classmethod
    def get_level_list(cls):
        return cls.SKILL_LEVEL_LIST.get(cls.skill_level_type)

    @classmethod
    def get_skill_effectiveness_points_gain(cls, effectiveness_level, level):
        skill_gain_curve = cls.SKILL_EFFECTIVENESS_GAIN.get(effectiveness_level)
        if skill_gain_curve is not None:
            return skill_gain_curve.get(level)
        logger.error('{} does not exist in SKILL_EFFECTIVENESS_GAIN mapping', effectiveness_level)
        return 0

    def _get_level_data_for_skill_level(self, skill_level):
        level_data = self.level_data.get(skill_level)
        if level_data is None:
            logger.debug('No level data found for skill [{}] at level [{}].', self, skill_level)
        return level_data

    @property
    def is_initial_value(self):
        return self.initial_value == self.get_value()

    def should_send_update(self, sim_info, stat_value):
        if sim_info.is_npc and not self.update_client_for_npcs:
            return False
        if self.hidden:
            return False
        if Skill.convert_to_user_value(stat_value) == 0:
            return False
        if self.reached_max_level:
            if self._max_level_update_sent:
                return False
            self._max_level_update_sent = True
        return True

    def on_initial_startup(self):
        super().on_initial_startup()
        skill_level = self.get_user_value()
        self._update_skill_level_buff(skill_level)

    def on_add(self):
        super().on_add()
        self._tracker.owner.add_modifiers_for_skill(self)
        level_data = self._get_level_data_for_skill_level(self.get_user_value())
        if level_data is not None:
            provided_affordances = []
            for provided_affordance in level_data.target_super_affordances:
                provided_affordance_data = ProvidedAffordanceData(provided_affordance.affordance, provided_affordance.object_filter, provided_affordance.allow_self)
                provided_affordances.append(provided_affordance_data)
            self._tracker.add_to_affordance_caches(level_data.super_affordances, provided_affordances)
            self._tracker.add_to_actor_mixer_cache(level_data.actor_mixers)
            sim = self._tracker._owner.get_sim_instance()
            apply_super_affordance_commodity_flags(sim, self, level_data.super_affordances)

    def on_remove(self, on_destroy=False):
        super().on_remove(on_destroy=on_destroy)
        self._destory_callback_handle()
        if not on_destroy:
            self._send_skill_delete_message()
        if self._skill_level_buff is not None:
            self._tracker.owner.remove_buff(self._skill_level_buff)
            self._skill_level_buff = None
        if not on_destroy:
            self._tracker.update_affordance_caches()
        sim = self._tracker._owner.get_sim_instance()
        remove_super_affordance_commodity_flags(sim, self)

    def on_zone_load(self):
        self._max_level_update_sent = False

    def _apply_multipliers_to_continuous_statistics(self):
        for stat in self.statistic_multipliers:
            if stat.continuous:
                owner_stat = self.tracker.get_statistic(stat)
                if owner_stat is not None:
                    owner_stat._recalculate_modified_decay_rate()

    @classproperty
    def default_value(cls):
        return cls.initial_value

    @flexmethod
    @caches.cached
    def get_user_value(cls, inst):
        inst_or_cls = inst if inst is not None else cls
        return super(__class__, inst_or_cls).get_user_value()

    def _clear_user_value_cache(self):
        self.get_user_value.func.cache.clear()

    def set_value(self, value, *args, from_load=False, interaction=None, **kwargs):
        old_value = self.get_value()
        super().set_value(value, *args, **kwargs)
        if not caches.skip_cache:
            self._clear_user_value_cache()
        if from_load:
            return
        event_manager = services.get_event_manager()
        sim_info = self._tracker._owner
        new_value = self.get_value()
        new_level = self.convert_to_user_value(value)
        if old_value == self.initial_value or old_value != new_value:
            event_manager.process_event(test_events.TestEvent.SkillValueChange, sim_info=sim_info, skill=self, statistic=self.stat_type, custom_keys=(self.stat_type,))
        old_level = self.convert_to_user_value(old_value)
        if old_level < new_level or old_value == self.initial_value:
            self._apply_multipliers_to_continuous_statistics()
            event_manager.process_event(test_events.TestEvent.SkillLevelChange, sim_info=sim_info, skill=self, new_level=new_level, custom_keys=(self.stat_type,))

    def add_value(self, add_amount, interaction=None, **kwargs):
        old_value = self.get_value()
        if old_value == self.initial_value:
            telemhook = TELEMETRY_HOOK_SKILL_INTERACTION_FIRST_TIME
        else:
            telemhook = TELEMETRY_HOOK_SKILL_INTERACTION
        super().add_value(add_amount, interaction=interaction)
        if not caches.skip_cache:
            self._clear_user_value_cache()
        if interaction is not None:
            interaction_name = interaction.affordance.__name__
        else:
            interaction_name = TELEMETRY_INTERACTION_NOT_AVAILABLE
        self.on_skill_updated(telemhook, old_value, self.get_value(), interaction_name)

    def _update_value(self):
        old_value = self._value
        if gsi_handlers.sim_handlers_log.skill_change_archiver.enabled:
            last_update = self._last_update
        time_delta = super()._update_value()
        if not caches.skip_cache:
            self._clear_user_value_cache()
        new_value = self._value
        if old_value < new_value:
            event_manager = services.get_event_manager()
            sim_info = self._tracker._owner if self._tracker is not None else None
            if old_value == self.initial_value:
                telemhook = TELEMETRY_HOOK_SKILL_INTERACTION_FIRST_TIME
                self.on_skill_updated(telemhook, old_value, new_value, TELEMETRY_INTERACTION_NOT_AVAILABLE)
            event_manager.process_event(test_events.TestEvent.SkillValueChange, sim_info=sim_info, skill=self, statistic=self.stat_type, custom_keys=(self.stat_type,))
            old_level = self.convert_to_user_value(old_value)
            new_level = self.convert_to_user_value(new_value)
            if gsi_handlers.sim_handlers_log.skill_change_archiver.enabled and self.tracker.owner.is_sim:
                gsi_handlers.sim_handlers_log.archive_skill_change(self.tracker.owner, self, time_delta, old_value, new_value, new_level, last_update)
            if old_level < new_level or old_value == self.initial_value:
                if self._tracker is not None:
                    self._tracker.notify_watchers(self.stat_type, self._value, self._value)
                event_manager.process_event(test_events.TestEvent.SkillLevelChange, sim_info=sim_info, skill=self, new_level=new_level, custom_keys=(self.stat_type,))

    def _on_statistic_modifier_changed(self, notify_watcher=True):
        super()._on_statistic_modifier_changed(notify_watcher=notify_watcher)
        if not self.reached_max_level:
            return
        event_manager = services.get_event_manager()
        sim_info = self._tracker._owner if self._tracker is not None else None
        event_manager.process_event(test_events.TestEvent.SkillValueChange, sim_info=sim_info, skill=self, statistic=self.stat_type, custom_keys=(self.stat_type,))

    def on_skill_updated(self, telemhook, old_value, new_value, affordance_name):
        owner_sim_info = self._tracker._owner
        if owner_sim_info.is_selectable:
            with telemetry_helper.begin_hook(skill_telemetry_writer, telemhook, sim_info=owner_sim_info) as hook:
                hook.write_guid(TELEMETRY_FIELD_SKILL_ID, self.guid64)
                hook.write_string(TELEMETRY_FIELD_SKILL_AFFORDANCE, affordance_name)
                hook.write_bool(TELEMETRY_FIELD_SKILL_AFFORDANCE_SUCCESS, True)
                hook.write_int(TELEMETRY_FIELD_SKILL_AFFORDANCE_VALUE_ADD, new_value - old_value)
        if old_value == self.initial_value:
            skill_level = self.convert_to_user_value(old_value)
            self._handle_skill_up(skill_level)

    def _send_skill_delete_message(self):
        if self.tracker.owner.is_npc:
            return
        skill_msg = Commodities_pb2.SkillDelete()
        skill_msg.skill_id = self.guid64
        op = GenericProtocolBufferOp(Operation.SIM_SKILL_DELETE, skill_msg)
        Distributor.instance().add_op(self.tracker.owner, op)

    @staticmethod
    def _callback_handler(stat_inst):
        new_level = stat_inst.get_user_value()
        old_level = new_level - 1
        stat_inst.on_skill_level_up(old_level, new_level)
        stat_inst.refresh_threshold_callback()

    def _handle_skill_up(self, skill_level):
        self._show_level_notification(skill_level)
        self._update_skill_level_buff(skill_level)
        self._try_give_skill_up_payout(skill_level)
        self._tracker.update_affordance_caches()
        sim = self._tracker._owner.get_sim_instance()
        remove_super_affordance_commodity_flags(sim, self)
        super_affordances = tuple(self._tracker.get_cached_super_affordances_gen())
        apply_super_affordance_commodity_flags(sim, self, super_affordances)

    def _recalculate_modified_decay_rate(self):
        pass

    def refresh_level_up_callback(self):
        self._destory_callback_handle()

        def _on_level_up_callback(stat_inst):
            new_level = stat_inst.get_user_value()
            old_level = new_level - 1
            stat_inst.on_skill_level_up(old_level, new_level)
            stat_inst.refresh_level_up_callback()

        self._callback_handle = self.create_and_add_callback_listener(Threshold(self._get_next_level_bound(), operator.ge), _on_level_up_callback)

    def on_skill_level_up(self, old_level, new_level):
        tracker = self.tracker
        sim_info = tracker._owner
        if self.reached_max_level:
            for skill in self.skill_unlocks_on_max:
                skill_instance = tracker.add_statistic(skill, force_add=True)
                skill_instance.set_value(skill.initial_value)
        with telemetry_helper.begin_hook(skill_telemetry_writer, TELEMETRY_HOOK_SKILL_LEVEL_UP, sim_info=sim_info) as hook:
            hook.write_guid(TELEMETRY_FIELD_SKILL_ID, self.guid64)
            hook.write_int(TELEMETRY_FIELD_SKILL_LEVEL, new_level)
        self._handle_skill_up(new_level)
        services.get_event_manager().process_event(test_events.TestEvent.SkillValueChange, sim_info=sim_info, statistic=self.stat_type, custom_keys=(self.stat_type,))

    def _show_level_notification(self, skill_level, ignore_npc_check=False):
        sim_info = self._tracker._owner
        if not (ignore_npc_check or not sim_info.is_npc):
            if skill_level == 1:
                tutorial_service = services.get_tutorial_service()
                if tutorial_service is not None and tutorial_service.is_tutorial_running():
                    return
            level_data = self._get_level_data_for_skill_level(skill_level)
            if level_data is not None:
                tutorial_id = None
                if self.tutorial is not None:
                    if skill_level == 1:
                        tutorial_id = self.tutorial.guid64
                notification = level_data.level_up_notification(sim_info, resolver=SingleSimResolver(sim_info))
                notification.show_dialog(icon_override=IconInfoData(icon_resource=self.icon), secondary_icon_override=IconInfoData(obj_instance=sim_info), additional_tokens=(skill_level,), tutorial_id=tutorial_id)
                if level_data.level_up_screen_slam is not None:
                    level_data.level_up_screen_slam.send_screen_slam_message(sim_info, sim_info, self.stat_name, skill_level)

    def _update_skill_level_buff(self, skill_level):
        level_data = self._get_level_data_for_skill_level(skill_level)
        new_buff = level_data.skill_level_buff if level_data is not None else None
        if self._skill_level_buff is not None:
            self._tracker.owner.remove_buff(self._skill_level_buff)
            self._skill_level_buff = None
        if new_buff is not None:
            self._skill_level_buff = self._tracker.owner.add_buff(new_buff)

    def _try_give_skill_up_payout(self, skill_level):
        level_data = self._get_level_data_for_skill_level(skill_level)
        if level_data is None:
            return
        if level_data.rewards:
            for reward in level_data.rewards:
                reward().open_reward(self._tracker.owner, reward_destination=RewardDestination.SIM, reward_source=self)
        if level_data.loot:
            resolver = SingleSimResolver(self._tracker.owner)
            for loot in level_data.loot:
                loot.apply_to_resolver(resolver)

    def force_show_level_notification(self, skill_level):
        self._show_level_notification(skill_level, ignore_npc_check=True)

    @classmethod
    def send_commodity_update_message(cls, sim_info, old_value, new_value):
        stat_instance = sim_info.get_statistic(cls.stat_type, add=False)
        if stat_instance is None or not stat_instance.should_send_update(sim_info, new_value):
            return
        msg = cls.create_skill_update_msg(sim_info.id, new_value)
        add_object_message(sim_info, MSG_SIM_SKILL_UPDATE, msg, False)
        change_rate = stat_instance.get_change_rate()
        hide_progress_bar = False
        if sim_info.is_npc or sim_info.is_skill_bar_suppressed():
            hide_progress_bar = True
        op = distributor.ops.SkillProgressUpdate(cls.guid64, change_rate, new_value, hide_progress_bar)
        distributor.ops.record(sim_info, op)

    def save_statistic(self, commodities, skills, ranked_stats, tracker):
        current_value = self.get_saved_value()
        if current_value == self.initial_value:
            return
        message = protocols.Skill()
        message.name_hash = self.guid64
        message.value = current_value
        if self._time_of_last_value_change:
            message.time_of_last_value_change = self._time_of_last_value_change.absolute_ticks()
        skills.append(message)

    def unlocks_skills_on_max(self):
        return True

    def can_decay(self):
        return False

    def get_skill_provided_affordances(self):
        level_data = self._get_level_data_for_skill_level(self.get_user_value())
        if level_data is None:
            return ((), ())
        return (level_data.super_affordances, level_data.target_super_affordances)

    def get_skill_provided_actor_mixers(self):
        level_data = self._get_level_data_for_skill_level(self.get_user_value())
        if level_data is None:
            return
        return level_data.actor_mixers

    def get_actor_mixers(self, super_interaction):
        level_data = self._get_level_data_for_skill_level(self.get_user_value())
        if level_data is None:
            return []
        mixers = level_data.actor_mixers.get(super_interaction, tuple()) if level_data is not None else []
        return mixers

    @flexmethod
    def populate_localization_token(cls, inst, token):
        inst_or_cls = inst if inst is not None else cls
        token.type = LocalizedStringToken.STRING
        token.text_string = inst_or_cls.stat_name
class Buff(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(sims4.resources.Types.BUFF)):
    __qualname__ = 'Buff'
    INSTANCE_TUNABLES = {'buff_name': TunableLocalizedString(description='\n        Name of buff.\n        ', tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'buff_description': TunableLocalizedString(description='\n        Tooltip description of the Buff Effect.\n        ', tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'icon': TunableResourceKey(description='\n        Icon to be displayed for buff\n        ', default=None, needs_tuning=True, resource_types=sims4.resources.CompoundTypes.IMAGE, tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'icon_highlight': TunableResourceKey(description=" \n        Icon to be displayed for when Mood Type is the Sim's active mood.\n        ", default=None, resource_types=sims4.resources.CompoundTypes.IMAGE, tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'ui_sort_order': Tunable(description='\n        Order buff should be sorted in UI.\n        ', tunable_type=int, tuning_group=GroupNames.UI, default=1, export_modes=ExportModes.All), 'visible': Tunable(description='\n        Whether this buff should be visible in the UI.\n        ', tunable_type=bool, default=True, tuning_group=GroupNames.UI), 'audio_sting_on_remove': TunableResourceKey(description='\n        The sound to play when this buff is removed.\n        ', default=None, resource_types=(sims4.resources.Types.PROPX,), export_modes=ExportModes.All), 'audio_sting_on_add': TunableResourceKey(description='\n        The sound to play when this buff is added.\n        ', default=None, resource_types=(sims4.resources.Types.PROPX,), export_modes=ExportModes.All), 'show_timeout': Tunable(description='\n        Whether timeout should be shown in the UI.\n        ', tunable_type=bool, default=True, tuning_group=GroupNames.UI), 'success_modifier': Tunable(description='\n        Base chance delta for interaction success\n        ', tunable_type=int, default=0), 'interactions': OptionalTunable(TunableTuple(weight=Tunable(description='\n            The selection weight to apply to all interactions added by this\n            buff. This takes the place of the SI weight that would be used on\n            SuperInteractions.\n            ', tunable_type=float, default=1), scored_commodity=statistics.commodity.Commodity.TunableReference(description="\n            The commodity that is scored when deciding whether or not to \n            perform these interactions.  This takes the place of the commodity\n            scoring for the SuperInteraction when Subaction Autonomy scores\n            all of the SI's in the SI State.  If this is None, the default \n            value of autonomy.autonomy_modes.SUBACTION_MOTIVE_UTILITY_FALLBACK_SCORE \n            will be used.\n            "), interaction_items=TunableAffordanceLinkList(description='\n            Mixer interactions to add to the Sim when this buff is active.\n            ', class_restrictions=(interactions.base.mixer_interaction.MixerInteraction,))), tuning_group=GroupNames.ANIMATION), 'topics': TunableList(description='\n        Topics that should be added to sim when buff is added.\n        ', tunable=TunableReference(manager=services.topic_manager(), class_restrictions=topics.topic.Topic)), 'game_effect_modifiers': GameEffectModifiers.TunableFactory(description="\n        A list of effects that that can modify a Sim's behavior.\n        "), 'mood_type': TunableReference(description='\n        The mood that this buff pushes onto the owning Sim. If None, does\n        not affect mood.\n        ', manager=services.mood_manager(), needs_tuning=True, export_modes=ExportModes.All), 'mood_weight': TunableRange(description='\n        Weight for this mood. The active mood is determined by summing all\n        buffs and choosing the mood with the largest weight.\n        ', tunable_type=int, default=0, minimum=0, export_modes=ExportModes.All), 'proximity_detection_tests': OptionalTunable(description="\n        Whether or not this buff should be added because of a Sim's proximity\n        to an object with a Proximity Component with this buff in its buffs\n        list.\n        ", tunable=event_testing.tests.TunableTestSet(description="\n            A list of tests groups. At least one must pass all its sub-tests to\n            pass the TestSet.\n            \n            Actor is the one who receives the buff.\n            \n            If this buff is for two Sims in proximity to each other, only Actor\n            and TargetSim should be tuned as Participant Types. Example: A Neat\n            Sim is disgusted when around a Sim that has low hygiene. The test\n            will be for Actor having the Neat trait and for TargetSim with low\n            hygiene motive.\n\n            If this buff is for a Sim near an object, only use participant\n            types Actor and Object. Example: A Sim who likes classical music\n            should get a buff when near a stereo that's playing classical\n            music. The test will be for Actor liking classical music and for\n            Object in the state of playing classical music.\n            "), enabled_by_default=False, disabled_name='no_proximity_detection', enabled_name='proximity_tests'), 'proximity_buff_added_reason': OptionalTunable(tunable=TunableLocalizedString(description="\n            If this is a proximity buff, this field will be the reason for why\n            the Sim received this buff. Doesn't use tokens.\n            "), enabled_by_default=False, disabled_name='no_proximity_add_reason', enabled_name='proximity_add_reason'), '_add_test_set': OptionalTunable(description='\n        Whether or not this buff should be added.\n        ', tunable=event_testing.tests.TunableTestSet(description='\n            A list of tests groups. At least one must pass all its sub-tests to\n            pass the TestSet. Only Actor should be tuned as Participant\n            Types.The Actor is the Sim that will receive the buff if all tests\n            pass."\n            '), enabled_by_default=False, disabled_name='always_allowed', enabled_name='tests_set'), 'walkstyle': OptionalTunable(TunableWalkstyle(description="\n        A walkstyle override to apply to the Sim while this buff is active.\n        Example: you can have Sims with the 'bummed' buff walk in a sad\n        fashion.\n        "), needs_tuning=True, tuning_group=GroupNames.ANIMATION), 'allow_running_for_long_distance_routes': Tunable(bool, True, description='\n        Sims will run when routing long distances outside. Setting this to False\n        will disable that functionality. Example: pregnant Sims and walk-by Sims\n        should probably never run for this reason.'), 'vfx': OptionalTunable(description='\n        vfx to play on the sim when buff is added.\n        ', tunable=PlayEffect.TunableFactory(), disabled_name='no_effect', enabled_name='play_effect', tuning_group=GroupNames.ANIMATION), 'static_commodity_to_add': TunableSet(description='\n        Static commodity that is added to the sim when buff is added to sim.\n        ', tunable=TunableReference(manager=services.static_commodity_manager(), class_restrictions=statistics.static_commodity.StaticCommodity)), '_operating_commodity': statistics.commodity.Commodity.TunableReference(description='\n        This is the commodity that is considered the owning commodity of the\n        buff.  Multiple commodities can reference the same buff.  This field\n        is used to determine which commodity is considered the authoritative\n        commodity.  This only needs to be filled if there are more than one\n        commodity referencing this buff.\n        \n        For example, motive_hunger and starvation_commodity both reference\n        the same buff.  Starvation commodity is marked as the operating\n        commodity.  If outcome action asks the buff what commodity it should\n        apply changes to it will modify the starvation commodity.\n        '), '_temporary_commodity_info': OptionalTunable(TunableTuple(description='\n        Tunables relating to the generation of a temporary commodity to control\n        the lifetime of this buff.  If enabled, this buff has no associated\n        commodities and will create its own to manage its lifetime.\n        ', max_duration=Tunable(description='\n            The maximum time buff can last for.  Example if set to 100, buff\n            only last at max 100 sim minutes.  If washing hands gives +10 sim\n            minutes for buff. Trying to run interaction for more than 10 times,\n            buff time will not increase\n            ', tunable_type=int, default=100), categories=TunableSet(description='\n                List of categories that this commodity is part of. Used for buff\n                removal by category.\n                ', tunable=StatisticCategory, needs_tuning=True))), '_appropriateness_tags': TunableSet(description='\n            A set of tags that define the appropriateness of the\n            interactions allowed by this buff.  All SIs are allowed by\n            default, so adding this tag generally implies that it is always\n            allowed even if another buff has said that it is\n            inappropriate.\n            ', tunable=TunableEnumEntry(tunable_type=Tag, default=Tag.INVALID)), '_inappropriateness_tags': TunableSet(description="\n            A set of tags that define the inappropriateness of the\n            interactions allowed by this buff.  All SIs are allowed by\n            default, so adding this tag generally implies that it's not\n            allowed.\n            ", tunable=TunableEnumEntry(tunable_type=Tag, default=Tag.INVALID)), 'communicable': OptionalTunable(tunable=LootActions.TunableReference(description='\n            The loot to give to Sims that this Sim interacts with while the buff is active.\n            This models transmitting the buff, so make sure to tune a percentage chance\n            on the loot action to determine the chance of the buff being transmitted.\n            ')), '_add_buff_on_remove': OptionalTunable(tunable=TunableBuffReference(description='\n            A buff to add to the Sim when this buff is removed.\n            ')), '_loot_on_addition': TunableList(description='\n        Loot that will be applied when buff is added to sim.\n        ', tunable=LootActions.TunableReference()), '_loot_on_removal': TunableList(description='\n        Loot that will be applied when buff is removed from sim.\n        ', tunable=LootActions.TunableReference()), 'refresh_on_add': Tunable(description='\n        This buff will have its duration refreshed if it gets added to a Sim\n        who already has the same buff.\n        ', tunable_type=bool, needs_tuning=True, default=True), 'flip_arrow_for_progress_update': Tunable(description='\n        This only for visible buffs with an owning commodity.\n        \n        If unchecked and owning commodity is increasing an up arrow will\n        appear on the buff and if owning commodity is decreasing a down arrow\n        will appear.\n        \n        If checked and owning commodity is increasing then a down arrow will\n        appear on the buff and if owning commodity is decreasing an up arrow\n        will appear.\n        \n        Example of being checked is motive failing buffs, when the commodity is\n        increasing we need to show down arrows for the buff.\n        ', tunable_type=bool, default=False, tuning_group=GroupNames.UI), 'timeout_string': TunableLocalizedStringFactory(description='\n        String to override the the timeout text. The first token (0.TimeSpan)\n        will be the timeout time and the second token will (1.String) will be\n        the  buff this buff is transitioning to.\n        \n        If this buff is not transitioning to another buff the only token valid\n        in string is 0.Timespan\n        \n        Example: If this is the hungry buff, then the commodity is decaying to\n        starving buff. Normally timeout in tooltip will say \'5 hours\'. With\n        this set it will pass in the next buff name as the first token into\n        this localized string.  So if string provided is \'Becomes {1.String}\n        in: {0.TimeSpan}. Timeout tooltip for buff now says \'Becomes Starving\n        in: 5 hours\'.\n        \n        Example: If buff is NOT transitioning into another buff. Localized\n        string could be "Great time for :{0.Timespan}". Buff will now say\n        "Great time for : 5 hours"\n        ', tuning_group=GroupNames.UI, export_modes=(ExportModes.ClientBinary,))}
    is_mood_buff = False
    exclusive_index = None
    exclusive_weight = None
    trait_replacement_buffs = None
    _owning_commodity = None

    @classmethod
    def _verify_tuning_callback(cls):
        if cls.visible and not cls.mood_type:
            logger.error('No mood type set for visible buff: {}.  Either provide a mood or make buff invisible.', cls, owner='Tuning')

    @classmethod
    def _tuning_loaded_callback(cls):
        if cls._temporary_commodity_info is not None:
            if cls._owning_commodity is None:
                cls._create_temporary_commodity()
            elif issubclass(cls._owning_commodity, RuntimeCommodity):
                cls._create_temporary_commodity(proxied_commodity=cls._owning_commodity)

    def __init__(self, owner, commodity_guid, replacing_buff_type, transition_into_buff_id):
        self._owner = owner
        self.commodity_guid = commodity_guid
        self.effect_modification = self.game_effect_modifiers(owner)
        self.buff_reason = None
        self.handle_ids = []
        self._static_commodites_added = None
        self._replacing_buff_type = replacing_buff_type
        self._mood_override = None
        self._vfx = None
        self.transition_into_buff_id = transition_into_buff_id
        self._walkstyle_active = False

    @classmethod
    def _cls_repr(cls):
        return '{}'.format(cls.__name__)

    @classmethod
    def can_add(cls, owner):
        if cls._add_test_set is not None:
            resolver = event_testing.resolver.SingleSimResolver(owner)
            result = cls._add_test_set.run_tests(resolver)
            if not result:
                return False
        return True

    @classproperty
    def polarity(cls):
        if cls.mood_type is not None:
            return cls.mood_type.buff_polarity
        return BuffPolarity.NEUTRAL

    @classproperty
    def buff_type(cls):
        return cls

    @classproperty
    def get_success_modifier(cls):
        return cls.success_modifier/100

    @classproperty
    def is_changeable(cls):
        if cls.mood_type is not None:
            return cls.mood_type.is_changeable
        return False

    @classmethod
    def add_owning_commodity(cls, commodity):
        if cls._owning_commodity is None:
            cls._owning_commodity = commodity
        elif cls._operating_commodity is None and cls._owning_commodity is not commodity:
            logger.error('Please fix tuning: Multiple commodities reference {} : commodity:{},  commodity:{}, Set _operating_commodity to authoratative commodity', cls, cls._owning_commodity, commodity)

    @flexproperty
    def commodity(cls, inst):
        if inst is not None and inst._replacing_buff_type is not None:
            return inst._replacing_buff_type.commodity
        return cls._operating_commodity or cls._owning_commodity

    @classmethod
    def build_critical_section(cls, sim, buff_reason, *sequence):
        buff_handler = BuffHandler(sim, cls, buff_reason=buff_reason)
        return build_critical_section_with_finally(buff_handler.begin, sequence, buff_handler.end)

    @classmethod
    def _create_temporary_commodity(cls, proxied_commodity=None, create_buff_state=True, initial_value=DEFAULT):
        if proxied_commodity is None:
            proxied_commodity = RuntimeCommodity.generate(cls.__name__)
        proxied_commodity.decay_rate = 1
        proxied_commodity.convergence_value = 0
        proxied_commodity.remove_on_convergence = True
        proxied_commodity.visible = False
        proxied_commodity.max_value_tuning = cls._temporary_commodity_info.max_duration
        proxied_commodity.min_value_tuning = 0
        proxied_commodity.initial_value = initial_value if initial_value is not DEFAULT else cls._temporary_commodity_info.max_duration
        proxied_commodity._categories = cls._temporary_commodity_info.categories
        proxied_commodity._time_passage_fixup_type = CommodityTimePassageFixupType.FIXUP_USING_TIME_ELAPSED
        if create_buff_state:
            buff_to_add = BuffReference(buff_type=cls)
            new_state_add_buff = CommodityState(value=0.1, buff=buff_to_add)
            new_state_remove_buff = CommodityState(value=0, buff=BuffReference())
            proxied_commodity.commodity_states = [new_state_remove_buff, new_state_add_buff]
        cls.add_owning_commodity(proxied_commodity)

    @classmethod
    def get_appropriateness(cls, tags):
        if cls._appropriateness_tags & tags:
            return Appropriateness.ALLOWED
        if cls._inappropriateness_tags & tags:
            return Appropriateness.NOT_ALLOWED
        return Appropriateness.DONT_CARE

    @property
    def mood_override(self):
        return self._mood_override

    @mood_override.setter
    def mood_override(self, value):
        if not self.is_changeable:
            logger.error('Trying to override mood for buff:{}, but mood for this is not considered changeable.', self, owner='msantander')
        self._mood_override = value

    def on_add(self, load_in_progress):
        self.effect_modification.on_add()
        for topic_type in self.topics:
            self._owner.add_topic(topic_type)
        tracker = self._owner.static_commodity_tracker
        for static_commodity_type in self.static_commodity_to_add:
            tracker.add_statistic(static_commodity_type)
            if self._static_commodites_added is None:
                self._static_commodites_added = []
            self._static_commodites_added.append(static_commodity_type)
        self._apply_walkstyle()
        self.apply_interaction_lockout_to_owner()
        if not load_in_progress:
            sim = self._owner.get_sim_instance(allow_hidden_flags=ALL_HIDDEN_REASONS)
            if sim is not None:
                self._start_vfx()
                if self._loot_on_addition:
                    self._apply_all_loot_actions()

    def _apply_all_loot_actions(self):
        sim = self._owner.get_sim_instance(allow_hidden_flags=ALL_HIDDEN_REASONS)
        if sim is not None:
            resolver = sim.get_resolver()
            for loot_action in self._loot_on_addition:
                loot_action.apply_to_resolver(resolver)

    def on_remove(self, apply_loot_on_remove=True):
        self.effect_modification.on_remove()
        for topic_type in self.topics:
            self._owner.remove_topic(topic_type)
        if self._static_commodites_added is not None:
            tracker = self._owner.static_commodity_tracker
            for static_commodity_type in self._static_commodites_added:
                tracker.remove_statistic(static_commodity_type)
        if self._add_buff_on_remove is not None:
            self._owner.add_buff_from_op(self._add_buff_on_remove.buff_type, self._add_buff_on_remove.buff_reason)
        self._release_walkstyle()
        self.on_sim_removed()
        if apply_loot_on_remove:
            sim = self._owner.get_sim_instance(allow_hidden_flags=ALL_HIDDEN_REASONS)
            if sim is not None:
                resolver = sim.get_resolver()
                while True:
                    for loot_action in self._loot_on_removal:
                        loot_action.apply_to_resolver(resolver)

    def clean_up(self):
        self.effect_modification.on_remove(on_destroy=True)
        self._release_walkstyle()
        self.on_sim_removed()
        if self._static_commodites_added:
            self._static_commodites_added.clear()
            self._static_commodites_added = None

    def on_sim_ready_to_simulate(self):
        for topic_type in self.topics:
            self._owner.add_topic(topic_type)
        self.apply_interaction_lockout_to_owner()
        self._start_vfx()

    def _apply_walkstyle(self):
        if self.walkstyle is not None and not self._walkstyle_active:
            self._owner.request_walkstyle(self.walkstyle, id(self))
            self._walkstyle_active = True

    def _release_walkstyle(self):
        if self._walkstyle_active:
            self._owner.remove_walkstyle(id(self))
            self._walkstyle_active = False

    def on_sim_removed(self, immediate=False):
        if self._vfx is not None:
            self._vfx.stop(immediate=immediate)
            self._vfx = None

    def apply_interaction_lockout_to_owner(self):
        if self.interactions is not None:
            for mixer_affordance in self.interactions.interaction_items:
                while mixer_affordance.lock_out_time_initial is not None:
                    self._owner.set_sub_action_lockout(mixer_affordance, initial_lockout=True)

    def add_handle(self, handle_id, buff_reason=None):
        self.handle_ids.append(handle_id)
        self.buff_reason = buff_reason

    def remove_handle(self, handle_id):
        if handle_id not in self.handle_ids:
            return False
        self.handle_ids.remove(handle_id)
        if self.handle_ids:
            return False
        return True

    def _start_vfx(self):
        if self._vfx is None and self.vfx:
            sim = self._owner.get_sim_instance(allow_hidden_flags=ALL_HIDDEN_REASONS)
            self._vfx = self.vfx(sim)
            self._vfx.start()

    def _get_tracker(self):
        if self.commodity is not None:
            return self._owner.get_tracker(self.commodity)

    def _get_commodity_instance(self):
        if self.commodity is None:
            return
        tracker = self._get_tracker()
        if tracker is None:
            return
        commodity_instance = tracker.get_statistic(self.commodity)
        if commodity_instance is None:
            return
        return commodity_instance

    def _get_absolute_timeout_time(self, commodity_instance, threshold):
        rate_multiplier = commodity_instance.get_decay_rate_modifier()
        if rate_multiplier < 1:
            time = commodity_instance.get_decay_time(threshold)
            rate_multiplier = 1
        else:
            time = commodity_instance.get_decay_time(threshold, use_decay_modifier=False)
        if time is not None and time != 0:
            time_now = services.time_service().sim_now
            time_stamp = time_now + interval_in_sim_minutes(time)
            return (time_stamp.absolute_ticks(), rate_multiplier)
        return NO_TIMEOUT

    def get_timeout_time(self):
        commodity_instance = self._get_commodity_instance()
        if commodity_instance is None:
            return NO_TIMEOUT
        buff_type = self.buff_type
        if self._replacing_buff_type is not None:
            buff_type = self._replacing_buff_type
        else:
            buff_type = self.buff_type
        state_index = commodity_instance.get_state_index_matches_buff_type(buff_type)
        if state_index is None:
            return NO_TIMEOUT
        state_lower_bound_value = commodity_instance.commodity_states[state_index].value
        if commodity_instance.convergence_value <= state_lower_bound_value:
            threshold_value = state_lower_bound_value
            comparison = operator.le
        else:
            comparison = operator.ge
            next_state_index = state_index + 1
            if next_state_index >= len(commodity_instance.commodity_states):
                threshold_value = commodity_instance.convergence_value
            else:
                threshold_value = commodity_instance.commodity_states[next_state_index].value
        threshold = sims4.math.Threshold(threshold_value, comparison)
        return self._get_absolute_timeout_time(commodity_instance, threshold)
Exemple #7
0
class BuffTransferOp(BaseTargetedLootOperation):
    FACTORY_TUNABLES = {
        'moods_only':
        Tunable(
            description=
            '\n            Checking this box will limit the operations to only the buffs with\n            an associated mood.\n            ',
            tunable_type=bool,
            default=True),
        'buff_reason':
        OptionalTunable(
            description=
            '\n            If set, specify a reason why the buff was added.\n            ',
            tunable=TunableLocalizedString(
                description=
                '\n                The reason the buff was added. This will be displayed in the\n                buff tooltip.\n                '
            )),
        'mood_types':
        OptionalTunable(
            TunableList(
                description=
                '\n                If enabled, only transfer buffs with associated moods in this list.\n                ',
                tunable=TunableReference(manager=services.mood_manager()))),
        'polarity':
        OptionalTunable(
            TunableEnumEntry(
                description=
                '\n                If enabled, only transfer buffs that match the selected polarity.\n                ',
                tunable_type=BuffPolarity,
                default=BuffPolarity.NEUTRAL,
                tuning_group=GroupNames.UI))
    }

    def __init__(self,
                 moods_only,
                 buff_reason,
                 mood_types=None,
                 polarity=None,
                 **kwargs):
        super().__init__(**kwargs)
        self._moods_only = moods_only
        self._buff_reason = buff_reason
        self._mood_types = mood_types
        self._polarity = polarity

    def _apply_to_subject_and_target(self, subject, target, resolver):
        old_buff_types = list(subject.get_active_buff_types())
        if self._moods_only:
            for buff_entry in old_buff_types:
                if buff_entry.mood_type is not None:
                    subject.remove_buff_by_type(buff_entry)
        else:
            for buff_entry in old_buff_types:
                subject.remove_buff_by_type(buff_entry)
        for target_buff in target.get_active_buff_types():
            if self._moods_only and target_buff.mood_type is None:
                continue
            if self._mood_types is not None and target_buff.mood_type not in self._mood_types:
                continue
            if self._polarity is not None and self._polarity is not target_buff.polarity:
                continue
            buff_commodity = target_buff.commodity
            subject.add_buff(target_buff)
            if buff_commodity is not None:
                tracker = subject.get_tracker(buff_commodity)
                tracker.set_max(buff_commodity)
                subject.set_buff_reason(target_buff, self._buff_reason)
Exemple #8
0
class BuffComponent(objects.components.Component, component_name=objects.components.types.BUFF_COMPONENT):
    __qualname__ = 'BuffComponent'
    DEFAULT_MOOD = TunableReference(services.mood_manager(), description='The default initial mood.')
    UPDATE_INTENSITY_BUFFER = TunableRange(description="\n        A buffer that prevents a mood from becoming active unless its intensity\n        is greater than the current active mood's intensity plus this amount.\n        \n        For example, if this tunable is 1, and the Sim is in a Flirty mood with\n        intensity 2, then a different mood would become the active mood only if\n        its intensity is 3+.\n        \n        If the predominant mood has an intensity that is less than the active\n        mood's intensity, that mood will become the active mood.\n        ", tunable_type=int, default=1, minimum=0)
    EXCLUSIVE_SET = TunableList(description='\n        A list of buff groups to determine which buffs are exclusive from each\n        other within the same group.  A buff cannot exist in more than one exclusive group.\n        \n        The following rule of exclusivity for a group:\n        1. Higher weight will always be added and remove any lower weight buffs\n        2. Lower weight buff will not be added if a higher weight already exist in component\n        3. Same weight buff will always be added and remove any buff with same weight.\n        \n        Example: Group 1:\n                    Buff1 with weight of 5 \n                    Buff2 with weight of 1\n                    Buff3 with weight of 1\n                 Group 2:\n                    Buff4 with weight of 6\n        \n        If sim has Buff1, trying to add Buff2 or Buff3 will not be added.\n        If sim has Buff2, trying to add Buff3 will remove Buff2 and add Buff3\n        If sim has Buff2, trying to add Buff1 will remove Buff 2 and add Buff3\n        If sim has Buff4, trying to add Buff1, Buff2, or Buff3 will be added and Buff4 will stay \n                          on component \n        ', tunable=TunableList(tunable=TunableTuple(buff_type=TunableReference(description='\n                    Buff in exclusive group\n                    ', manager=services.get_instance_manager(sims4.resources.Types.BUFF)), weight=Tunable(description='\n                    weight to determine if this buff should be added and\n                    remove other buffs in the exclusive group or not added at all.\n                    \n                    Example: Buff1 with weight of 5 \n                             Buff2 with weight of 1\n                             Buff3 with weight of 1\n                    \n                    If sim has Buff1, trying to add Buff2 or Buff3 will not be added.\n                    If sim has Buff2, trying to add Buff3 will remove Buff2 and add Buff3\n                    if sim has Buff2, trying to add Buff1 will remove Buff 2 and add Buff3\n                    ', tunable_type=int, default=1))))

    def __init__(self, owner):
        super().__init__(owner)
        self._active_buffs = {}
        self._get_next_handle_id = UniqueIdGenerator()
        self._success_chance_modification = 0
        self._active_mood = self.DEFAULT_MOOD
        self._active_mood_intensity = 0
        self._active_mood_buff_handle = None
        self.on_mood_changed = CallableList()
        self.on_mood_changed.append(self._publish_mood_update)
        self.on_mood_changed.append(self._send_mood_changed_event)
        self.load_in_progress = False
        self.on_buff_added = CallableList()
        self.on_buff_removed = CallableList()
        self.buff_update_alarms = {}
        if self._active_mood is None:
            logger.error('No default mood tuned in buff_component.py')
        elif self._active_mood.buffs:
            initial_buff_ref = self._active_mood.buffs[0]
            if initial_buff_ref and initial_buff_ref.buff_type:
                self._active_mood_buff_handle = self.add_buff(initial_buff_ref.buff_type)

    def __iter__(self):
        return self._active_buffs.values().__iter__()

    def __len__(self):
        return len(self._active_buffs)

    def on_sim_ready_to_simulate(self):
        for buff in self:
            buff.on_sim_ready_to_simulate()
        self._publish_mood_update()

    def on_sim_removed(self, *args, **kwargs):
        for buff in self:
            buff.on_sim_removed(*args, **kwargs)

    def clean_up(self):
        for (buff_type, buff_entry) in tuple(self._active_buffs.items()):
            self.remove_auto_update(buff_type)
            buff_entry.clean_up()
        self._active_buffs.clear()
        self.on_mood_changed.clear()
        self.on_buff_added.clear()
        self.on_buff_removed.clear()

    @objects.components.componentmethod
    def add_buff_from_op(self, buff_type, buff_reason=None):
        (can_add, _) = self._can_add_buff_type(buff_type)
        if not can_add:
            return False
        buff_commodity = buff_type.commodity
        if buff_commodity is not None:
            if not buff_type.refresh_on_add and self.has_buff(buff_type):
                return False
            tracker = self.owner.get_tracker(buff_commodity)
            if buff_commodity.convergence_value == buff_commodity.max_value:
                tracker.set_min(buff_commodity)
            else:
                tracker.set_max(buff_commodity)
            self.set_buff_reason(buff_type, buff_reason, use_replacement=True)
        else:
            self.add_buff(buff_type, buff_reason=buff_reason)
        return True

    @objects.components.componentmethod
    def add_buff(self, buff_type, buff_reason=None, update_mood=True, commodity_guid=None, replacing_buff=None, timeout_string=None, transition_into_buff_id=0, change_rate=None, immediate=False):
        replacement_buff_type = self._get_replacement_buff_type(buff_type)
        if replacement_buff_type is not None:
            return self.owner.add_buff(replacement_buff_type, buff_reason=buff_reason, update_mood=update_mood, commodity_guid=commodity_guid, replacing_buff=buff_type, timeout_string=timeout_string, transition_into_buff_id=transition_into_buff_id, change_rate=change_rate, immediate=immediate)
        (can_add, conflicting_buff_type) = self._can_add_buff_type(buff_type)
        if not can_add:
            return
        buff = self._active_buffs.get(buff_type)
        if buff is None:
            buff = buff_type(self.owner, commodity_guid, replacing_buff, transition_into_buff_id)
            self._active_buffs[buff_type] = buff
            buff.on_add(self.load_in_progress)
            self._update_chance_modifier()
            if update_mood:
                self._update_current_mood()
            if self.owner.household is not None:
                services.get_event_manager().process_event(test_events.TestEvent.BuffBeganEvent, sim_info=self.owner, sim_id=self.owner.sim_id, buff=buff_type)
                self.register_auto_update(self.owner, buff_type)
            self.on_buff_added(buff_type)
        handle_id = self._get_next_handle_id()
        buff.add_handle(handle_id, buff_reason=buff_reason)
        self.send_buff_update_msg(buff, True, change_rate=change_rate, immediate=immediate)
        if conflicting_buff_type is not None:
            self.remove_buff_by_type(conflicting_buff_type)
        return handle_id

    def _get_replacement_buff_type(self, buff_type):
        if buff_type.trait_replacement_buffs is not None:
            trait_tracker = self.owner.trait_tracker
            for (trait, replacement_buff_type) in buff_type.trait_replacement_buffs.items():
                while trait_tracker.has_trait(trait):
                    return replacement_buff_type

    def register_auto_update(self, sim_info_in, buff_type_in):
        if buff_type_in in self.buff_update_alarms:
            self.remove_auto_update(buff_type_in)
        if sim_info_in.is_selectable and buff_type_in.visible:
            self.buff_update_alarms[buff_type_in] = alarms.add_alarm(self, create_time_span(minutes=15), lambda _, sim_info=sim_info_in, buff_type=buff_type_in: services.get_event_manager().process_event(test_events.TestEvent.BuffUpdateEvent, sim_info=sim_info, sim_id=sim_info.sim_id, buff=buff_type), True)

    def remove_auto_update(self, buff_type):
        if buff_type in self.buff_update_alarms:
            alarms.cancel_alarm(self.buff_update_alarms[buff_type])
            del self.buff_update_alarms[buff_type]

    @objects.components.componentmethod
    def remove_buff(self, handle_id, update_mood=True, immediate=False, on_destroy=False):
        for (buff_type, buff_entry) in self._active_buffs.items():
            while handle_id in buff_entry.handle_ids:
                should_remove = buff_entry.remove_handle(handle_id)
                if should_remove:
                    del self._active_buffs[buff_type]
                    buff_entry.on_remove(not self.load_in_progress and not on_destroy)
                    if not on_destroy:
                        if update_mood:
                            self._update_current_mood()
                        self._update_chance_modifier()
                        self.send_buff_update_msg(buff_entry, False, immediate=immediate)
                        services.get_event_manager().process_event(test_events.TestEvent.BuffEndedEvent, sim_info=self.owner, sim_id=self.owner.sim_id, buff=buff_type)
                    if buff_type in self.buff_update_alarms:
                        self.remove_auto_update(buff_type)
                    self.on_buff_removed(buff_type)
                break

    @objects.components.componentmethod
    def get_buff_type(self, handle_id):
        for (buff_type, buff_entry) in self._active_buffs.items():
            while handle_id in buff_entry.handle_ids:
                return buff_type

    @objects.components.componentmethod
    def has_buff(self, buff_type):
        return buff_type in self._active_buffs

    @objects.components.componentmethod
    def get_active_buff_types(self):
        return self._active_buffs.keys()

    @objects.components.componentmethod
    def get_buff_reason(self, handle_id):
        for buff_entry in self._active_buffs.values():
            while handle_id in buff_entry.handle_ids:
                return buff_entry.buff_reason

    @objects.components.componentmethod
    def debug_add_buff_by_type(self, buff_type):
        (can_add, conflicting_buff_type) = self._can_add_buff_type(buff_type)
        if not can_add:
            return False
        if buff_type.commodity is not None:
            tracker = self.owner.get_tracker(buff_type.commodity)
            state_index = buff_type.commodity.get_state_index_matches_buff_type(buff_type)
            if state_index is not None:
                index = state_index + 1
                if index < len(buff_type.commodity.commodity_states):
                    commodity_to_value = buff_type.commodity.commodity_states[index].value - 1
                else:
                    commodity_to_value = buff_type.commodity.max_value
                tracker.set_value(buff_type.commodity, commodity_to_value)
            else:
                logger.error('commodity ({}) has no states with buff ({}), Buff will not be added.', buff_type.commodity, buff_type)
                return False
        else:
            self.add_buff(buff_type)
        if conflicting_buff_type is not None:
            self.remove_buff_by_type(conflicting_buff_type)
        return True

    @objects.components.componentmethod
    def remove_buff_by_type(self, buff_type, on_destroy=False):
        buff_entry = self._active_buffs.get(buff_type)
        self.remove_buff_entry(buff_entry, on_destroy=on_destroy)

    @objects.components.componentmethod
    def remove_buff_entry(self, buff_entry, on_destroy=False):
        if buff_entry is not None:
            if buff_entry.commodity is not None:
                tracker = self.owner.get_tracker(buff_entry.commodity)
                commodity_inst = tracker.get_statistic(buff_entry.commodity)
                if commodity_inst is not None and commodity_inst.core:
                    if not on_destroy:
                        logger.callstack('Attempting to explicitly remove the buff {}, which is given by a core commodity.                                           This would result in the removal of a core commodity and will be ignored.', buff_entry, owner='tastle', level=sims4.log.LEVEL_ERROR)
                    return
                tracker.remove_statistic(buff_entry.commodity, on_destroy=on_destroy)
            elif buff_entry.buff_type in self._active_buffs:
                buff_entry.on_remove(on_destroy)
                del self._active_buffs[buff_entry.buff_type]
                if not on_destroy:
                    self._update_chance_modifier()
                    self._update_current_mood()
                    self.send_buff_update_msg(buff_entry, False)
                    services.get_event_manager().process_event(test_events.TestEvent.BuffEndedEvent, sim_info=self.owner, buff=type(buff_entry), sim_id=self.owner.id)

    @objects.components.componentmethod
    def set_buff_reason(self, buff_type, buff_reason, use_replacement=False):
        if use_replacement:
            replacement_buff_type = self._get_replacement_buff_type(buff_type)
            if replacement_buff_type is not None:
                buff_type = replacement_buff_type
        buff_entry = self._active_buffs.get(buff_type)
        if buff_entry is not None and buff_reason is not None:
            buff_entry.buff_reason = buff_reason
            self.send_buff_update_msg(buff_entry, True)

    @objects.components.componentmethod
    def buff_commodity_changed(self, handle_id, change_rate=None):
        for (_, buff_entry) in self._active_buffs.items():
            while handle_id in buff_entry.handle_ids:
                if buff_entry.show_timeout:
                    self.send_buff_update_msg(buff_entry, True, change_rate=change_rate)
                break

    @objects.components.componentmethod
    def get_success_chance_modifier(self):
        return self._success_chance_modification

    @objects.components.componentmethod
    def get_actor_scoring_modifier(self, affordance):
        total = 0
        for buff_entry in self._active_buffs.values():
            total += buff_entry.effect_modification.get_affordance_scoring_modifier(affordance)
        return total

    @objects.components.componentmethod
    def get_actor_success_modifier(self, affordance):
        total = 0
        for buff_entry in self._active_buffs.values():
            total += buff_entry.effect_modification.get_affordance_success_modifier(affordance)
        return total

    @objects.components.componentmethod
    def get_mood(self):
        return self._active_mood

    @objects.components.componentmethod
    def get_mood_animation_param_name(self):
        param_name = self._active_mood.asm_param_name
        if param_name is not None:
            return param_name
        (mood, _, _) = self._get_largest_mood(predicate=lambda mood: return True if mood.asm_param_name else False)
        return mood.asm_param_name

    @objects.components.componentmethod
    def get_mood_intensity(self):
        return self._active_mood_intensity

    @objects.components.componentmethod
    def get_effective_skill_level(self, skill):
        if skill.stat_type == skill:
            skill = self.owner.get_stat_instance(skill)
            if skill is None:
                return 0
        modifier = 0
        for buff_entry in self._active_buffs.values():
            modifier += buff_entry.effect_modification.get_effective_skill_modifier(skill)
        return skill.get_user_value() + modifier

    @objects.components.componentmethod
    def effective_skill_modified_buff_gen(self, skill):
        if skill.stat_type == skill:
            skill = self.owner.get_stat_instance(skill)
        for buff_entry in self._active_buffs.values():
            modifier = buff_entry.effect_modification.get_effective_skill_modifier(skill)
            while modifier != 0:
                yield (buff_entry, modifier)

    @objects.components.componentmethod
    def is_appropriate(self, tags):
        final_appropriateness = Appropriateness.DONT_CARE
        for buff in self._active_buffs:
            appropriateness = buff.get_appropriateness(tags)
            while appropriateness > final_appropriateness:
                final_appropriateness = appropriateness
        if final_appropriateness == Appropriateness.NOT_ALLOWED:
            return False
        return True

    def get_additional_create_ops_gen(self):
        yield GenericProtocolBufferOp(Operation.SIM_MOOD_UPDATE, self._create_mood_update_msg())
        for buff in self:
            while buff.visible:
                yield GenericProtocolBufferOp(Operation.SIM_BUFF_UPDATE, self._create_buff_update_msg(buff, True))

    def _publish_mood_update(self):
        if self.owner.valid_for_distribution and self.owner.visible_to_client == True:
            Distributor.instance().add_op(self.owner, GenericProtocolBufferOp(Operation.SIM_MOOD_UPDATE, self._create_mood_update_msg()))

    def _send_mood_changed_event(self):
        if not self.owner.is_npc:
            self.owner.whim_tracker.refresh_emotion_whim()
        services.get_event_manager().process_event(test_events.TestEvent.MoodChange, sim_info=self.owner)

    def _create_mood_update_msg(self):
        mood_msg = Commodities_pb2.MoodUpdate()
        mood_msg.sim_id = self.owner.id
        mood_msg.mood_key = self._active_mood.guid64
        mood_msg.mood_intensity = self._active_mood_intensity
        return mood_msg

    def _create_buff_update_msg(self, buff, equipped, change_rate=None):
        buff_msg = Sims_pb2.BuffUpdate()
        buff_msg.buff_id = buff.guid64
        buff_msg.sim_id = self.owner.id
        buff_msg.equipped = equipped
        if buff.buff_reason is not None:
            buff_msg.reason = buff.buff_reason
        if equipped and buff.show_timeout:
            (timeout, rate_multiplier) = buff.get_timeout_time()
            buff_msg.timeout = timeout
            buff_msg.rate_multiplier = rate_multiplier
            if change_rate is not None:
                if change_rate == 0:
                    progress_arrow = Sims_pb2.BUFF_PROGRESS_NONE
                elif change_rate > 0:
                    progress_arrow = Sims_pb2.BUFF_PROGRESS_UP if not buff.flip_arrow_for_progress_update else Sims_pb2.BUFF_PROGRESS_DOWN
                else:
                    progress_arrow = Sims_pb2.BUFF_PROGRESS_DOWN if not buff.flip_arrow_for_progress_update else Sims_pb2.BUFF_PROGRESS_UP
                buff_msg.buff_progress = progress_arrow
        buff_msg.is_mood_buff = buff.is_mood_buff
        buff_msg.commodity_guid = buff.commodity_guid or 0
        if buff.mood_override is not None:
            buff_msg.mood_type_override = buff.mood_override.guid64
        buff_msg.transition_into_buff_id = buff.transition_into_buff_id
        return buff_msg

    def send_buff_update_msg(self, buff, equipped, change_rate=None, immediate=False):
        if not buff.visible:
            return
        if self.owner.valid_for_distribution and self.owner.is_sim and self.owner.is_selectable:
            buff_msg = self._create_buff_update_msg(buff, equipped, change_rate=change_rate)
            if gsi_handlers.buff_handlers.sim_buff_log_archiver.enabled:
                gsi_handlers.buff_handlers.archive_buff_message(buff_msg, equipped, change_rate)
            Distributor.instance().add_op(self.owner, GenericProtocolBufferOp(Operation.SIM_BUFF_UPDATE, buff_msg))

    def _can_add_buff_type(self, buff_type):
        if not buff_type.can_add(self.owner):
            return (False, None)
        mood = buff_type.mood_type
        if mood is not None and mood.excluding_traits is not None and self.owner.trait_tracker.has_any_trait(mood.excluding_traits):
            return (False, None)
        if buff_type.exclusive_index is None:
            return (True, None)
        for conflicting_buff_type in self._active_buffs:
            while conflicting_buff_type.exclusive_index == buff_type.exclusive_index:
                if buff_type.exclusive_weight < conflicting_buff_type.exclusive_weight:
                    return (False, None)
                return (True, conflicting_buff_type)
        return (True, None)

    def _update_chance_modifier(self):
        positive_success_buff_delta = 0
        negative_success_buff_delta = 1
        for buff_entry in self._active_buffs.values():
            if buff_entry.success_modifier > 0:
                positive_success_buff_delta += buff_entry.get_success_modifier
            else:
                negative_success_buff_delta *= 1 + buff_entry.get_success_modifier
        self._success_chance_modification = positive_success_buff_delta - (1 - negative_success_buff_delta)

    def _get_largest_mood(self, predicate=None, buffs_to_ignore=()):
        weights = {}
        polarity_to_changeable_buffs = collections.defaultdict(list)
        polarity_to_largest_mood_and_weight = {}
        for buff_entry in self._active_buffs.values():
            current_mood = buff_entry.mood_type
            current_weight = buff_entry.mood_weight
            while not current_mood is None:
                if current_weight == 0:
                    pass
                if not (predicate is not None and predicate(current_mood)):
                    pass
                if buff_entry in buffs_to_ignore:
                    pass
                current_polarity = current_mood.buff_polarity
                if buff_entry.is_changeable:
                    polarity_to_changeable_buffs[current_polarity].append(buff_entry)
                total_current_weight = weights.get(current_mood, 0)
                total_current_weight += current_weight
                weights[current_mood] = total_current_weight
                (largest_mood, largest_weight) = polarity_to_largest_mood_and_weight.get(current_polarity, (None, None))
                if largest_mood is None:
                    polarity_to_largest_mood_and_weight[current_polarity] = (current_mood, total_current_weight)
                else:
                    while total_current_weight > largest_weight:
                        polarity_to_largest_mood_and_weight[current_polarity] = (current_mood, total_current_weight)
        all_changeable_buffs = []
        for (buff_polarity, changeable_buffs) in polarity_to_changeable_buffs.items():
            (largest_mood, largest_weight) = polarity_to_largest_mood_and_weight.get(buff_polarity, (None, None))
            if largest_mood is not None:
                for buff_entry in changeable_buffs:
                    if buff_entry.mood_override is not largest_mood:
                        all_changeable_buffs.append((buff_entry, largest_mood))
                    largest_weight += buff_entry.mood_weight
                polarity_to_largest_mood_and_weight[buff_polarity] = (largest_mood, largest_weight)
            else:
                weights = {}
                largest_weight = 0
                for buff_entry in changeable_buffs:
                    if buff_entry.mood_override is not None:
                        all_changeable_buffs.append((buff_entry, None))
                    current_mood = buff_entry.mood_type
                    current_weight = buff_entry.mood_weight
                    total_current_weight = weights.get(current_mood, 0)
                    total_current_weight += current_weight
                    weights[current_mood] = total_current_weight
                    while total_current_weight > largest_weight:
                        largest_weight = total_current_weight
                        largest_mood = current_mood
                while largest_mood is not None and largest_weight != 0:
                    polarity_to_largest_mood_and_weight[buff_polarity] = (largest_mood, largest_weight)
        largest_weight = 0
        largest_mood = self.DEFAULT_MOOD
        active_mood = self._active_mood
        if polarity_to_largest_mood_and_weight:
            (mood, weight) = max(polarity_to_largest_mood_and_weight.values(), key=operator.itemgetter(1))
            if weight > largest_weight or weight == largest_weight and mood is active_mood:
                largest_weight = weight
                largest_mood = mood
        return (largest_mood, largest_weight, all_changeable_buffs)

    def _update_current_mood(self):
        (largest_mood, largest_weight, changeable_buffs) = self._get_largest_mood()
        if largest_mood is not None:
            intensity = self._get_intensity_from_mood(largest_mood, largest_weight)
            if self._should_update_mood(largest_mood, intensity, changeable_buffs):
                if self._active_mood_buff_handle is not None:
                    active_mood_buff_handle = self._active_mood_buff_handle
                    self.remove_buff(active_mood_buff_handle, update_mood=False)
                    if active_mood_buff_handle == self._active_mood_buff_handle:
                        self._active_mood_buff_handle = None
                    else:
                        return
                self._active_mood = largest_mood
                self._active_mood_intensity = intensity
                if len(largest_mood.buffs) >= intensity:
                    tuned_buff = largest_mood.buffs[intensity]
                    if tuned_buff is not None and tuned_buff.buff_type is not None:
                        self._active_mood_buff_handle = self.add_buff(tuned_buff.buff_type, update_mood=False)
                if gsi_handlers.buff_handlers.sim_mood_log_archiver.enabled and self.owner.valid_for_distribution and self.owner.visible_to_client == True:
                    gsi_handlers.buff_handlers.archive_mood_message(self.owner.id, self._active_mood, self._active_mood_intensity, self._active_buffs, changeable_buffs)
                caches.clear_all_caches()
                self.on_mood_changed()
        for (changeable_buff, mood_override) in changeable_buffs:
            changeable_buff.mood_override = mood_override
            self.send_buff_update_msg(changeable_buff, True)

    def _get_intensity_from_mood(self, mood, weight):
        intensity = 0
        for threshold in mood.intensity_thresholds:
            if weight >= threshold:
                intensity += 1
            else:
                break
        return intensity

    def _should_update_mood(self, mood, intensity, changeable_buffs):
        active_mood = self._active_mood
        active_mood_intensity = self._active_mood_intensity
        if mood is active_mood:
            return intensity != active_mood_intensity
        total_weight = sum(buff_entry.mood_weight for buff_entry in self._active_buffs.values() if buff_entry.mood_type is active_mood)
        active_mood_intensity = self._get_intensity_from_mood(active_mood, total_weight)
        if changeable_buffs and not self._active_mood.is_changeable:
            buffs_to_ignore = [changeable_buff for (changeable_buff, _) in changeable_buffs]
            (largest_mood, largest_weight, _) = self._get_largest_mood(buffs_to_ignore=buffs_to_ignore)
            new_intensity = self._get_intensity_from_mood(largest_mood, largest_weight)
            if self._should_update_mood(largest_mood, new_intensity, None):
                active_mood = largest_mood
                active_mood_intensity = new_intensity
        if active_mood.is_changeable and mood.buff_polarity == active_mood.buff_polarity:
            return True
        if not intensity or intensity < active_mood_intensity:
            return True
        if intensity >= active_mood_intensity + self.UPDATE_INTENSITY_BUFFER:
            return True
        if mood is self.DEFAULT_MOOD or active_mood is self.DEFAULT_MOOD:
            return True
        return False
Exemple #9
0
class Trait(HasTunableReference,
            SuperAffordanceProviderMixin,
            TargetSuperAffordanceProviderMixin,
            HasTunableLodMixin,
            MixerActorMixin,
            MixerProviderMixin,
            metaclass=HashedTunedInstanceMetaclass,
            manager=services.trait_manager()):
    EQUIP_SLOT_NUMBER_MAP = TunableMapping(
        description=
        '\n        The number of personality traits available to Sims of specific ages.\n        ',
        key_type=TunableEnumEntry(
            description="\n            The Sim's age.\n            ",
            tunable_type=sim_info_types.Age,
            default=sim_info_types.Age.YOUNGADULT),
        value_type=Tunable(
            description=
            '\n            The number of personality traits available to a Sim of the specified\n            age.\n            ',
            tunable_type=int,
            default=3),
        key_name='Age',
        value_name='Slot Number')
    PERSONALITY_TRAIT_TAG = TunableEnumEntry(
        description=
        '\n        The tag that marks a trait as a personality trait.\n        ',
        tunable_type=tag.Tag,
        default=tag.Tag.INVALID,
        invalid_enums=(tag.Tag.INVALID, ))
    DAY_NIGHT_TRACKING_BUFF_TAG = TunableEnumWithFilter(
        description=
        '\n        The tag that marks buffs as opting in to Day Night Tracking on traits..\n        ',
        tunable_type=tag.Tag,
        filter_prefixes=['buff'],
        default=tag.Tag.INVALID,
        invalid_enums=(tag.Tag.INVALID, ))
    INSTANCE_TUNABLES = {
        'trait_type':
        TunableEnumEntry(
            description='\n            The type of the trait.\n            ',
            tunable_type=TraitType,
            default=TraitType.PERSONALITY,
            export_modes=ExportModes.All,
            tuning_group=GroupNames.APPEARANCE),
        'display_name':
        TunableLocalizedStringFactory(
            description=
            "\n            The trait's display name. This string is provided with the owning\n            Sim as its only token.\n            ",
            allow_none=True,
            export_modes=ExportModes.All,
            tuning_group=GroupNames.APPEARANCE),
        'display_name_gender_neutral':
        TunableLocalizedString(
            description=
            "\n            The trait's gender-neutral display name. This string is not provided\n            any tokens, and thus can't rely on context to properly form\n            masculine and feminine forms.\n            ",
            allow_none=True,
            tuning_group=GroupNames.APPEARANCE),
        'trait_description':
        TunableLocalizedStringFactory(
            description="\n            The trait's description.\n            ",
            allow_none=True,
            export_modes=ExportModes.All,
            tuning_group=GroupNames.APPEARANCE),
        'trait_origin_description':
        TunableLocalizedString(
            description=
            "\n            A description of how the Sim obtained this trait. Can be overloaded\n            for other uses in certain cases:\n            - When the trait type is AGENT this string is the name of the \n                agency's Trade type and will be provided with the owning sim \n                as its token.\n            - When the trait type is HIDDEN and the trait is used by the CAS\n                STORIES flow, this can be used as a secondary description in \n                the CAS Stories UI. If this trait is tagged as a CAREER CAS \n                stories trait, this description will be used to explain which \n                skills are also granted with this career.\n            ",
            allow_none=True,
            export_modes=ExportModes.All,
            tuning_group=GroupNames.APPEARANCE),
        'icon':
        TunableResourceKey(
            description="\n            The trait's icon.\n            ",
            allow_none=True,
            resource_types=CompoundTypes.IMAGE,
            export_modes=ExportModes.All,
            tuning_group=GroupNames.APPEARANCE),
        'pie_menu_icon':
        TunableResourceKey(
            description=
            "\n            The trait's pie menu icon.\n            ",
            resource_types=CompoundTypes.IMAGE,
            default=None,
            allow_none=True,
            tuning_group=GroupNames.APPEARANCE),
        'trait_asm_overrides':
        TunableTuple(
            description=
            '\n            Tunables that will specify if a Trait will add any parameters\n            to the Sim and how it will affect their boundary conditions.\n            ',
            param_type=OptionalTunable(
                description=
                '\n                Define if this trait is parameterized as an on/off value or as\n                part of an enumeration.\n                ',
                tunable=Tunable(
                    description=
                    '\n                    The name of the parameter enumeration. For example, if this\n                    value is tailType, then the tailType actor parameter is set\n                    to the value specified in param_value, for this Sim.\n                    ',
                    tunable_type=str,
                    default=None),
                disabled_name='boolean',
                enabled_name='enum'),
            trait_asm_param=Tunable(
                description=
                "\n                The ASM parameter for this trait. If unset, it will be auto-\n                generated depending on the instance name (e.g. 'trait_Clumsy').\n                ",
                tunable_type=str,
                default=None),
            consider_for_boundary_conditions=Tunable(
                description=
                '\n                If enabled the trait_asm_param will be considered when a Sim\n                is building the goals and validating against its boundary\n                conditions.\n                This should ONLY be enabled, if we need this parameter for\n                cases like a posture transition, or boundary specific cases. \n                On regular cases like an animation outcome, this is not needed.\n                i.e. Vampire trait has an isVampire parameter set to True, so\n                when animatin out of the coffin it does different get in/out \n                animations.  When this is enabled, isVampire will be set to \n                False for every other Sim.\n                ',
                tunable_type=bool,
                default=False),
            tuning_group=GroupNames.ANIMATION),
        'ages':
        TunableSet(
            description=
            '\n            The allowed ages for this trait. If no ages are specified, then all\n            ages are considered valid.\n            ',
            tunable=TunableEnumEntry(tunable_type=Age,
                                     default=None,
                                     export_modes=ExportModes.All),
            tuning_group=GroupNames.AVAILABILITY),
        'genders':
        TunableSet(
            description=
            '\n            The allowed genders for this trait. If no genders are specified,\n            then all genders are considered valid.\n            ',
            tunable=TunableEnumEntry(tunable_type=Gender,
                                     default=None,
                                     export_modes=ExportModes.All),
            tuning_group=GroupNames.AVAILABILITY),
        'species':
        TunableSet(
            description=
            '\n            The allowed species for this trait. If not species are specified,\n            then all species are considered valid.\n            ',
            tunable=TunableEnumEntry(tunable_type=Species,
                                     default=Species.HUMAN,
                                     invalid_enums=(Species.INVALID, ),
                                     export_modes=ExportModes.All),
            tuning_group=GroupNames.AVAILABILITY),
        'conflicting_traits':
        TunableList(
            description=
            '\n            Conflicting traits for this trait. If the Sim has any of the\n            specified traits, then they are not allowed to be equipped with this\n            one.\n            \n            e.g.\n             Family Oriented conflicts with Hates Children, and vice-versa.\n            ',
            tunable=TunableReference(manager=services.trait_manager(),
                                     pack_safe=True),
            export_modes=ExportModes.All,
            tuning_group=GroupNames.AVAILABILITY),
        'is_npc_only':
        Tunable(
            description=
            '\n            If checked, this trait will get removed from Sims that have a home\n            when the zone is loaded or whenever they switch to a household that\n            has a home zone.\n            ',
            tunable_type=bool,
            default=False,
            tuning_group=GroupNames.AVAILABILITY),
        'cas_selected_icon':
        TunableResourceKey(
            description=
            '\n            Icon to be displayed in CAS when this trait has already been applied\n            to a Sim.\n            ',
            resource_types=CompoundTypes.IMAGE,
            default=None,
            allow_none=True,
            export_modes=(ExportModes.ClientBinary, ),
            tuning_group=GroupNames.CAS),
        'cas_idle_asm_key':
        TunableInteractionAsmResourceKey(
            description=
            '\n            The ASM to use for the CAS idle.\n            ',
            default=None,
            allow_none=True,
            category='asm',
            export_modes=ExportModes.All,
            tuning_group=GroupNames.CAS),
        'cas_idle_asm_state':
        Tunable(
            description=
            '\n            The state to play for the CAS idle.\n            ',
            tunable_type=str,
            default=None,
            source_location='cas_idle_asm_key',
            source_query=SourceQueries.ASMState,
            export_modes=ExportModes.All,
            tuning_group=GroupNames.CAS),
        'cas_trait_asm_param':
        Tunable(
            description=
            '\n            The ASM parameter for this trait for use with CAS ASM state machine,\n            driven by selection of this Trait, i.e. when a player selects the a\n            romantic trait, the Flirty ASM is given to the state machine to\n            play. The name tuned here must match the animation state name\n            parameter expected in Swing.\n            ',
            tunable_type=str,
            default=None,
            export_modes=ExportModes.All,
            tuning_group=GroupNames.CAS),
        'tags':
        TunableList(
            description=
            "\n            The associated categories of the trait. Need to distinguish among\n            'Personality Traits', 'Achievement Traits' and 'Walkstyle\n            Traits'.\n            ",
            tunable=TunableEnumEntry(tunable_type=tag.Tag,
                                     default=tag.Tag.INVALID),
            export_modes=ExportModes.All,
            tuning_group=GroupNames.CAS),
        'sim_info_fixup_actions':
        TunableList(
            description=
            '\n            A list of fixup actions which will be performed on a sim_info with\n            this trait when it is loaded.\n            ',
            tunable=TunableVariant(
                career_fixup_action=_SimInfoCareerFixupAction.TunableFactory(
                    description=
                    '\n                    A fix up action to set a career with a specific level.\n                    '
                ),
                skill_fixup_action=_SimInfoSkillFixupAction.TunableFactory(
                    description=
                    '\n                    A fix up action to set a skill with a specific level.\n                    '
                ),
                unlock_fixup_action=_SimInfoUnlockFixupAction.TunableFactory(
                    description=
                    '\n                    A fix up action to unlock certain things for a Sim\n                    '
                ),
                perk_fixup_action=_SimInfoPerkFixupAction.TunableFactory(
                    description=
                    '\n                    A fix up action to grant perks to a Sim. It checks perk required\n                    unlock tuning and unlocks prerequisite perks first.\n                    '
                ),
                default='career_fixup_action'),
            tuning_group=GroupNames.CAS),
        'sim_info_fixup_actions_timing':
        TunableEnumEntry(
            description=
            "\n            This is DEPRECATED, don't tune this field. We usually don't do trait-based\n            fixup unless it's related to CAS stories. We keep this field only for legacy\n            support reason.\n            \n            This is mostly to optimize performance when applying fix-ups to\n            a Sim.  We ideally would not like to spend time scanning every Sim \n            on every load to see if they need fixups.  Please be sure you \n            consult a GPE whenever you are creating fixup tuning.\n            ",
            tunable_type=SimInfoFixupActionTiming,
            default=SimInfoFixupActionTiming.ON_FIRST_SIMINFO_LOAD,
            tuning_group=GroupNames.DEPRECATED,
            deprecated=True),
        'teleport_style_interaction_to_inject':
        TunableReference(
            description=
            '\n             When this trait is added to a Sim, if a teleport style interaction\n             is specified, any time another interaction runs, we may run this\n             teleport style interaction to shorten or replace the route to the \n             target.\n             ',
            manager=services.get_instance_manager(
                sims4.resources.Types.INTERACTION),
            class_restrictions=('TeleportStyleSuperInteraction', ),
            allow_none=True,
            tuning_group=GroupNames.SPECIAL_CASES),
        'interactions':
        OptionalTunable(
            description=
            '\n            Mixer interactions that are available to Sims equipped with this\n            trait.\n            ',
            tunable=ContentSet.TunableFactory(locked_args={
                'phase_affordances': frozendict(),
                'phase_tuning': None
            })),
        'buffs_add_on_spawn_only':
        Tunable(
            description=
            '\n            If unchecked, buffs are added to the Sim as soon as this trait is\n            added. If checked, buffs will be added only when the Sim is\n            instantiated and removed when the Sim uninstantiates.\n            \n            General guidelines: If the buffs only matter to Sims, for example\n            buffs that alter autonomy behavior or walkstyle, this should be\n            checked.\n            ',
            tunable_type=bool,
            default=True),
        'buffs':
        TunableList(
            description=
            '\n            Buffs that should be added to the Sim whenever this trait is\n            equipped.\n            ',
            tunable=TunableBuffReference(pack_safe=True),
            unique_entries=True),
        'buffs_proximity':
        TunableList(
            description=
            '\n            Proximity buffs that are active when this trait is equipped.\n            ',
            tunable=TunableReference(manager=services.buff_manager())),
        'buff_replacements':
        TunableMapping(
            description=
            '\n            A mapping of buff replacement. If Sim has this trait on, whenever he\n            get the buff tuned in the key of the mapping, it will get replaced\n            by the value of the mapping.\n            ',
            key_type=TunableReference(
                description=
                '\n                Buff that will get replaced to apply on Sim by this trait.\n                ',
                manager=services.buff_manager(),
                reload_dependent=True,
                pack_safe=True),
            value_type=TunableTuple(
                description=
                '\n                Data specific to this buff replacement.\n                ',
                buff_type=TunableReference(
                    description=
                    '\n                    Buff used to replace the buff tuned as key.\n                    ',
                    manager=services.buff_manager(),
                    reload_dependent=True,
                    pack_safe=True),
                buff_reason=OptionalTunable(
                    description=
                    '\n                    If enabled, override the buff reason.\n                    ',
                    tunable=TunableLocalizedString(
                        description=
                        '\n                        The overridden buff reason.\n                        '
                    )),
                buff_replacement_priority=TunableEnumEntry(
                    description=
                    "\n                    The priority of this buff replacement, relative to other\n                    replacements. Tune this to be a higher value if you want\n                    this replacement to take precedence.\n                    \n                    e.g.\n                     (NORMAL) trait_HatesChildren (buff_FirstTrimester -> \n                                                   buff_FirstTrimester_HatesChildren)\n                     (HIGH)   trait_Male (buff_FirstTrimester -> \n                                          buff_FirstTrimester_Male)\n                                          \n                     In this case, both traits have overrides on the pregnancy\n                     buffs. However, we don't want males impregnated by aliens\n                     that happen to hate children to lose their alien-specific\n                     buffs. Therefore we tune the male replacement at a higher\n                     priority.\n                    ",
                    tunable_type=TraitBuffReplacementPriority,
                    default=TraitBuffReplacementPriority.NORMAL))),
        'excluded_mood_types':
        TunableList(
            TunableReference(
                description=
                '\n            List of moods that are prevented by having this trait.\n            ',
                manager=services.mood_manager())),
        'outfit_replacements':
        TunableMapping(
            description=
            "\n            A mapping of outfit replacements. If the Sim has this trait, outfit\n            change requests are intercepted to produce the tuned result. If\n            multiple traits with outfit replacements exist, the behavior is\n            undefined.\n            \n            Tuning 'Invalid' as a key acts as a fallback and applies to all\n            reasons.\n            \n            Tuning 'Invalid' as a value keeps a Sim in their current outfit.\n            ",
            key_type=TunableEnumEntry(tunable_type=OutfitChangeReason,
                                      default=OutfitChangeReason.Invalid),
            value_type=TunableEnumEntry(tunable_type=OutfitChangeReason,
                                        default=OutfitChangeReason.Invalid)),
        'disable_aging':
        OptionalTunable(
            description=
            '\n            If enabled, aging out of specific ages can be disabled.\n            ',
            tunable=TunableTuple(
                description=
                '\n                The tuning that disables aging out of specific age groups.\n                ',
                allowed_ages=TunableSet(
                    description=
                    '\n                    A list of ages that the Sim CAN age out of. If an age is in\n                    this list then the Sim is allowed to age out of it. If an\n                    age is not in this list than a Sim is not allowed to age out\n                    of it. For example, if the list only contains Child and\n                    Teen, then a Child Sim would be able to age up to Teen and\n                    a Teen Sim would be able to age up to Young Adult. But, a\n                    Young Adult, Adult, or Elder Sim would not be able to age\n                    up.\n                    ',
                    tunable=TunableEnumEntry(Age, default=Age.ADULT)),
                tooltip=OptionalTunable(
                    description=
                    '\n                    When enabled, this tooltip will be displayed in the aging\n                    progress bar when aging is disabled because of the trait.\n                    ',
                    tunable=TunableLocalizedStringFactory(
                        description=
                        '\n                        The string that displays in the aging UI when aging up\n                        is disabled due to the trait.\n                        '
                    ))),
            tuning_group=GroupNames.SPECIAL_CASES),
        'can_die':
        Tunable(
            description=
            '\n            When set, Sims with this trait are allowed to die. When unset, Sims\n            are prevented from dying.\n            ',
            tunable_type=bool,
            default=True,
            tuning_group=GroupNames.SPECIAL_CASES),
        'culling_behavior':
        TunableVariant(
            description=
            '\n            The culling behavior of a Sim with this trait.\n            ',
            default_behavior=CullingBehaviorDefault.TunableFactory(),
            immune_to_culling=CullingBehaviorImmune.TunableFactory(),
            importance_as_npc_score=CullingBehaviorImportanceAsNpc.
            TunableFactory(),
            default='default_behavior',
            tuning_group=GroupNames.SPECIAL_CASES),
        'always_send_test_event_on_add':
        Tunable(
            description=
            '\n            If checked, will send out a test event when added to a trait\n            tracker even if the receiving sim is hidden or not instanced.\n            ',
            tunable_type=bool,
            default=False,
            tuning_group=GroupNames.SPECIAL_CASES),
        'voice_effect':
        OptionalTunable(
            description=
            '\n            The voice effect of a Sim with this trait. This is prioritized\n            against other traits with voice effects.\n            \n            The Sim may only have one voice effect at a time.\n            ',
            tunable=VoiceEffectRequest.TunableFactory()),
        'plumbbob_override':
        OptionalTunable(
            description=
            '\n            If enabled, allows a new plumbbob model to be used when a Sim has\n            this occult type.\n            ',
            tunable=PlumbbobOverrideRequest.TunableFactory()),
        'vfx_mask':
        OptionalTunable(
            description=
            '\n            If enabled when this trait is added the masks will be applied to\n            the Sim affecting the visibility of specific VFX.\n            Example: TRAIT_CHILDREN will provide a mask MASK_CHILDREN which \n            the monster battle object will only display VFX for any Sim \n            using that mask.\n            ',
            tunable=TunableEnumFlags(
                description=
                "\n                Mask that will be added to the Sim's mask when the trait is\n                added.\n                ",
                enum_type=VFXMask),
            enabled_name='apply_vfx_mask',
            disabled_name='no_vfx_mask'),
        'day_night_tracking':
        OptionalTunable(
            description=
            "\n            If enabled, allows this trait to track various aspects of day and\n            night via buffs on the owning Sim.\n            \n            For example, if this is enabled and the Sunlight Buff is tuned with\n            buffs, the Sim will get the buffs added every time they're in\n            sunlight and removed when they're no longer in sunlight.\n            ",
            tunable=DayNightTracking.TunableFactory()),
        'persistable':
        Tunable(
            description=
            '\n            If checked then this trait will be saved onto the sim.  If\n            unchecked then the trait will not be saved.\n            Example unchecking:\n            Traits that are applied for the sim being in the region.\n            ',
            tunable_type=bool,
            default=True),
        'initial_commodities':
        TunableSet(
            description=
            '\n            A list of commodities that will be added to a sim on load, if the\n            sim has this trait.\n            \n            If a given commodity is also blacklisted by another trait that the\n            sim also has, it will NOT be added.\n            \n            Example:\n            Adult Age Trait adds Hunger.\n            Vampire Trait blacklists Hunger.\n            Hunger will not be added.\n            ',
            tunable=Commodity.TunableReference(pack_safe=True)),
        'initial_commodities_blacklist':
        TunableSet(
            description=
            "\n            A list of commodities that will be prevented from being\n            added to a sim that has this trait.\n            \n            This always takes priority over any commodities listed in any\n            trait's initial_commodities.\n            \n            Example:\n            Adult Age Trait adds Hunger.\n            Vampire Trait blacklists Hunger.\n            Hunger will not be added.\n            ",
            tunable=Commodity.TunableReference(pack_safe=True)),
        'ui_commodity_sort_override':
        OptionalTunable(
            description=
            '\n            Optional list of commodities to override the default UI sort order.\n            ',
            tunable=TunableList(
                description=
                '\n                The position of the commodity in this list represents the sort order.\n                Add all possible combination of traits in the list.\n                If we have two traits which have sort override, we will implement\n                a priority system to determine which determines which trait sort\n                order to use.\n                ',
                tunable=Commodity.TunableReference())),
        'ui_category':
        OptionalTunable(
            description=
            '\n            If enabled then this trait will be displayed in a specific category\n            within the relationship panel if this trait would be displayed\n            within that panel.\n            ',
            tunable=TunableEnumEntry(
                description=
                '\n                The UI trait category that we use to categorize this trait\n                within the relationship panel.\n                ',
                tunable_type=TraitUICategory,
                default=TraitUICategory.PERSONALITY),
            export_modes=ExportModes.All,
            enabled_name='ui_trait_category_tag'),
        'loot_on_trait_add':
        OptionalTunable(
            description=
            '\n            If tuned, this list of loots will be applied when trait is added in game.\n            ',
            tunable=TunableList(
                description=
                '\n                List of loot to apply on the sim when this trait is added not\n                through CAS.\n                ',
                tunable=TunableReference(
                    description=
                    '\n                    Loot to apply.\n                    ',
                    manager=services.get_instance_manager(
                        sims4.resources.Types.ACTION),
                    pack_safe=True))),
        'npc_leave_lot_interactions':
        OptionalTunable(
            description=
            '\n            If enabled, allows tuning a set of Leave Lot and Leave Lot Must Run\n            interactions that this trait provides. NPC Sims with this trait will\n            use these interactions to leave the lot instead of the defaults.\n            ',
            tunable=TunableTuple(
                description=
                '\n                Leave Lot Now and Leave Lot Now Must Run interactions.\n                ',
                leave_lot_now_interactions=TunableSet(
                    TunableReference(
                        description=
                        '\n                    If tuned, the Sim will consider these interaction when trying to run\n                    any "leave lot" situation.\n                    ',
                        manager=services.get_instance_manager(
                            sims4.resources.Types.INTERACTION),
                        allow_none=False,
                        pack_safe=True)),
                leave_lot_now_must_run_interactions=TunableSet(
                    TunableReference(
                        description=
                        '\n                    If tuned, the Sim will consider these interaction when trying to run\n                    any "leave lot must run" situation.\n                    ',
                        manager=services.get_instance_manager(
                            sims4.resources.Types.INTERACTION),
                        allow_none=False,
                        pack_safe=True)))),
        'hide_relationships':
        Tunable(
            description=
            '\n            If checked, then any relationships with a Sim who has this trait\n            will not be displayed in the UI. This is done by keeping the\n            relationship from having any tracks to actually track which keeps\n            it out of the UI.\n            ',
            tunable_type=bool,
            default=False,
            tuning_group=GroupNames.RELATIONSHIP),
        'whim_set':
        OptionalTunable(
            description=
            '\n            If enabled then this trait will offer a whim set to the Sim when it\n            is active.\n            ',
            tunable=TunableReference(
                description=
                '\n                A whim set that is active when this trait is active.\n                ',
                manager=services.get_instance_manager(
                    sims4.resources.Types.ASPIRATION),
                class_restrictions=('ObjectivelessWhimSet', ))),
        'allow_from_gallery':
        Tunable(
            description=
            '\n            If checked, then this trait is allowed to be transferred over from\n            Sims downloaded from the gallery.\n            ',
            tunable_type=bool,
            default=True,
            tuning_group=GroupNames.SPECIAL_CASES),
        'remove_on_death':
        Tunable(
            description=
            '\n            If checked, when a Sim dies this trait will be removed.\n            ',
            tunable_type=bool,
            default=False,
            tuning_group=GroupNames.SPECIAL_CASES),
        'build_buy_purchase_tracking':
        OptionalTunable(
            description=
            '\n            If enabled, allows this trait to track various build-buy purchases\n            via event listening in the trait tracker.\n            ',
            tunable=TunableList(
                description=
                '\n                Loots to apply to the hamper when clothing pile is being put.\n                ',
                tunable=TunableReference(manager=services.get_instance_manager(
                    sims4.resources.Types.ACTION),
                                         class_restrictions=('LootActions', ),
                                         pack_safe=True)))
    }
    _asm_param_name = None
    default_trait_params = set()

    def __repr__(self):
        return '<Trait:({})>'.format(self.__name__)

    def __str__(self):
        return '{}'.format(self.__name__)

    @classmethod
    def _tuning_loaded_callback(cls):
        cls._asm_param_name = cls.trait_asm_overrides.trait_asm_param
        if cls._asm_param_name is None:
            cls._asm_param_name = cls.__name__
        if cls.trait_asm_overrides.trait_asm_param is not None and cls.trait_asm_overrides.consider_for_boundary_conditions:
            cls.default_trait_params.add(
                cls.trait_asm_overrides.trait_asm_param)
        for (buff, replacement_buff) in cls.buff_replacements.items():
            if buff.trait_replacement_buffs is None:
                buff.trait_replacement_buffs = {}
            buff.trait_replacement_buffs[cls] = replacement_buff
        for mood in cls.excluded_mood_types:
            if mood.excluding_traits is None:
                mood.excluding_traits = []
            mood.excluding_traits.append(cls)

    @classmethod
    def _verify_tuning_callback(cls):
        if cls.display_name:
            if not cls.display_name_gender_neutral.hash:
                logger.error(
                    'Trait {} specifies a display name. It must also specify a gender-neutral display name. These must use different string keys.',
                    cls,
                    owner='BadTuning')
            if cls.display_name._string_id == cls.display_name_gender_neutral.hash:
                logger.error(
                    'Trait {} has the same string tuned for its display name and its gender-neutral display name. These must be different strings for localization.',
                    cls,
                    owner='BadTuning')
        if cls.day_night_tracking is not None:
            if not cls.day_night_tracking.sunlight_buffs and not (
                    not cls.day_night_tracking.shade_buffs
                    and not (not cls.day_night_tracking.day_buffs
                             and not cls.day_night_tracking.night_buffs)):
                logger.error(
                    'Trait {} has Day Night Tracking enabled but no buffs are tuned. Either tune buffs or disable the tracking.',
                    cls,
                    owner='BadTuning')
            else:
                tracking_buff_tag = Trait.DAY_NIGHT_TRACKING_BUFF_TAG
                if any(
                        buff for buff in cls.day_night_tracking.sunlight_buffs
                        if not buff.buff_type.has_tag(tracking_buff_tag)
                ) or (any(buff for buff in cls.day_night_tracking.shade_buffs
                          if not buff.buff_type.has_tag(tracking_buff_tag))
                      or any(buff for buff in cls.day_night_tracking.day_buffs
                             if not buff.buff_type.has_tag(tracking_buff_tag))
                      ) or any(
                          buff
                          for buff in cls.day_night_tracking.night_buffs
                          if not buff.buff_type.has_tag(tracking_buff_tag)):
                    logger.error(
                        'Trait {} has Day Night tracking with an invalid\n                    buff. All buffs must be tagged with {} in order to be\n                    used as part of Day Night Tracking. Add these buffs with the\n                    understanding that, regardless of what system added them, they\n                    will always be on the Sim when the condition is met (i.e.\n                    Sunlight Buffs always added with sunlight is out) and they will\n                    always be removed when the condition is not met. Even if another\n                    system adds the buff, they will be removed if this trait is\n                    tuned to do that.\n                    ',
                        cls, tracking_buff_tag)
        for buff_reference in cls.buffs:
            if buff_reference.buff_type.broadcaster is not None:
                logger.error(
                    'Trait {} has a buff {} with a broadcaster tuned that will never be removed. This is a potential performance hit, and a GPE should decide whether this is the best place for such.',
                    cls,
                    buff_reference,
                    owner='rmccord')
        for commodity in cls.initial_commodities:
            if not commodity.persisted_tuning:
                logger.error(
                    'Trait {} has an initial commodity {} that does not have persisted tuning.',
                    cls, commodity)

    @classproperty
    def is_personality_trait(cls):
        return cls.trait_type == TraitType.PERSONALITY

    @classproperty
    def is_aspiration_trait(cls):
        return cls.trait_type == TraitType.ASPIRATION

    @classproperty
    def is_gender_option_trait(cls):
        return cls.trait_type == TraitType.GENDER_OPTIONS

    @classproperty
    def is_ghost_trait(cls):
        return cls.trait_type == TraitType.GHOST

    @classproperty
    def is_robot_trait(cls):
        return cls.trait_type == TraitType.ROBOT

    @classmethod
    def is_valid_trait(cls, sim_info_data):
        if cls.ages and sim_info_data.age not in cls.ages:
            return False
        if cls.genders and sim_info_data.gender not in cls.genders:
            return False
        elif cls.species and sim_info_data.species not in cls.species:
            return False
        return True

    @classmethod
    def should_apply_fixup_actions(cls, fixup_source):
        if cls.sim_info_fixup_actions and cls.sim_info_fixup_actions_timing == fixup_source:
            if fixup_source != SimInfoFixupActionTiming.ON_FIRST_SIMINFO_LOAD:
                logger.warn(
                    'Trait {} has fixup actions not from CAS flow.This should only happen to old saves before EP08',
                    cls,
                    owner='yozhang')
            return True
        return False

    @classmethod
    def apply_fixup_actions(cls, sim_info):
        for fixup_action in cls.sim_info_fixup_actions:
            fixup_action(sim_info)

    @classmethod
    def can_age_up(cls, current_age):
        if not cls.disable_aging:
            return True
        return current_age in cls.disable_aging.allowed_ages

    @classmethod
    def is_conflicting(cls, trait):
        if trait is None:
            return False
        if cls.conflicting_traits and trait in cls.conflicting_traits:
            return True
        elif trait.conflicting_traits and cls in trait.conflicting_traits:
            return True
        return False

    @classmethod
    def get_outfit_change_reason(cls, outfit_change_reason):
        replaced_reason = cls.outfit_replacements.get(
            outfit_change_reason if outfit_change_reason is not None else
            OutfitChangeReason.Invalid)
        if replaced_reason is not None:
            return replaced_reason
        elif outfit_change_reason is not None:
            replaced_reason = cls.outfit_replacements.get(
                OutfitChangeReason.Invalid)
            if replaced_reason is not None:
                return replaced_reason
        return outfit_change_reason

    @classmethod
    def get_teleport_style_interaction_to_inject(cls):
        return cls.teleport_style_interaction_to_inject
class Recipe(metaclass=HashedTunedInstanceMetaclass,
             manager=services.get_instance_manager(
                 sims4.resources.Types.RECIPE)):
    __qualname__ = 'Recipe'
    INSTANCE_TUNABLES = {
        'name':
        TunableLocalizedStringFactory(
            description='\n            The name of this recipe.\n            '
        ),
        'phase_interaction_name':
        TunableLocalizedStringFactory(
            description=
            "\n            The name of each phase's interaction.\n            "
        ),
        'final_product':
        TunableRecipeObjectInfo(
            description=
            '\n            The final product of the crafting process.\n            ',
            optional_create=False),
        '_first_phases':
        TunableList(
            description=
            '\n            The names of the phases that can be done first.  This cannot be empty.\n            ',
            tunable=TunableEnumEntry(PhaseName, default=None)),
        '_phases':
        TunableMapping(
            description=
            '\n            The phases that make up this recipe.\n            ',
            key_type=TunableEnumEntry(PhaseName, None),
            value_type=TunablePhaseVariant()),
        'resume_affordance':
        OptionalTunable(
            description=
            '\n            The interaction to use when resuming crafting this recipe.\n            ',
            tunable=TunableReference(
                manager=services.get_instance_manager(
                    sims4.resources.Types.INTERACTION),
                class_restrictions=('CraftingResumeInteraction', ))),
        'skill_test':
        OptionalTunable(
            description=
            '\n            The skill level required to use this recipe.\n            ',
            tunable=event_testing.test_variants.SkillRangeTest.TunableFactory(
            )),
        'additional_tests':
        event_testing.tests.TunableTestSetWithTooltip(),
        'additional_tests_ignored_on_resume':
        Tunable(
            description=
            '\n            If set, additional tests are ignored when testing to see if a recipe\n            can be resumed.\n            ',
            tunable_type=bool,
            default=False),
        'utility_info':
        OptionalTunable(TunableList(
            description=
            '\n            Tuning that specifies which utilities this recipe requires to\n            be made/cooked.\n            ',
            tunable=TunableEnumEntry(Utilities, None)),
                        needs_tuning=True),
        '_retail_price':
        Tunable(
            description=
            '\n            Retail price of the recipe. \n            This is not the total price of the recipe.  The total price of the \n            recipe will be _retail_price+delta_ingredient_price\n            ',
            tunable_type=int,
            default=0),
        'crafting_cost':
        TunableVariant(
            description=
            '\n            This determines the amount of money the Sim will have to pay in\n            order to craft this recipe.\n            \n            Crafting Discount is multiplied by the Retail Price to determine the\n            amount of money the Sim must pay to craft this.\n            \n            Flat Fee is just a flat fee the Sim must pay to craft this.\n            ',
            discount=Tunable(
                description=
                '\n                This number is multiplied by the Retail Price to to determine\n                the amount of money the Sim will have to pay in order to craft this.\n                ',
                tunable_type=float,
                default=0.8),
            flat_fee=Tunable(
                description=
                '\n                This is a flat amount of simoleons that the Sim will have to pay\n                in order to craft this.\n                ',
                tunable_type=int,
                default=0),
            default='discount'),
        'delta_ingredient_price':
        Tunable(
            description=
            '\n            Delta price of a recipe will be a delta value that will increase\n            or decrease depending on how many ingredients for the recipe the \n            sim has.\n            e.g  For a 3 ingredient recipe:\n            - If no ingredient is found,  price will be retail_price + delta\n            - If 1 ingredient is found,  price will be retail_price + 2/3 of \n            delta\n            - If 2 ingredient is found,  price will be retail_price + 1/3 of \n            delta\n            - If 3 ingredient is found,  price will be retail_price\n            ',
            tunable_type=int,
            default=0),
        '_no_initial_charge':
        Tunable(
            description=
            '\n            If set, there is no initial charge for making this recipe\n            ',
            tunable_type=bool,
            needs_tuning=True,
            default=False),
        'icon_override':
        TunableResourceKey(
            description=
            "\n            This will override the default icon for this recipe. If left blank, the thumbnail of the recipe's final product will be used.\n            ",
            default=None,
            resource_types=sims4.resources.CompoundTypes.IMAGE),
        'recipe_description':
        TunableLocalizedStringFactory(description='recipe description'),
        'autonomy_weight':
        Tunable(
            description=
            '\n            The relative weight for autonomy to choose to make or order this recipe\n            ',
            tunable_type=float,
            default=0),
        'buff_weight_multipliers':
        TunableBuffWeightMultipliers(),
        'base_recipe':
        OptionalTunable(
            description=
            '\n            If set, the single serving counterpart for this recipe\n            ',
            tunable=TunableReference(services.recipe_manager())),
        'visible_as_subrow':
        Tunable(
            description=
            '\n            If this recipe has base recipe, this boolean will decide whether or\n            not this recipe will show up in the subrow entry of the base\n            recipe.\n            ',
            tunable_type=bool,
            default=True),
        'base_recipe_category':
        OptionalTunable(
            description=
            '\n            The pie menu category of the base recipe if the picker dialog to\n            choose recipes is tuned to use pie menu formation.\n            ',
            tunable=TunableReference(
                description=
                '\n                Pie menu category for pie menu mixers.\n                ',
                manager=services.get_instance_manager(
                    sims4.resources.Types.PIE_MENU_CATEGORY))),
        'multi_serving_name':
        OptionalTunable(
            TunableLocalizedStringFactory(
                description=
                'The name of multiple serving to show in the sub row of ObjectPicker.'
            ),
            enabled_name
            ='use_multi_serving_name',
            disabled_name='use_recipe_name',
            description=
            "The string that shows up in the ObjectPicker when this recipe is a multi_serving. Please use 'use_recipe_name' when it's a single serving"
        ),
        'push_consume':
        Tunable(
            description=
            '\n            Whether to push the consume after finish the recipe.\n            ',
            tunable_type=bool,
            needs_tuning=True,
            default=True),
        'push_consume_threshold':
        OptionalTunable(
            description=
            '\n            If Push Consume is checked and this threshold is enabled, the consume affordance will\n            only be pushed if the threshold is met.\n            ',
            tunable=TunableTuple(
                description=
                '\n                The commodity/threshold pair.\n                ',
                commodity=TunableReference(
                    description=
                    '\n                    The commodity to be tested.\n                    ',
                    manager=services.statistic_manager(),
                    class_restrictions='Commodity'),
                threshold=TunableThreshold(
                    description=
                    '\n                    The threshold at which to remove this bit.\n                    '
                ))),
        'skill_loot_data':
        TunableSkillLootData(
            description=
            'Loot Data for DynamicSkillLootOp. This will only be used if in the loot list of the outcome there is a dynamic loot op.'
        ),
        'masterwork_name':
        TunableLocalizedString(
            description=
            'If this can be a masterwork, what should its masterwork state be called? Usually this is just Masterpiece.'
        ),
        'resumable':
        Tunable(
            description=
            '\n            If set to False, this recipe is not resumable, for example drinks made at bar.\n            ',
            tunable_type=bool,
            default=True),
        'resumable_by_different_sim':
        Tunable(
            description=
            '\n            If set, this recipe can be resumed by a sim other than the one that started it\n            ',
            tunable_type=bool,
            needs_tuning=True,
            default=False),
        'entitlement':
        TunableEntitlement(
            description=
            '\n            Entitlement required to use this recipe.\n            '
        ),
        'hidden_until_unlock':
        Tunable(
            description=
            "\n            If checked, this recipe will not show up in picker or piemenu if\n            it's not unlocked, either by skill up, or unlock outcome, or\n            entitlement.\n            ",
            tunable_type=bool,
            needs_tuning=True,
            default=True),
        'use_ingredients':
        OptionalTunable(
            description=
            '\n            If checked recipe will have the ability to use ingredients to \n            improve its quality when prepared.\n            ',
            tunable=TunableTuple(
                description=
                '\n                Ingredient data for a recipe.\n                ',
                all_ingredients_required=Tunable(
                    description=
                    '\n                    If checked recipe will not be available unless all \n                    ingredients are found on the sim inventory or the fridge\n                    inventory.\n                    ',
                    tunable_type=bool,
                    default=False),
                ingredient_list=TunableList(
                    description=
                    '\n                    List of ingredients the recipe can use\n                    ',
                    tunable=TunableVariant(
                        description=
                        '\n                        Possible ingredient mapping by object definition of by \n                        catalog object Tag.\n                        ',
                        ingredient_by_definition=TunableIngredientByDefFactory(
                        ),
                        ingredient_by_tag=TunableIngredientByTagFactory())))),
        'crafted_by_text':
        TunableLocalizedStringFactory(
            description=
            "\n            Text that describes who made it, e.g. 'Made By: <Sim>'. Loc\n            parameter 0 is the Sim crafter.\n            "
        ),
        'value_text':
        TunableLocalizedStringFactory(
            description=
            "\n            Text (if any) that describes the value in the tooltip.  \n            e.g. 'Value: $500'. Loc parameter 0 is the value.\n            "
        ),
        'mood_list':
        TunableList(
            description=
            '\n            A list of possible moods this Recipe may associate with. Note that\n            this list is for coloring-purposes only. Interaction availability\n            testing is relegated to the individual mood tests on the\n            interaction. Future refactoring necessary.\n            ',
            tunable=TunableReference(manager=services.mood_manager()))
    }
    _tuning_loaded = False
    _referenced_by_start_crafting = False

    @classmethod
    def _tuning_loaded_callback(cls):
        cls._visible_phase_sequence = []
        cls.phases = {
            phase_id: phase(recipe=cls, phase_id=phase_id)
            for (phase_id, phase) in cls._phases.items()
        }
        first_phases = []
        for name in cls._first_phases:
            if name in cls.phases:
                first_phases.append(cls.phases[name])
            else:
                logger.error(
                    "Unknown phase '{}' specified in first_phases for recipe '{}'.",
                    name, cls)
        cls.first_phases = first_phases
        for phase in cls.phases.values():
            phase.recipe_tuning_loaded()
        if cls.first_phases:
            cls._build_visible_phase_sequence(cls.first_phases[0])
        cls._tuning_loaded = True
        if cls._referenced_by_start_crafting:
            cls.validate_for_start_crafting()

    @classmethod
    def _verify_tuning_callback(cls):
        if cls.use_ingredients:
            for ingredient in cls.use_ingredients.ingredient_list:
                while ingredient is None:
                    logger.error('Recipe {} has unset ingredients',
                                 cls.__name__)
            if not cls.use_ingredients.ingredient_list:
                logger.error(
                    'Recipe {} has an empty ingredient list and its tuned to use ingredients.',
                    cls.__name__)
        if not cls.name:
            logger.error('Recipe {} does not have a name set', cls.__name__)
        if not cls.phase_interaction_name:
            logger.error(
                'Recipe {} does not have a phase interaction name set',
                cls.__name__)
        cls._validate_final_product()
        services.get_instance_manager(
            sims4.resources.Types.RECIPE).add_on_load_complete(
                cls.validate_base_recipe)

    @classmethod
    def _validate_final_product(cls):
        if cls.final_product_definition is None:
            return
        supported_states = objects.components.state.get_supported_state(
            cls.final_product.definition)
        unsupported_values = []
        for state_value in itertools.chain(cls.final_product.initial_states,
                                           cls.final_product.apply_states):
            while supported_states is None or state_value.state not in supported_states:
                unsupported_values.append(state_value)
        if supported_states is None:
            error = "\n    A recipe wants to set one or more state value on its final product, but that\n    object doesn't have a StateComponent.  The recipe shouldn't be trying to set\n    any state values, or the object's tuning should be updated to add these\n    states."
        else:
            error = "\n    A recipe wants to set a state value on its final product, but that object's\n    state component tuning doesn't have an entry for that state.  The recipe\n    shouldn't be trying to set these state values, or the object's tuning should\n    be updated to add these states."
        logger.warn(
            'Recipe tuning error:{}\n        Recipe: {}\n        Missing States: {}\n        Final Product: {} ({})'
            .format(
                error, cls.__name__, ', '.join(
                    sorted({e.state.__name__
                            for e in unsupported_values})),
                cls.final_product.definition.name,
                cls.final_product.definition.cls.__name__))
        for sa in cls.final_product.super_affordances:
            while sa.consumes_object() or sa.contains_stat(
                    CraftingTuning.CONSUME_STATISTIC):
                logger.error(
                    'Recipe: Interaction {} on {} is consume affordance, should tune on ConsumableComponent of the object.',
                    sa.__name__,
                    cls.__name__,
                    owner='tastle/cjiang')

    @classmethod
    def validate_for_start_crafting(cls):
        if not cls._tuning_loaded:
            cls._referenced_by_start_crafting = True
        elif not cls.first_phases:
            logger.error(
                'Recipe is tuned to be craftable but has no first phases defined: {}',
                cls)

    @classmethod
    def validate_base_recipe(cls, manager):
        base_recipe = cls.base_recipe
        if base_recipe is not None and cls.hidden_until_unlock != base_recipe.hidden_until_unlock:
            logger.error(
                "Recipe({})'s hidden_until_unlock({}) != base_recipe({})'s hidden_until_unlock({})",
                cls.__name__, cls.hidden_until_unlock, base_recipe.__name__,
                base_recipe.hidden_until_unlock)

    @classmethod
    def _build_visible_phase_sequence(cls, phase):
        if phase.is_visible:
            cls._visible_phase_sequence.append(phase)
        if phase.next_phases:
            next_phase = phase.next_phases[0]
            cls._build_visible_phase_sequence(next_phase)

    @classproperty
    def final_product_definition(cls):
        return cls.final_product.definition

    @classproperty
    def final_product_definition_id(cls):
        return cls.final_product.definition.id

    @classproperty
    def has_final_product_definition(cls):
        return cls.final_product.definition is not None

    @classproperty
    def final_product_geo_hash(cls):
        if cls.final_product.thumbnail_geo_state is not None:
            return sims4.hash_util.hash32(
                cls.final_product.thumbnail_geo_state)
        return cls.final_product_definition.thumbnail_geo_state_hash

    @classproperty
    def final_product_material_hash(cls):
        if cls.final_product.thumbnail_material_state is not None:
            return sims4.hash_util.hash32(
                cls.final_product.thumbnail_material_state)
        return 0

    @classproperty
    def final_product_type(cls):
        return cls.final_product.definition.cls

    @classproperty
    def apply_tags(cls):
        obj_info = cls.final_product
        if obj_info is not None:
            return obj_info.apply_tags
        return set()

    @classmethod
    def get_final_product_quality_adjustment(cls, effective_skill):
        quality_adjustment = cls.final_product.quality_adjustment
        skill_delta = effective_skill - cls.required_skill_level
        quality_value = quality_adjustment.base_quality + skill_delta * quality_adjustment.skill_adjustment
        return quality_value

    @classmethod
    def setup_crafted_object(cls, crafted_object, crafter, is_final_product):
        pass

    @classproperty
    def crafting_price(cls):
        if cls._no_initial_charge:
            return 0
        if isinstance(cls.crafting_cost, int):
            return cls.crafting_cost
        return int(cls._retail_price * cls.crafting_cost)

    @classproperty
    def retail_price(cls):
        return cls._retail_price

    @classproperty
    def simoleon_value_modifiers(cls):
        return cls.final_product.simoleon_value_modifiers_map

    @classproperty
    def simoleon_value_skill_curve(cls):
        return cls.final_product.simoleon_value_skill_curve

    @classproperty
    def masterworks_data(cls):
        return cls.final_product.masterworks

    @classproperty
    def required_skill_level(cls):
        if cls.skill_test is not None:
            return cls.skill_test.skill_range_min
        return 0

    @classproperty
    def required_skill(cls):
        if cls.skill_test is not None:
            return cls.skill_test.skill

    @classmethod
    def get_base_recipe(cls):
        return cls.base_recipe or cls

    @classmethod
    def get_recipe_picker_name(cls, *args):
        if cls.multi_serving_name is not None:
            return cls.multi_serving_name(*args)
        return cls.name(*args)

    @classmethod
    def get_recipe_name(cls, *args):
        return cls.name(*args)

    @classmethod
    def get_price(cls, is_retail=False, ingredient_modifier=1):
        if is_retail:
            if cls.retail_price != 0:
                return cls.retail_price
            return cls.crafting_price
        return cls.crafting_price + int(
            cls.delta_ingredient_price * ingredient_modifier)

    @classmethod
    def calculate_autonomy_weight(cls, sim):
        total_weight = cls.autonomy_weight
        for (buff, weight) in cls.buff_weight_multipliers.items():
            while sim.has_buff(buff):
                total_weight *= weight
        return total_weight

    @classproperty
    def total_visible_phases(cls):
        return len(cls._visible_phase_sequence)

    @classmethod
    def get_visible_phase_index(cls, phase):
        if phase in cls._visible_phase_sequence:
            return cls._visible_phase_sequence.index(phase) + 1
        return 0

    @classproperty
    def is_single_phase_recipe(cls):
        return len(cls._phases) == 1

    @classmethod
    def update_hovertip(cls, owner, crafter=None):
        description = cls.recipe_description(crafter)
        genre = Genre.get_genre_localized_string(owner)
        if genre is not None:
            description = LocalizationHelperTuning.get_new_line_separated_strings(
                description, genre)
        value_text = cls.value_text
        if value_text:
            localized_value = value_text(owner.current_value)
            description = LocalizationHelperTuning.get_new_line_separated_strings(
                description, localized_value)
        objects.components.crafting_component._set_recipe_decription(
            owner, description)

    @property
    def debug_name(self):
        return type(self).__name__

    @classmethod
    def debug_dump(cls, dump=dump_logger.warn):
        dump('Recipe Name: {}'.format(type(cls).__name__))
        dump('Phases: {}'.format(len(cls.phases)))
        for phase in cls.phases.values():
            dump('  Phase {}:'.format(phase.id))
            while phase.num_turns > 0:
                dump('    Turns: {}'.format(phase.turns))