Example #1
0
 def __init__(self, **kwargs):
     super().__init__(
         position=TunableVector2(
             description=
             '\n                    Position of this actor in the scene.\n                    ',
             default=sims4.math.Vector2.ZERO()),
         rotation=TunableAngle(
             description=
             '\n                    Angle of this actor in the scene.\n                    ',
             default=0),
         **kwargs)
Example #2
0
class _PortalLocation(_PortalLocationBase):
    FACTORY_TUNABLES = {
        'translation': TunableVector2(default=TunableVector2.DEFAULT_ZERO)
    }

    def __init__(self, obj, translation, *args, **kwargs):
        self._translation = translation
        super().__init__(obj, *args, **kwargs)

    def get_translation(self, obj):
        return obj.transform.transform_point(
            Vector3(self._translation.x, 0, self._translation.y))
Example #3
0
class Skill(HasTunableReference,
            statistics.continuous_statistic_tuning.TunedContinuousStatistic,
            metaclass=HashedTunedInstanceMetaclass,
            manager=services.get_instance_manager(
                sims4.resources.Types.STATISTIC)):
    __qualname__ = 'Skill'
    SKILL_LEVEL_LIST = TunableMapping(
        key_type=TunableEnumEntry(SkillLevelType, SkillLevelType.MAJOR),
        value_type=TunableList(
            Tunable(int, 0),
            description=
            'The level boundaries for skill type, specified as a delta from the previous value'
        ),
        export_modes=ExportModes.All)
    SKILL_EFFECTIVENESS_GAIN = TunableMapping(
        key_type=TunableEnumEntry(SkillEffectiveness,
                                  SkillEffectiveness.STANDARD),
        value_type=TunableCurve(),
        description='Skill gain points based on skill effectiveness.')
    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            Localized name of this Statistic\n            ',
            export_modes=ExportModes.All),
        '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))),
        'weight':
        Tunable(
            description=
            "\n            The weight of the Skill with regards to autonomy.  It's ignored \n            for the purposes of sorting stats, but it's applied when scoring \n            the actual statistic operation for the SI.\n            ",
            tunable_type=float,
            default=0.5),
        'skill_level_type':
        TunableEnumEntry(
            description='\n            Skill level list to use.\n            ',
            tunable_type=SkillLevelType,
            default=SkillLevelType.MAJOR,
            export_modes=ExportModes.All),
        'locked_description':
        TunableLocalizedString(
            description=
            "\n            The skill description when it's locked.\n            ",
            export_modes=ExportModes.All),
        'skill_description':
        TunableLocalizedString(
            description=
            "\n            The skill's normal description.\n            ",
            export_modes=ExportModes.All),
        'is_default':
        Tunable(
            description=
            '\n            Whether Sim will default has this skill.\n            ',
            tunable_type=bool,
            default=False),
        'genders':
        TunableSet(
            description=
            '\n            Skill allowed gender, empty set means not specified\n            ',
            tunable=TunableEnumEntry(tunable_type=sim_info_types.Gender,
                                     default=None,
                                     export_modes=ExportModes.All)),
        'ages':
        TunableSet(
            description=
            '\n            Skill allowed ages, empty set means not specified\n            ',
            tunable=TunableEnumEntry(tunable_type=sim_info_types.Age,
                                     default=None,
                                     export_modes=ExportModes.All)),
        'entitlement':
        TunableEntitlement(
            description=
            '\n            Entitlement required to use this skill.\n            '
        ),
        'icon':
        TunableResourceKey(
            description=
            '\n            Icon to be displayed for the Skill.\n            ',
            default='PNG:missing_image',
            resource_types=sims4.resources.CompoundTypes.IMAGE,
            export_modes=ExportModes.All),
        'tags':
        TunableList(
            description=
            '\n            The associated categories of the skill\n            ',
            tunable=TunableEnumEntry(tunable_type=tag.Tag,
                                     default=tag.Tag.INVALID)),
        'priority':
        Tunable(
            description=
            '\n            Skill priority.  Higher priority skill will trump other skills when\n            being displayed on the UI side. When a sim gains multiple skills at\n            the same time only the highest priority one will display a progress\n            bar over its head.\n            ',
            tunable_type=int,
            default=1,
            export_modes=ExportModes.All),
        '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),
        'next_level_teaser':
        TunableList(
            description=
            '\n            Tooltip which describes what the next level entails.\n            ',
            tunable=TunableLocalizedString(),
            export_modes=(ExportModes.ClientBinary, )),
        '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(),
                    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(),
            export_modes=ExportModes.All),
        'stat_asm_param':
        TunableStatAsmParam.TunableFactory(),
        '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),
            class_restrictions=('Tutorial', )),
        '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', )))
    }
    REMOVE_INSTANCE_TUNABLES = ('min_value_tuning', 'max_value_tuning',
                                'decay_rate', '_default_convergence_value')

    def __init__(self, tracker):
        super().__init__(tracker, self.initial_value)
        self._delta_enabled = True
        self._callback_handle = None
        if self.tracker.owner.is_simulating:
            self.on_initial_startup()
        self._max_level_update_sent = False

    def on_initial_startup(self):
        if self.tracker.owner.is_selectable:
            self.refresh_level_up_callback()

    def on_remove(self, on_destroy=False):
        super().on_remove(on_destroy=on_destroy)
        self._destory_callback_handle()

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

    @caches.cached
    def get_user_value(self):
        return super(Skill, self).get_user_value()

    def set_value(self,
                  value,
                  *args,
                  from_load=False,
                  interaction=None,
                  **kwargs):
        old_value = self.get_value()
        super().set_value(value, *args, **kwargs)
        self.get_user_value.cache.clear()
        if not from_load:
            new_value = self.get_value()
            new_level = self.convert_to_user_value(value)
            if old_value == self.initial_value and old_value != new_value:
                sim_info = self._tracker._owner
                services.get_event_manager().process_event(
                    test_events.TestEvent.SkillLevelChange,
                    sim_info=sim_info,
                    statistic=self.stat_type)
            old_level = self.convert_to_user_value(old_value)
            if old_level < new_level:
                self._apply_multipliers_to_continuous_statistics()

    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)
        self.get_user_value.cache.clear()
        if interaction is not None:
            self.on_skill_updated(telemhook, old_value, self.get_value(),
                                  interaction.affordance.__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()
        self.get_user_value.cache.clear()
        new_value = self._value
        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)
            sim_info = self._tracker._owner
            services.get_event_manager().process_event(
                test_events.TestEvent.SkillLevelChange,
                sim_info=sim_info,
                statistic=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_value < new_value and old_level < new_level:
            if self._tracker is not None:
                self._tracker.notify_watchers(self.stat_type, self._value,
                                              self._value)

    def on_skill_updated(self, telemhook, old_value, new_value,
                         affordance_name):
        owner_sim = self._tracker._owner
        if owner_sim.is_selectable:
            with telemetry_helper.begin_hook(skill_telemetry_writer,
                                             telemhook,
                                             sim=owner_sim) 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._show_level_notification(skill_level)

    def _destory_callback_handle(self):
        if self._callback_handle is not None:
            self.remove_callback(self._callback_handle)
            self._callback_handle = None

    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.add_callback(
            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=sim_info) as hook:
            hook.write_guid(TELEMETRY_FIELD_SKILL_ID, self.guid64)
            hook.write_int(TELEMETRY_FIELD_SKILL_LEVEL, new_level)
        if sim_info.account is not None:
            services.social_service.post_skill_message(sim_info, self,
                                                       old_level, new_level)
        self._show_level_notification(new_level)
        services.get_event_manager().process_event(
            test_events.TestEvent.SkillLevelChange,
            sim_info=sim_info,
            statistic=self.stat_type)

    def _show_level_notification(self, skill_level):
        sim_info = self._tracker._owner
        if not sim_info.is_npc:
            level_data = self.level_data.get(skill_level)
            if level_data is not None:
                tutorial_id = None
                if self.tutorial is not None and 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=(self.icon, None),
                                         secondary_icon_override=(None,
                                                                  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)

    @classproperty
    def skill_type(cls):
        return cls

    @classproperty
    def remove_on_convergence(cls):
        return False

    @classmethod
    def can_add(cls, owner, force_add=False, **kwargs):
        if force_add:
            return True
        if cls.genders and owner.gender not in cls.genders:
            return False
        if cls.ages and owner.age not in cls.ages:
            return False
        if cls.entitlement is None:
            return True
        if owner.is_npc:
            return False
        return mtx.has_entitlement(cls.entitlement)

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

    @classmethod
    def get_max_skill_value(cls):
        level_list = cls.get_level_list()
        return sum(level_list)

    @classmethod
    def get_skill_value_for_level(cls, level):
        level_list = cls.get_level_list()
        if level > len(level_list):
            logger.error('Level {} out of bounds', level)
            return 0
        return sum(level_list[:level])

    @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

    @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')

    @classmethod
    def convert_to_user_value(cls, value):
        if not cls.get_level_list():
            return 0
        current_value = value
        for (level, level_threshold) in enumerate(cls.get_level_list()):
            current_value -= level_threshold
            while 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 _get_level_bounds(cls, level):
        level_list = cls.get_level_list()
        level_min = sum(level_list[:level])
        if level < cls.max_level:
            level_max = sum(level_list[:level + 1])
        else:
            level_max = sum(level_list)
        return (level_min, level_max)

    def _get_next_level_bound(self):
        level = self.convert_to_user_value(self._value)
        (_, level_max) = self._get_level_bounds(level)
        return level_max

    @property
    def reached_max_level(self):
        max_value = self.get_max_skill_value()
        if self.get_value() >= max_value:
            return True
        return False

    @property
    def should_send_update(self):
        if not self.reached_max_level:
            return True
        if not self._max_level_update_sent:
            self._max_level_update_sent = True
            return True
        return False

    @classproperty
    def is_skill(cls):
        return True

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

    @classmethod
    def create_skill_update_msg(cls, sim_id, stat_value):
        if not cls.convert_to_user_value(stat_value) > 0:
            return
        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

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

    @classproperty
    def valid_for_stat_testing(cls):
        return True
Example #4
0
class FormationTypeFollow(FormationTypeBase):
    ATTACH_NODE_COUNT = 3
    ATTACH_NODE_RADIUS = 0.25
    ATTACH_NODE_ANGLE = math.PI
    ATTACH_NODE_FLAGS = 4
    RAYTRACE_HEIGHT = 1.5
    RAYTRACE_RADIUS = 0.1
    FACTORY_TUNABLES = {
        'formation_offsets':
        TunableList(
            description=
            '\n            A list of offsets, relative to the master, that define where slaved\n            Sims are positioned.\n            ',
            tunable=TunableVector2(default=Vector2.ZERO()),
            minlength=1),
        'formation_constraints':
        TunableList(
            description=
            '\n            A list of constraints that slaved Sims must satisfy any time they\n            run interactions while in this formation. This can be a geometric\n            constraint, for example, that ensures Sims are always placed within\n            a radius or cone of their slaved position.\n            ',
            tunable=TunableConstraintVariant(
                constraint_locked_args={'multi_surface': True},
                circle_locked_args={'require_los': False},
                disabled_constraints={'spawn_points', 'relative_circle'})),
        '_route_length_interval':
        TunableInterval(
            description=
            '\n            Sims are slaved in formation only if the route is within this range\n            amount, in meters.\n            \n            Furthermore, routes shorter than the minimum\n            will not interrupt behavior (e.g. a socializing Sim will not force\n            dogs to get up and move around).\n            \n            Also routes longer than the maximum will make the slaved sim  \n            instantly position next to their master\n            (e.g. if a leashed dog gets too far from the owner, we place it next to the owner).\n            ',
            tunable_type=float,
            default_lower=1,
            default_upper=20,
            minimum=0),
        'fgl_on_routes':
        TunableTuple(
            description=
            '\n            Data associated with the FGL Context on following slaves.\n            ',
            slave_should_face_master=Tunable(
                description=
                '\n                If enabled, the Slave should attempt to face the master at the end\n                of routes.\n                ',
                tunable_type=bool,
                default=False),
            height_tolerance=OptionalTunable(
                description=
                '\n                If enabled than we will set the height tolerance in FGL.\n                ',
                tunable=TunableRange(
                    description=
                    '\n                    The height tolerance piped to FGL.\n                    ',
                    tunable_type=float,
                    default=0.035,
                    minimum=0,
                    maximum=1)))
    }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._attachment_chain = []
        formation_count = self.master.get_routing_slave_data_count(
            self._formation_cls)
        self._formation_offset = self.formation_offsets[formation_count]
        self._setup_right_angle_connections()
        self._offset = Vector3.ZERO()
        for attachment_info in self._attachment_chain:
            self._offset.x = self._offset.x + attachment_info.parent_offset.x - attachment_info.offset.x
            self._offset.z = self._offset.z + attachment_info.parent_offset.y - attachment_info.offset.y
        self._slave_constraint = None
        self._slave_lock = None
        self._final_transform = None

    @classproperty
    def routing_type(cls):
        return FormationRoutingType.FOLLOW

    @property
    def offset(self):
        return self._formation_offset

    @property
    def slave_attachment_type(self):
        return Routing_pb2.SlaveData.SLAVE_FOLLOW_ATTACHMENT

    @staticmethod
    def get_max_slave_count(tuned_factory):
        return len(tuned_factory._tuned_values.formation_offsets)

    @property
    def route_length_minimum(self):
        return self._route_length_interval.lower_bound

    @property
    def route_length_maximum(self):
        return self._route_length_interval.upper_bound

    def attachment_info_gen(self):
        yield from self._attachment_chain

    def on_master_route_start(self):
        self._build_routing_slave_constraint()
        self._lock_slave()
        if self._slave.is_sim:
            for si in self._slave.get_all_running_and_queued_interactions():
                if si.transition is not None:
                    if si.transition is not self.master.transition_controller:
                        si.transition.derail(DerailReason.CONSTRAINTS_CHANGED,
                                             self._slave)

    def on_master_route_end(self):
        self._build_routing_slave_constraint()
        if self._slave.is_sim:
            for si in self._slave.get_all_running_and_queued_interactions():
                if si.transition is not None:
                    if si.transition is not self.master.transition_controller:
                        si.transition.derail(DerailReason.CONSTRAINTS_CHANGED,
                                             self._slave)
        self._unlock_slave()
        self._final_transform = None

    def _lock_slave(self):
        self._slave_lock = self._slave.add_work_lock(self)

    def _unlock_slave(self):
        self._slave.remove_work_lock(self)

    def _build_routing_slave_constraint(self):
        self._slave_constraint = ANYWHERE
        for constraint in self.formation_constraints:
            constraint = constraint.create_constraint(
                self._slave,
                target=self._master,
                target_position=self._master.intended_position)
            self._slave_constraint = self._slave_constraint.intersect(
                constraint)

    def get_routing_slave_constraint(self):
        if self._slave_constraint is None or not self._slave_constraint.valid:
            self._build_routing_slave_constraint()
        return self._slave_constraint

    def _add_attachment_node(self, parent_offset: Vector2, offset: Vector2,
                             radius, angle_constraint, flags, node_type):
        attachment_node = _RoutingFormationAttachmentNode(
            parent_offset, offset, radius, angle_constraint, flags, node_type)
        self._attachment_chain.append(attachment_node)

    def find_good_location_for_slave(self, master_location):
        restrictions = []
        fgl_kwargs = {}
        fgl_flags = 0
        fgl_tuning = self.fgl_on_routes
        slave_position = master_location.transform.transform_point(
            self._offset)
        orientation = master_location.transform.orientation
        routing_surface = master_location.routing_surface
        if routing_surface is None:
            master_parent = master_location.parent
            if master_parent:
                routing_surface = master_parent.routing_surface
        if self.slave.is_sim or isinstance(self.slave, StubActor):
            (min_water_depth, max_water_depth
             ) = OceanTuning.make_depth_bounds_safe_for_surface_and_sim(
                 routing_surface, self.slave)
        else:
            min_water_depth = None
            max_water_depth = None
        (min_water_depth, max_water_depth
         ) = OceanTuning.make_depth_bounds_safe_for_surface_and_sim(
             routing_surface,
             self.master,
             min_water_depth=min_water_depth,
             max_water_depth=max_water_depth)
        fgl_kwargs.update({
            'min_water_depth': min_water_depth,
            'max_water_depth': max_water_depth
        })
        if fgl_tuning.height_tolerance is not None:
            fgl_kwargs['height_tolerance'] = fgl_tuning.height_tolerance
        if fgl_tuning.slave_should_face_master:
            restrictions.append(
                RelativeFacingRange(master_location.transform.translation, 0))
            fgl_kwargs.update({
                'raytest_radius':
                self.RAYTRACE_RADIUS,
                'raytest_start_offset':
                self.RAYTRACE_HEIGHT,
                'raytest_end_offset':
                self.RAYTRACE_HEIGHT,
                'ignored_object_ids': {self.master.id, self.slave.id},
                'raytest_start_point_override':
                master_location.transform.translation
            })
            fgl_flags = FGLSearchFlag.SHOULD_RAYTEST
            orientation_offset = sims4.math.angle_to_yaw_quaternion(
                sims4.math.vector3_angle(
                    sims4.math.vector_normalize(self._offset)))
            orientation = Quaternion.concatenate(orientation,
                                                 orientation_offset)
        starting_location = placement.create_starting_location(
            position=slave_position,
            orientation=orientation,
            routing_surface=routing_surface)
        if self.slave.is_sim:
            fgl_flags |= FGLSearchFlagsDefaultForSim
            fgl_context = placement.create_fgl_context_for_sim(
                starting_location,
                self.slave,
                search_flags=fgl_flags,
                restrictions=restrictions,
                **fgl_kwargs)
        else:
            fgl_flags |= FGLSearchFlagsDefault
            footprint = self.slave.get_footprint()
            master_position = master_location.position if hasattr(
                master_location,
                'position') else master_location.transform.translation
            fgl_context = FindGoodLocationContext(
                starting_location,
                object_id=self.slave.id,
                object_footprints=(footprint, )
                if footprint is not None else None,
                search_flags=fgl_flags,
                restrictions=restrictions,
                connectivity_group_override_point=master_position,
                **fgl_kwargs)
        (new_position,
         new_orientation) = placement.find_good_location(fgl_context)
        if new_position is None or new_orientation is None:
            logger.warn(
                'No good location found for {} after slaved in a routing formation headed to {}.',
                self.slave,
                starting_location,
                owner='rmccord')
            return sims4.math.Transform(
                Vector3(*starting_location.position),
                Quaternion(*starting_location.orientation))
        new_position.y = services.terrain_service.terrain_object(
        ).get_routing_surface_height_at(new_position.x, new_position.z,
                                        master_location.routing_surface)
        final_transform = sims4.math.Transform(new_position, new_orientation)
        return final_transform

    def on_release(self):
        self._unlock_slave()

    def _setup_right_angle_connections(self):
        formation_offset_x = Vector2(self._formation_offset.x / 6.0, 0.0)
        formation_offset_y = Vector2(0.0, self._formation_offset.y)
        for _ in range(self.ATTACH_NODE_COUNT):
            self._add_attachment_node(
                formation_offset_x, formation_offset_x * -1,
                self.ATTACH_NODE_RADIUS, 0, self.ATTACH_NODE_FLAGS,
                RoutingFormationFollowType.NODE_TYPE_FOLLOW_LEADER)
        self._setup_direct_connections(formation_offset_y)

    def _setup_direct_connections(self, formation_offset):
        formation_vector_magnitude = formation_offset.magnitude()
        normalized_offset = formation_offset / formation_vector_magnitude
        attachment_node_step = formation_vector_magnitude / (
            (self.ATTACH_NODE_COUNT - 1) * 2)
        attachment_vector = normalized_offset * attachment_node_step
        for i in range(0, self.ATTACH_NODE_COUNT - 1):
            flags = self.ATTACH_NODE_FLAGS
            if i == self.ATTACH_NODE_COUNT - 2:
                flags = 5
            self._add_attachment_node(
                attachment_vector, attachment_vector * -1,
                self.ATTACH_NODE_RADIUS, self.ATTACH_NODE_ANGLE, flags,
                RoutingFormationFollowType.NODE_TYPE_CHAIN)

    def should_slave_for_path(self, path):
        path_length = path.length() if path is not None else MAX_INT32
        final_path_node = path.nodes[-1]
        final_position = sims4.math.Vector3(*final_path_node.position)
        final_orientation = sims4.math.Quaternion(*final_path_node.orientation)
        routing_surface = final_path_node.routing_surface_id
        final_position.y = services.terrain_service.terrain_object(
        ).get_routing_surface_height_at(final_position.x, final_position.z,
                                        routing_surface)
        final_transform = sims4.math.Transform(final_position,
                                               final_orientation)
        slave_position = final_transform.transform_point(self._offset)
        slave_position.y = services.terrain_service.terrain_object(
        ).get_routing_surface_height_at(slave_position.x, slave_position.z,
                                        routing_surface)
        final_dist_sq = (slave_position -
                         self.slave.position).magnitude_squared()
        if path_length >= self.route_length_minimum or final_dist_sq >= self.route_length_minimum * self.route_length_minimum:
            return True
        return False

    def build_routing_slave_pb(self, slave_pb, path=None):
        starting_location = path.final_location if path is not None else self.master.intended_location
        slave_transform = self.find_good_location_for_slave(starting_location)
        slave_loc = slave_pb.final_location_override
        (slave_loc.translation.x, slave_loc.translation.y,
         slave_loc.translation.z) = slave_transform.translation
        (slave_loc.orientation.x, slave_loc.orientation.y,
         slave_loc.orientation.z,
         slave_loc.orientation.w) = slave_transform.orientation
        self._final_transform = slave_transform

    def update_slave_position(self,
                              master_transform,
                              master_orientation,
                              routing_surface,
                              distribute=True,
                              path=None,
                              canceled=False):
        master_transform = sims4.math.Transform(master_transform.translation,
                                                master_orientation)
        if distribute and not canceled:
            slave_transform = self._final_transform if self._final_transform is not None else self.slave.transform
            slave_position = slave_transform.translation
        else:
            slave_position = master_transform.transform_point(self._offset)
            slave_transform = sims4.math.Transform(slave_position,
                                                   master_orientation)
        slave_route_distance_sqrd = (self._slave.position -
                                     slave_position).magnitude_squared()
        if path is not None and path.length(
        ) < self.route_length_minimum and slave_route_distance_sqrd < self.route_length_minimum * self.route_length_minimum:
            return
        slave_too_far_from_master = False
        if slave_route_distance_sqrd > self.route_length_maximum * self.route_length_maximum:
            slave_too_far_from_master = True
        if distribute and not slave_too_far_from_master:
            self._slave.move_to(routing_surface=routing_surface,
                                transform=slave_transform)
        else:
            location = self.slave.location.clone(
                routing_surface=routing_surface, transform=slave_transform)
            self.slave.set_location_without_distribution(location)
Example #5
0
class DebugSetupLotInteraction(TerrainImmediateSuperInteraction):
    INSTANCE_TUNABLES = {
        'setup_lot_destroy_old_objects':
        Tunable(bool,
                False,
                description=
                'Destroy objects previously created by this interaction.'),
        'setup_lot_objects':
        TunableList(
            TunableTuple(
                definition=TunableReference(definition_manager()),
                position=TunableVector2(Vector2.ZERO()),
                angle=TunableRange(int, 0, -360, 360),
                children=TunableList(
                    TunableTuple(
                        definition=TunableReference(
                            definition_manager(),
                            description=
                            'The child object to create.  It will appear in the first available slot in which it fits, subject to additional restrictions specified in the other values of this tuning.'
                        ),
                        part_index=OptionalTunable(
                            Tunable(
                                int,
                                0,
                                description=
                                'If specified, restrict slot selection to the given part index.'
                            )),
                        bone_name=OptionalTunable(
                            Tunable(
                                str,
                                '_ctnm_chr_',
                                description=
                                'If specified, restrict slot selection to one with this exact bone name.'
                            )),
                        slot_type=OptionalTunable(
                            TunableReference(
                                manager=services.get_instance_manager(
                                    Types.SLOT_TYPE),
                                description=
                                'If specified, restrict slot selection to ones that support this type of slot.'
                            )),
                        init_state_values=TunableList(
                            description=
                            '\n                                List of states the children object will be set to.\n                                ',
                            tunable=TunableStateValueReference()))),
                init_state_values=TunableList(
                    description=
                    '\n                    List of states the created object will be pushed to.\n                    ',
                    tunable=TunableStateValueReference())))
    }
    _zone_to_cls_to_created_objects = WeakKeyDictionary()

    @classproperty
    def destroy_old_objects(cls):
        return cls.setup_lot_destroy_old_objects

    @classproperty
    def created_objects(cls):
        created_objects = cls._zone_to_cls_to_created_objects.setdefault(
            services.current_zone(), {})
        return setdefault_callable(created_objects, cls, WeakSet)

    def _run_interaction_gen(self, timeline):
        with supress_posture_graph_build():
            if self.destroy_old_objects:
                while self.created_objects:
                    obj = self.created_objects.pop()
                    obj.destroy(
                        source=self,
                        cause='Destroying old objects in setup debug lot.')
            position = self.context.pick.location
            self.spawn_objects(position)
        return True
        yield

    def _create_object(self,
                       definition_id,
                       position=Vector3.ZERO(),
                       orientation=Quaternion.IDENTITY(),
                       level=0,
                       owner_id=0):
        obj = objects.system.create_object(definition_id)
        if obj is not None:
            transform = Transform(position, orientation)
            location = Location(transform, self.context.pick.routing_surface)
            obj.location = location
            obj.set_household_owner_id(owner_id)
            self.created_objects.add(obj)
        return obj

    def spawn_objects(self, position):
        root = sims4.math.Vector3(position.x, position.y, position.z)
        zone = services.current_zone()
        lot = zone.lot
        owner_id = lot.owner_household_id
        if not self.contained_in_lot(lot, root):
            closest_point = self.find_nearest_point_on_lot(lot, root)
            if closest_point is None:
                return False
            radius = (self.top_right_pos -
                      self.bottom_left_pos).magnitude_2d() / 2
            root = closest_point + sims4.math.vector_normalize(
                sims4.math.vector_flatten(lot.center) -
                closest_point) * (radius + 1)
            if not self.contained_in_lot(lot, root):
                sims4.log.warn(
                    'Placement',
                    "Placed the lot objects but the entire bounding box isn't inside the lot. This is ok. If you need them to be inside the lot run the interaction again at a diffrent location."
                )

        def _generate_vector(offset_x, offset_z):
            ground_obj = services.terrain_service.terrain_object()
            ret_vector = sims4.math.Vector3(root.x + offset_x, root.y,
                                            root.z + offset_z)
            ret_vector.y = ground_obj.get_height_at(ret_vector.x, ret_vector.z)
            return ret_vector

        def _generate_quat(rot):
            return sims4.math.Quaternion.from_axis_angle(
                rot, sims4.math.Vector3(0, 1, 0))

        for info in self.setup_lot_objects:
            new_pos = _generate_vector(info.position.x, info.position.y)
            new_rot = _generate_quat(sims4.math.PI / 180 * info.angle)
            new_obj = self._create_object(info.definition,
                                          new_pos,
                                          new_rot,
                                          owner_id=owner_id)
            if new_obj is None:
                sims4.log.error('SetupLot', 'Unable to create object: {}',
                                info)
            else:
                for state_value in info.init_state_values:
                    new_obj.set_state(state_value.state, state_value)
                for child_info in info.children:
                    slot_owner = new_obj
                    if child_info.part_index is not None:
                        for obj_part in new_obj.parts:
                            if obj_part.subroot_index == child_info.part_index:
                                slot_owner = obj_part
                                break
                    bone_name_hash = None
                    if child_info.bone_name is not None:
                        bone_name_hash = hash32(child_info.bone_name)
                    slot_type = None
                    if child_info.slot_type is not None:
                        slot_type = child_info.slot_type
                    for runtime_slot in slot_owner.get_runtime_slots_gen(
                            slot_types={slot_type},
                            bone_name_hash=bone_name_hash):
                        if runtime_slot.is_valid_for_placement(
                                definition=child_info.definition):
                            break
                    else:
                        sims4.log.error(
                            'SetupLot',
                            'Unable to find slot for child object: {}',
                            child_info)
                    child = self._create_object(child_info.definition,
                                                owner_id=owner_id)
                    if child is None:
                        sims4.log.error('SetupLot',
                                        'Unable to create child object: {}',
                                        child_info)
                    else:
                        runtime_slot.add_child(child)
                        for state_value in child_info.init_state_values:
                            child.set_state(state_value.state, state_value)

    def contained_in_lot(self, lot, root):
        self.find_corner_points(root)
        return True

    def find_corner_points(self, root):
        max_x = 0
        min_x = 0
        max_z = 0
        min_z = 0
        for info in self.setup_lot_objects:
            if info.position.x > max_x:
                max_x = info.position.x
            if info.position.x < min_x:
                min_x = info.position.x
            if info.position.y > max_z:
                max_z = info.position.y
            if info.position.y < min_z:
                min_z = info.position.y
        self.top_right_pos = sims4.math.Vector3(root.x + max_x, root.y,
                                                root.z + max_z)
        self.bottom_right_pos = sims4.math.Vector3(root.x + max_x, root.y,
                                                   root.z + min_z)
        self.top_left_pos = sims4.math.Vector3(root.x + min_x, root.y,
                                               root.z + max_z)
        self.bottom_left_pos = sims4.math.Vector3(root.x + min_x, root.y,
                                                  root.z + min_z)

    def find_nearest_point_on_lot(self, lot, root):
        lot_corners = lot.corners
        segments = [(lot_corners[0], lot_corners[1]),
                    (lot_corners[1], lot_corners[2]),
                    (lot_corners[2], lot_corners[3]),
                    (lot_corners[3], lot_corners[1])]
        dist = 0
        closest_point = None
        for segment in segments:
            new_point = sims4.math.get_closest_point_2D(segment, root)
            new_distance = (new_point - root).magnitude()
            if dist == 0:
                dist = new_distance
                closest_point = new_point
            elif new_distance < dist:
                dist = new_distance
                closest_point = new_point
        return closest_point
Example #6
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
Example #7
0
class _PartData(HasTunableSingletonFactory, AutoFactoryInit):
    FACTORY_TUNABLES = {'part_definition': ObjectPart.TunableReference(description='\n            The part definition associated with this part instance.\n            \n            The part definition defines supported postures and interactions,\n            disallowed buffs and portal data.\n            ', pack_safe=True), 'disabling_states': TunableList(description='\n            A list of state values which, if active on this object, will\n            disable this part.\n            ', tunable=TunableStateValueReference(pack_safe=True)), 'adjacent_parts': TunableList(description='\n            The parts that are adjacent to this part. You must reference a part\n            that is tuned in this mapping.\n            \n            An empty list indicates that no part is adjacent to this part.\n            ', tunable=Tunable(tunable_type=str, default=None), unique_entries=True), 'overlapping_parts': TunableList(description='\n            The parts that are unusable when this part is in use. You must\n            reference a part that is tuned in this mapping.\n            ', tunable=Tunable(tunable_type=str, default=None), unique_entries=True), 'subroot_index': OptionalTunable(description='\n            If enabled, this part will have a subroot index associated with it.\n            This will affect the way Sims animate, i.e. they will animate\n            relative to the position of the part, not relative to the object.\n            ', tunable=Tunable(description='\n                The subroot suffix associated with this part.\n                ', tunable_type=int, default=0, needs_tuning=False), enabled_by_default=True), 'anim_overrides': TunableAnimationOverrides(description='\n            Animation overrides for this part.\n            '), 'is_mirrored': OptionalTunable(description='\n            Specify whether or not solo animations played on this part\n            should be mirrored or not.\n            ', tunable=Tunable(description='\n                If checked, mirroring is enabled. If unchecked,\n                mirroring is disabled.\n                ', tunable_type=bool, default=False)), 'forward_direction_for_picking': TunableVector2(description="\n            When you click on the object this part belongs to, this offset will\n            be applied to this part when determining which part is closest to\n            where you clicked.\n            \n            By default, the object's forward vector will be used. It should only\n            be necessary to tune this value if multiple parts overlap at the\n            same location (e.g. the single bed).\n            ", default=TunableVector2.DEFAULT_Z, x_axis_name='x', y_axis_name='z'), 'disable_sim_aop_forwarding': Tunable(description='\n            If checked, Sims using this specific part will never forward\n            AOPs.\n            ', tunable_type=bool, default=False), 'disable_child_aop_forwarding': Tunable(description='\n            If checked, objects parented to this specific part will\n            never forward AOPs.\n            ', tunable_type=bool, default=False), 'restrict_autonomy_preference': Tunable(description='\n            If checked, this specific part can be used for use only autonomy preference\n            restriction.\n            ', tunable_type=bool, default=False), 'name': OptionalTunable(description='\n            Name of this part.  For use if the part name needs to be surfaced\n            to the player.  (i.e. when assigning sim to specific side of bed.)\n            ', tunable=TunableLocalizedString())}
Example #8
0
 def __init__(self, description='Generate a privacy region for this object', callback=None, **kwargs):
     super().__init__(tests=TunableTestSet(description='\n                Any Sim who passes these tests will be allowed to violate the\n                privacy region.\n                '), shoo_exempt_tests=TunableTestSet(description='\n                Any violator who passes these tests will still be considered a\n                violator, but ill be exempt from being shooed.\n                i.e. A cat will get shooed when it breaks a privacy region, but\n                cats will ignore the shoo behavior.\n                '), max_line_of_sight_radius=Tunable(description='\n                The maximum possible distance from this object than an\n                interaction can reach.\n                ', tunable_type=float, default=5), map_divisions=Tunable(description='\n                The number of points around the object to check collision from.\n                More points means higher accuracy.\n                ', tunable_type=int, default=30), simplification_ratio=Tunable(description='\n                A factor determining how much to combine edges in the line of\n                sight polygon.\n                ', tunable_type=float, default=0.25), boundary_epsilon=Tunable(description='\n                The LOS origin is allowed to be outside of the boundary by this\n                amount.\n                ', tunable_type=float, default=0.01), facing_offset=Tunable(description='\n                The LOS origin is offset from the object origin by this amount\n                (mainly to avoid intersecting walls).\n                ', tunable_type=float, default=0.1), routing_surface_only=Tunable(description="\n                If this is checked, then the privacy constraint is generated on\n                the surface defined by the interaction's target. If the\n                interaction has no target or the target does not provide a\n                routable surface, no privacy is generated.\n                \n                Furthermore, privacy that is exclusive to routing surface will\n                only shoo Sims that are on the routable surface.\n                \n                e.g. A Sim cleaning a counter needs to shoo cats on the counter.\n                \n                The default behavior is for privacy to be generated on the\n                surface the Sim is on, and for it to apply to Sims on all\n                surfaces.\n                \n                e.g. A Sim using the toilet would shoo cats within the privacy\n                region that happen to be on routable surfaces, such as counters.\n                ", tunable_type=bool, default=False), shoo_constraint_radius=OptionalTunable(description='\n                If enabled, you can tune a specific radius for the shoo\n                constraint. If disabled, the values tuned in the Privacy module\n                tuning will be used.\n                ', tunable=Tunable(description='\n                    The radius of the constraint a Shooed Sim will attempt to\n                    route to.\n                    ', tunable_type=float, default=2.5), disabled_name=self.OPTIONAL_TUNABLE_DISABLED_NAME), unavailable_tooltip=OptionalTunable(description='\n                If enabled, allows a custom tool tip to be displayed when the\n                player tries to run an interaction on an object inside the\n                privacy region. If disabled, the values tuned in the Privacy\n                module tuning will be used.\n                ', tunable=TunableLocalizedStringFactory(description='\n                    Tool tip displayed when an object is not accessible due to\n                    being inside a privacy region.\n                    '), disabled_name=self.OPTIONAL_TUNABLE_DISABLED_NAME), embarrassed_affordance=OptionalTunable(description='\n                If enabled, a specific affordance can be tuned for a Sim to\n                play when walking into the privacy region. If disabled, the\n                values tuned in the Privacy module tuning will be used.\n                ', tunable=TunableReference(description='\n                    The affordance a Sim will play when getting embarrassed by\n                    walking in on a privacy situation.\n                    ', manager=services.get_instance_manager(Types.INTERACTION)), disabled_name=self.OPTIONAL_TUNABLE_DISABLED_NAME), post_route_affordance=OptionalTunable(description='\n                Optionally define an interaction that will run after the Sim\n                routes away.\n                ', tunable=TunableReference(description='\n                    The affordance a Sim will play when getting embarrassed by\n                    walking in on a privacy situation.\n                    ', manager=services.get_instance_manager(Types.INTERACTION))), privacy_cost_override=OptionalTunable(description='\n                If set, override the cost of the privacy region.\n                ', tunable=TunableRange(tunable_type=int, default=20, minimum=1), disabled_name=self.OPTIONAL_TUNABLE_DISABLED_NAME), additional_exit_offsets=TunableList(description="\n                If set, adds additional exit goals to add to the satisfy shoo\n                constraint.  For most cases this isn't needed, since most\n                privacy situations may kick a player out of a room through\n                a door and there are few exit options. \n                However for open-space privacy areas, default behavior\n                (using zone's corners) can cause a Sim to always attempt to exit \n                the privacy area in a consistent and often not optimal route, \n                (e.g. an open cross-shaped hall with 4 ways out, with default\n                behavior the Sim could consistently choose to exit using \n                the same route even though other routes would yield \n                a shorter distance out of the privacy region)\n                ", tunable=TunableVector2(default=TunableVector2.DEFAULT_ZERO)), reserved_surface_space=OptionalTunable(description='\n                If enabled privacy will generate an additional footprint around\n                the target object surface (if  routing_surface_only is enabled\n                then this will happen on the object routable surface). \n                This footprint will affect any Sim from routing through for the \n                duration of the interaction.\n                ', tunable=TunableTuple(description='\n                    Reserved space and blocking options for the created\n                    footprint.\n                    ', allow_routing=Tunable(description='\n                        If True, then the footprint will only discourage \n                        routing, instead of blocking the whole area from\n                        being used.\n                        ', tunable_type=bool, default=True), reserved_space=TunableReservedSpace(description='\n                        Defined space to generate the Jig that will block the \n                        routable surface space..\n                        ')), enabled_name='define_blocking_area'), vehicle_tests=OptionalTunable(description='\n                If enabled, vehicles that pass through this privacy region will\n                be tested to see if the vehicle is allowed in the privacy\n                region. Otherwise, the vehicle will always be affected by\n                privacy.\n                Note: The Object Routing Component specifies what happens when\n                the drone enters a privacy region.\n                ', tunable=TunableTestSet(description='\n                    The tests that the vehicle must pass to be allowed in the\n                    privacy region. \n                    Note: The Object Routing Component specifies what happens\n                    when the drone enters a privacy region.\n                    ')), privacy_violators=TunableEnumFlags(description='\n                Defines violators of privacy: currently, only SIM and VEHICLES are suppported.\n                for example: if  PrivacyViolators.SIM is false , but PrivacyViolators.VEHICLES is enabled\n                sims need not obey privacy rules for the object at hand.\n                ', enum_type=PrivacyViolators, allow_no_flags=True, default=PrivacyViolators.SIM | PrivacyViolators.VEHICLES), verify_tunable_callback=TunablePrivacy.verify_tunable_callback, description=description, **kwargs)
Example #9
0
class RelationshipTrack(TunedContinuousStatistic,
                        HasTunableReference,
                        metaclass=HashedTunedInstanceMetaclass,
                        manager=services.statistic_manager()):
    __qualname__ = 'RelationshipTrack'
    FRIENDSHIP_TRACK = TunableReference(
        description=
        '\n        A reference to the friendship track so that the client knows which\n        track is the friendship one.\n        ',
        manager=services.statistic_manager(),
        class_restrictions='RelationshipTrack',
        export_modes=sims4.tuning.tunable_base.ExportModes.All)
    FRIENDSHIP_TRACK_FILTER_THRESHOLD = Tunable(
        description=
        '\n        Value that the client will use when filtering friendship on the Sim\n        Picker.  Sims that have a track value equal to or above this value will\n        be shown with the friendship filter.\n        ',
        tunable_type=int,
        default=0,
        export_modes=sims4.tuning.tunable_base.ExportModes.All)
    ROMANCE_TRACK = TunableReference(
        description=
        '\n        A reference to the romance track so that the client knows which\n        track is the romance one.\n        ',
        manager=services.statistic_manager(),
        class_restrictions='RelationshipTrack',
        export_modes=sims4.tuning.tunable_base.ExportModes.All)
    ROMANCE_TRACK_FILTER_THRESHOLD = Tunable(
        description=
        '\n        Value that the client will use when filtering romance on the Sim\n        Picker.  Sims that have a track value equal to or above this value will\n        be shown with the romance filter.\n        ',
        tunable_type=int,
        default=0,
        export_modes=sims4.tuning.tunable_base.ExportModes.All)
    ROMANCE_TRACK_FILTER_BITS = TunableSet(
        description=
        '\n        A set of relationship bits that will be used in the Sim Picker for\n        filtering based on romance.  If a Sim has any of these bits then they\n        will be displayed in the Sim Picker when filtering for romance.\n        ',
        tunable=TunableReference(
            description=
            '\n                A specific bit used for filtering romance in the Sim Picker.\n                ',
            manager=services.get_instance_manager(
                sims4.resources.Types.RELATIONSHIP_BIT)),
        export_modes=sims4.tuning.tunable_base.ExportModes.All)
    REMOVE_INSTANCE_TUNABLES = ('stat_asm_param', 'persisted_tuning')
    INSTANCE_TUNABLES = {
        'bit_data_tuning':
        TunableVariant(bit_set=TunableRelationshipBitData(),
                       _2dMatrix=TunableRelationshipTrack2dLink()),
        'ad_data':
        TunableList(
            TunableVector2(sims4.math.Vector2(0, 0),
                           description='Point on a Curve'),
            description=
            'A list of Vector2 points that define the desire curve for this relationship track.'
        ),
        'relationship_obj_prefence_curve':
        TunableWeightedUtilityCurveAndWeight(
            description=
            "A curve that maps desire of a sim to interact with an object made by a sim of this relation to this relationship's actual score."
        ),
        '_add_bit_on_threshold':
        OptionalTunable(
            description=
            '\n                If enabled, the referenced bit will be added this track reaches the threshold.\n                ',
            tunable=TunableTuple(
                description=
                '\n                    The bit & threshold pair.\n                    ',
                bit=TunableReference(
                    description=
                    '\n                        The bit to add.\n                        ',
                    manager=services.get_instance_manager(
                        sims4.resources.Types.RELATIONSHIP_BIT)),
                threshold=TunableThreshold(
                    description=
                    '\n                        The threshold at which to add this bit.\n                        '
                ))),
        'display_priority':
        TunableRange(
            description=
            '\n                The display priority of this relationship track.  Tracks with a\n                display priority greater than zero will be displayed in\n                ascending order in the UI.  So a relationship track with a\n                display priority of 1 will show above a relationship track\n                with a display priority of 2.  Relationship tracks with the\n                same display priority will show up in potentially\n                non-deterministic ways.  Relationship tracks with display\n                priorities of 0 will not be shown.\n                ',
            tunable_type=int,
            default=0,
            minimum=0,
            export_modes=sims4.tuning.tunable_base.ExportModes.All),
        'display_popup_priority':
        TunableRange(
            description=
            '\n                The display popup priority.  This is the priority that the\n                relationship score increases will display. If there are\n                multiple relationship changes at the same time.\n                ',
            tunable_type=int,
            default=0,
            minimum=0,
            export_modes=sims4.tuning.tunable_base.ExportModes.All),
        '_neutral_bit':
        TunableReference(
            description=
            "\n                The neutral bit for this relationship track.  This is the bit\n                that is displayed when there are holes in the relationship\n                track's bit data.\n                ",
            manager=services.get_instance_manager(
                sims4.resources.Types.RELATIONSHIP_BIT)),
        'decay_only_affects_selectable_sims':
        Tunable(
            description=
            '\n                If this is True, the decay is only enabled if one or both of \n                the sims in the relationship are selectable. \n                ',
            tunable_type=bool,
            default=False),
        'delay_until_decay_is_applied':
        OptionalTunable(
            description=
            '\n                If enabled, the decay for this track will be disabled whenever\n                the value changes by any means other than decay.  It will then \n                be re-enabled after this amount of time (in sim minutes) passes.\n                ',
            tunable=TunableRange(
                description=
                '\n                    The amount of time, in sim minutes, that it takes before \n                    decay is enabled.\n                    ',
                tunable_type=int,
                default=10,
                minimum=1)),
        'causes_delayed_removal_on_convergence':
        Tunable(
            description=
            '\n                If True, this track may cause the relationship to get culled when \n                it reaches convergence.  This is not guaranteed, based on the \n                culling rules.  Sim relationships will NOT be culled if any of \n                the folling conditions are met:\n                - The sim has any relationship bits that are tuned to prevent this.\n                - The Sims are in the same household\n                ',
            tunable_type=bool,
            default=False),
        'visible_test_set':
        OptionalTunable(event_testing.tests.TunableTestSet(
            description=
            '\n                If set , tests whether relationship should be sent to client.\n                If no test given, then as soon as track is added to relationship\n                it will be visible to client.\n                '
        ),
                        disabled_value=DEFAULT,
                        disabled_name='always_visible',
                        enabled_name='run_test')
    }
    bit_data = None

    def __init__(self, tracker):
        super().__init__(tracker, self.initial_value)
        self._per_instance_data = self.bit_data.get_track_instance_data(self)
        self.visible_to_client = True if self.visible_test_set is DEFAULT else False
        self._decay_alarm_handle = None
        self._convergence_callback_data = None
        self._first_same_sex_relationship_callback_data = None
        self._set_initial_decay()

    @classproperty
    def is_short_term_context(cls):
        return False

    def on_add(self):
        if not self.tracker.suppress_callback_setup_during_load:
            self._per_instance_data.setup_callbacks()
            self.update_instance_data()
        if self._add_bit_on_threshold is not None:
            self.add_callback(self._add_bit_on_threshold.threshold,
                              self._on_add_bit_from_threshold_callback)
        if self._should_initialize_first_same_sex_relationship_callback():
            self._first_same_sex_relationship_callback_data = self.add_callback(
                Threshold(
                    sims.global_gender_preference_tuning.
                    GlobalGenderPreferenceTuning.
                    ENABLE_AUTOGENERATION_SAME_SEX_PREFERENCE_THRESHOLD,
                    operator.ge), self._first_same_sex_relationship_callback)

    def on_remove(self, on_destroy=False):
        self.remove_callback(self._first_same_sex_relationship_callback_data)
        super().on_remove(on_destroy=on_destroy)
        self._destroy_decay_alarm()

    def get_statistic_multiplier_increase(self):
        sim_info_manager = services.sim_info_manager()
        target_sim_info = sim_info_manager.get(
            self.tracker.relationship.target_sim_id)
        target_sim_multiplier = 1
        if target_sim_info is not None:
            target_sim_stat = target_sim_info.relationship_tracker.get_relationship_track(
                self.tracker.relationship.sim_id, track=self.stat_type)
            if target_sim_stat is not None:
                target_sim_multiplier = target_sim_stat._statistic_multiplier_increase
        return self._statistic_multiplier_increase * target_sim_multiplier

    def get_statistic_multiplier_decrease(self):
        sim_info_manager = services.sim_info_manager()
        target_sim_info = sim_info_manager.get(
            self.tracker.relationship.target_sim_id)
        target_sim_multiplier = 1
        if target_sim_info is not None:
            target_sim_stat = target_sim_info.relationship_tracker.get_relationship_track(
                self.tracker.relationship.sim_id, track=self.stat_type)
            if target_sim_stat is not None:
                target_sim_multiplier = target_sim_stat._statistic_multiplier_decrease
        return self._statistic_multiplier_decrease * target_sim_multiplier

    def set_value(self, value, *args, **kwargs):
        self._update_value()
        old_value = self._value
        delta = value - old_value
        sim_info = self.tracker.relationship.find_sim_info()
        self.tracker.trigger_test_event(
            sim_info,
            event_testing.test_events.TestEvent.PrerelationshipChanged)
        super().set_value(value, *args, **kwargs)
        self._update_visiblity()
        self._reset_decay_alarm()
        self.tracker.relationship.send_relationship_info(deltas={self: delta})
        self.tracker.trigger_test_event(
            sim_info, event_testing.test_events.TestEvent.RelationshipChanged)

    @property
    def is_visible(self):
        return self.visible_to_client

    def fixup_callbacks_during_load(self):
        super().fixup_callbacks_during_load()
        self._per_instance_data.setup_callbacks()

    def update_instance_data(self):
        self._per_instance_data.request_full_update()

    def apply_social_group_decay(self):
        pass

    def remove_social_group_decay(self):
        pass

    def _on_statistic_modifier_changed(self, notify_watcher=True):
        super()._on_statistic_modifier_changed(notify_watcher=notify_watcher)
        if self._statistic_modifier == 0:
            self._reset_decay_alarm()
        self.tracker.relationship.send_relationship_info()

    def _update_visiblity(self):
        if not self.visible_to_client:
            sim_info_manager = services.sim_info_manager()
            actor_sim_info = sim_info_manager.get(
                self.tracker.relationship.sim_id)
            if actor_sim_info is None:
                return
            target_sim_info = sim_info_manager.get(
                self.tracker.relationship.target_sim_id)
            if target_sim_info is None:
                return
            resolver = DoubleSimResolver(actor_sim_info, target_sim_info)
            self.visible_to_client = True if self.visible_test_set.run_tests(
                resolver) else False

    @classmethod
    def _tuning_loaded_callback(cls):
        super()._tuning_loaded_callback()
        cls.bit_data = cls.bit_data_tuning()
        cls.bit_data.build_track_data()
        cls._build_utility_curve_from_tuning_data(cls.ad_data)

    @classmethod
    def _verify_tuning_callback(cls):
        if cls._neutral_bit is None:
            logger.error('No Neutral Bit tuned for Relationship Track: {}',
                         cls)

    @staticmethod
    def check_relationship_track_display_priorities(statistic_manager):
        if not __debug__:
            return
        relationship_track_display_priority = collections.defaultdict(list)
        for statistic in statistic_manager.types.values():
            if not issubclass(statistic, RelationshipTrack):
                pass
            if statistic.display_priority == 0:
                pass
            relationship_track_display_priority[
                statistic.display_priority].append(statistic)
        for relationship_priority_level in relationship_track_display_priority.values(
        ):
            if len(relationship_priority_level) <= 1:
                pass
            logger.warn(
                'Multiple Relationship Tracks have the same display priority: {}',
                relationship_priority_level)

    @classmethod
    def type_id(cls):
        return cls.guid64

    @classmethod
    def get_bit_track_node_for_bit(cls, relationship_bit):
        for node in cls.bit_data.bit_track_node_gen():
            while node.bit is relationship_bit:
                return node

    @classmethod
    def bit_track_node_gen(cls):
        for node in cls.bit_data.bit_track_node_gen():
            yield node

    @classmethod
    def get_bit_at_relationship_value(cls, value):
        for bit_node in reversed(tuple(cls.bit_track_node_gen())):
            while bit_node.min_rel <= value <= bit_node.max_rel:
                return bit_node.bit or cls._neutral_bit
        return cls._neutral_bit

    @classproperty
    def persisted(cls):
        return True

    def get_active_bit(self):
        return self._per_instance_data.get_active_bit()

    def get_bit_for_client(self):
        active_bit = self.get_active_bit()
        if active_bit is None:
            return self._neutral_bit
        return active_bit

    def _set_initial_decay(self):
        if self._should_decay():
            self.decay_enabled = True
        self._convergence_callback_data = self.add_callback(
            Threshold(self.convergence_value, operator.eq),
            self._on_convergence_callback)
        logger.debug('Setting decay on track {} to {} for {}', self,
                     self.decay_enabled, self.tracker.relationship)

    def _should_decay(self):
        if self.decay_rate == 0:
            return False
        if self.decay_only_affects_selectable_sims:
            sim_info = self.tracker.relationship.find_sim_info()
            target_sim_info = self.tracker.relationship.find_target_sim_info()
            if sim_info is None or target_sim_info is None:
                return False
            if sim_info.is_selectable or target_sim_info.is_selectable:
                return True
        else:
            return True
        return False

    def _reset_decay_alarm(self):
        self._destroy_decay_alarm()
        if self._should_decay(
        ) and self.delay_until_decay_is_applied is not None:
            logger.debug('Resetting decay alarm for track {} for {}.', self,
                         self.tracker.relationship)
            delay_time_span = date_and_time.create_time_span(
                minutes=self.delay_until_decay_is_applied)
            self._decay_alarm_handle = alarms.add_alarm(
                self, delay_time_span, self._decay_alarm_callback)
            self.decay_enabled = False

    def _decay_alarm_callback(self, handle):
        logger.debug('Decay alarm triggered on track {} for {}.', self,
                     self.tracker.relationship)
        self._destroy_decay_alarm()
        self.decay_enabled = True

    def _destroy_decay_alarm(self):
        if self._decay_alarm_handle is not None:
            alarms.cancel_alarm(self._decay_alarm_handle)
            self._decay_alarm_handle = None

    def _on_convergence_callback(self, _):
        logger.debug(
            'Track {} reached convergence; rel might get culled for {}', self,
            self.tracker.relationship)
        self.tracker.relationship.track_reached_convergence(self)

    def _on_add_bit_from_threshold_callback(self, _):
        logger.debug('Track {} is adding its extra bit: {}'.format(
            self, self._add_bit_on_threshold.bit))
        self.tracker.relationship.add_bit(self._add_bit_on_threshold.bit)

    def _should_initialize_first_same_sex_relationship_callback(self):
        if self.stat_type is not self.ROMANCE_TRACK:
            return False
        if sims.global_gender_preference_tuning.GlobalGenderPreferenceTuning.enable_autogeneration_same_sex_preference:
            return False
        sim_info_a = self.tracker.relationship.find_sim_info()
        sim_info_b = self.tracker.relationship.find_target_sim_info()
        if sim_info_a is None or sim_info_b is None:
            return False
        if sim_info_a.gender is not sim_info_b.gender:
            return False
        if sim_info_a.is_npc and sim_info_b.is_npc:
            return False
        return True

    def _first_same_sex_relationship_callback(self, _):
        sims.global_gender_preference_tuning.GlobalGenderPreferenceTuning.enable_autogeneration_same_sex_preference = True
        self.remove_callback(self._first_same_sex_relationship_callback_data)
Example #10
0
class BaseRelationshipTrack:
    INSTANCE_TUNABLES = {
        'bit_data_tuning':
        TunableVariant(
            description=
            '\n            Bit tuning for all the bits that compose this relationship \n            track.\n            The structure tuned here, either 2d or simple track should include \n            bits for all the possible range of the track.\n            ',
            bit_set=TunableRelationshipBitData(),
            _2dMatrix=TunableRelationshipTrack2dLink()),
        '_neutral_bit':
        TunableReference(
            description=
            "\n            The neutral bit for this relationship track.  This is the bit\n            that is displayed when there are holes in the relationship\n            track's bit data.\n            ",
            manager=services.get_instance_manager(
                sims4.resources.Types.RELATIONSHIP_BIT),
            tuning_group=GroupNames.CORE),
        'ad_data':
        TunableList(
            description=
            '\n            A list of Vector2 points that define the desire curve for this \n            relationship track.\n            ',
            tunable=TunableVector2(
                description=
                '\n                Point on a Curve.\n                ',
                default=sims4.math.Vector2(0, 0)),
            tuning_group=GroupNames.SPECIAL_CASES),
        '_add_bit_on_threshold':
        OptionalTunable(
            description=
            '\n            If enabled, the referenced bit will be added this track reaches the\n            threshold.\n            ',
            tunable=TunableTuple(
                description=
                '\n                The bit & threshold pair.\n                ',
                bit=TunableReference(
                    description=
                    '\n                    The bit to add.\n                    ',
                    manager=services.get_instance_manager(
                        sims4.resources.Types.RELATIONSHIP_BIT)),
                threshold=TunableThreshold(
                    description=
                    '\n                    The threshold at which to add this bit.\n                    '
                )),
            tuning_group=GroupNames.CORE),
        'causes_delayed_removal_on_convergence':
        Tunable(
            description=
            '\n            If True, this track may cause the relationship to get culled\n            when it reaches convergence.  This is not guaranteed, based on\n            the culling rules.  Sim relationships will NOT be culled if any\n            of the folling conditions are met: \n            - Sim has any relationship bits that are tuned to prevent this. \n            - The sims are in the same household\n            \n            Note: This value is ignored by the Relationship Culling Story\n            Progression Action.\n            ',
            tunable_type=bool,
            default=False,
            tuning_group=GroupNames.CORE),
        'tested_initial_modifier':
        OptionalTunable(
            description=
            '\n            If enabled, a modifier will be applied to the initial value when\n            the track is created.\n            ',
            tunable=TestedSum.TunableFactory(
                description=
                '\n                The test to run and the outcome if test passes.\n                '
            ),
            tuning_group=GroupNames.CORE),
        'visible_test_set':
        OptionalTunable(event_testing.tests.TunableTestSet(
            description=
            '\n                If set, tests whether relationship should be sent to client. If\n                no test given, then as soon as track is added to the\n                relationship, it will be visible to client.\n                '
        ),
                        disabled_value=DEFAULT,
                        disabled_name='always_visible',
                        enabled_name='run_test',
                        tuning_group=GroupNames.SPECIAL_CASES),
        'delay_until_decay_is_applied':
        OptionalTunable(
            description=
            '\n            If enabled, the decay for this track will be disabled whenever\n            the value changes by any means other than decay.  It will then \n            be re-enabled after this amount of time (in sim minutes) passes.\n            ',
            tunable=TunableRange(
                description=
                '\n                The amount of time, in sim minutes, that it takes before \n                decay is enabled.\n                ',
                tunable_type=int,
                default=10,
                minimum=1),
            tuning_group=GroupNames.DECAY),
        'display_priority':
        TunableRange(
            description=
            '\n            The display priority of this relationship track.  Tracks with a\n            display priority greater than zero will be displayed in ascending\n            order in the UI.\n            \n            So a relationship track with a display priority of 1 will show\n            above a relationship track with a display priority of 2.\n            Relationship tracks with the same display priority will show up\n            in potentially non-deterministic ways.  Relationship tracks\n            with display priorities of 0 will not be shown.\n            ',
            tunable_type=int,
            default=0,
            minimum=0,
            tuning_group=GroupNames.UI),
        'headline':
        OptionalTunable(
            description=
            '\n            If enabled when this relationship track updates we will display\n            a headline update to the UI.\n            ',
            tunable=TunableReference(
                description=
                '\n                The headline that we want to send down.\n                ',
                manager=services.get_instance_manager(
                    sims4.resources.Types.HEADLINE)),
            tuning_group=GroupNames.UI),
        'display_popup_priority':
        TunableRange(
            description=
            '\n            The display popup priority.  This is the priority that the\n            relationship score increases will display if there are multiple\n            relationship changes at the same time.\n            ',
            tunable_type=int,
            default=0,
            minimum=0,
            tuning_group=GroupNames.UI),
        'persist_at_convergence':
        Tunable(
            description=
            '\n            If unchecked, this track will not be persisted if it is at\n            convergence. This prevents a ton of tracks, in particular short\n            term context tracks, from piling up on relationships with a value\n            of 0.\n            \n            If checked, the track will be persisted even if it is at 0. This\n            should only used on tracks where its presence matters.\n            ',
            tunable_type=bool,
            default=False,
            tuning_group=GroupNames.SPECIAL_CASES)
    }

    def __init__(self, tracker):
        super().__init__(tracker, self.initial_value)
        self._per_instance_data = self.bit_data.get_track_instance_data(self)
        self.visible_to_client = True if self.visible_test_set is DEFAULT else False
        self._decay_alarm_handle = None
        self._cached_ticks_until_decay_begins = -1
        self._convergence_callback_data = None
        self._set_initial_decay()
        if not self._tracker.suppress_callback_setup_during_load:
            self._create_convergence_callback()

    def on_add(self):
        if not self.tracker.suppress_callback_setup_during_load:
            self._per_instance_data.setup_callbacks()
            (old_bit, new_bit) = self.update_instance_data()
            if not self.tracker.load_in_progress:
                sim_id_a = self.tracker.rel_data.relationship.sim_id_a
                sim_id_b = self.tracker.rel_data.relationship.sim_id_b
                if old_bit is not None and old_bit is not new_bit:
                    self.tracker.rel_data.relationship.remove_bit(
                        sim_id_a, sim_id_b, old_bit)
                if new_bit is not None and not self.tracker.rel_data.relationship.has_bit(
                        sim_id_a, new_bit):
                    self.tracker.rel_data.relationship.add_relationship_bit(
                        sim_id_a, sim_id_b, new_bit)
        if self._add_bit_on_threshold is not None:
            self.create_and_add_callback_listener(
                self._add_bit_on_threshold.threshold,
                self._on_add_bit_from_threshold_callback)

    def _set_initial_decay(self):
        if self._should_decay():
            self.decay_enabled = True

    def _should_decay(self):
        if self.decay_rate == 0:
            return False
        if self.tracker.is_track_locked(self):
            return False
        if self.tracker.rel_data.relationship.is_object_rel():
            return True
        if self.decay_only_affects_played_sims:
            if not services.sim_info_manager():
                return False
            sim_info = self.tracker.rel_data.relationship.find_sim_info_a()
            target_sim_info = self.tracker.rel_data.relationship.find_sim_info_b(
            )
            if sim_info is None or target_sim_info is None:
                return False
            active_household = services.active_household()
            if active_household is None:
                return False
            if sim_info in active_household or target_sim_info in active_household:
                return True
            if sim_info.is_player_sim or target_sim_info.is_player_sim:
                if not self.tracker.rel_data.relationship.can_cull_relationship(
                        consider_convergence=False):
                    return False
                current_value = self.get_value()
                if self.decay_affecting_played_sims.range_decay_threshold.lower_bound < current_value < self.decay_affecting_played_sims.range_decay_threshold.upper_bound:
                    return True
        else:
            return True
        return False

    def _create_convergence_callback(self):
        if self._convergence_callback_data is None:
            self._convergence_callback_data = self.create_and_add_callback_listener(
                Threshold(self.convergence_value, operator.eq),
                self._on_convergence_callback)
        else:
            logger.error(
                'Track {} attempted to create convergence callback twice.'.
                format(self))

    def _on_convergence_callback(self, _):
        logger.debug(
            'Track {} reached convergence; rel might get culled for {}', self,
            self.tracker.rel_data)
        self.tracker.rel_data.track_reached_convergence(self)

    @classmethod
    def _tuning_loaded_callback(cls):
        super()._tuning_loaded_callback()
        cls.bit_data = cls.bit_data_tuning()
        cls.bit_data.build_track_data()
        cls._build_utility_curve_from_tuning_data(cls.ad_data)

    def fixup_callbacks_during_load(self):
        self._create_convergence_callback()
        super().fixup_callbacks_during_load()
        self._per_instance_data.setup_callbacks()

    def update_instance_data(self):
        return self._per_instance_data.request_full_update()

    def reset_decay_alarm(self, use_cached_time=False):
        self._destroy_decay_alarm()
        if self.delay_until_decay_is_applied is not None:
            if self._should_decay():
                delay_time_span = None
                if use_cached_time:
                    if self._cached_ticks_until_decay_begins > 0:
                        delay_time_span = date_and_time.TimeSpan(
                            self._cached_ticks_until_decay_begins)
                    elif self._cached_ticks_until_decay_begins == 0:
                        self.decay_enabled = True
                        return
                if delay_time_span is None:
                    delay_time_span = date_and_time.create_time_span(
                        minutes=self.delay_until_decay_is_applied)
                self._decay_alarm_handle = alarms.add_alarm(
                    self,
                    delay_time_span,
                    self._decay_alarm_callback,
                    cross_zone=True)
                self.decay_enabled = False

    def get_bit_for_client(self):
        active_bit = self.get_active_bit()
        if active_bit is None:
            return self._neutral_bit
        return active_bit

    def get_active_bit(self):
        return self._per_instance_data.get_active_bit()

    def _destroy_decay_alarm(self):
        if self._decay_alarm_handle is not None:
            alarms.cancel_alarm(self._decay_alarm_handle)
            self._decay_alarm_handle = None

    def get_saved_ticks_until_decay_begins(self):
        if self.decay_enabled:
            return 0
        if self._decay_alarm_handle:
            return self._decay_alarm_handle.get_remaining_time().in_ticks()
        return self._cached_ticks_until_decay_begins

    def set_time_until_decay_begins(self, ticks_until_decay_begins):
        if self.delay_until_decay_is_applied is None:
            self._cached_ticks_until_decay_begins = ticks_until_decay_begins
            if self._cached_ticks_until_decay_begins != 0.0 and self._cached_ticks_until_decay_begins != -1.0:
                logger.error('Rel Track {} loaded with bad persisted value {}',
                             self, self._cached_ticks_until_decay_begins)
            return
        max_tuning = date_and_time.create_time_span(
            minutes=self.delay_until_decay_is_applied).in_ticks()
        self._cached_ticks_until_decay_begins = min(ticks_until_decay_begins,
                                                    max_tuning)
        if self._cached_ticks_until_decay_begins < -1.0:
            logger.error('Rel Track {} loaded with bad persisted value {}',
                         self, self._cached_ticks_until_decay_begins)

    def update_track_index(self, relationship):
        self._per_instance_data.full_load_update(relationship)
Example #11
0
class Street(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.street_manager()):
    WORLD_DESCRIPTION_TUNING_MAP = TunableMapping(description='\n        A mapping between Catalog world description and street tuning instance.\n        This way we can find out what world description the current zone\n        belongs to at runtime then grab its street tuning instance.\n        ', key_type=TunableWorldDescription(description='\n            Catalog-side World Description.\n            ', pack_safe=True), value_type=TunableReference(description="\n            Street Tuning instance. This is retrieved at runtime based on what\n            the active zone's world description is.\n            ", pack_safe=True, manager=services.street_manager()), key_name='WorldDescription', value_name='Street')
    INSTANCE_TUNABLES = {'open_street_director': TunablePackSafeReference(description='\n            The Scheduling Open Street Director to use for this world file.\n            This open street director will be able to load object layers and\n            spin up situations.\n            ', manager=services.get_instance_manager(sims4.resources.Types.OPEN_STREET_DIRECTOR)), 'travel_lot': OptionalTunable(description='\n            If enabled then this street will have a specific lot that it will\n            want to travel to when we travel to this "street."\n            ', tunable=TunableLotDescription(description='\n                The specific lot that we will travel to when asked to travel to\n                this street.\n                ')), 'townie_demographics': TunableTuple(description='\n            Townie population demographics for the street.\n            ', target_population=OptionalTunable(description='\n                If enabled, Sims created for other purposes will passively be\n                assigned to live on this street, gaining the filter features.\n                Sims are assigned out in round robin fashion up until all\n                streets have reached their target, after which those streets\n                will be assigned Sims in round robin fashion past their target.\n                \n                If disabled, this street will not passively be assigned townies\n                unless the Lives On Street filter explicitly requires the\n                Sim to be on the street.\n                ', tunable=TunableRange(description="\n                    The ideal number of townies that live on the street.\n                    \n                    0 is valid if you don't want Sims to live on this street\n                    while other streets haven't met their target population.\n                    ", tunable_type=int, default=1, minimum=0)), filter_features=TunableList(description='\n                Sims created as townies living on this street, they will gain\n                one set of features in this list. Features are applied as\n                Sim creation tags and additional filter terms to use.\n                ', tunable=TunableTuple(description='\n                    ', filter_terms=TunableList(description='\n                        Filter terms to inject into the filter.\n                        ', tunable=FilterTermVariant(conform_optional=True)), sim_creator_tags=TunableReference(description="\n                        Tags to inject into the filter's Sim template.\n                        ", manager=services.get_instance_manager(sims4.resources.Types.TAG_SET), allow_none=True, class_restrictions=('TunableTagSet',)), sim_name_type=TunableEnumEntry(description='\n                        What type of name the sim should have.\n                        ', tunable_type=SimNameType, default=SimNameType.DEFAULT), weight=TunableRange(description='\n                        Weighted chance.\n                        ', tunable_type=float, default=1, minimum=0)))), 'valid_conditional_layers': TunableSet(description='\n            A list of all of the conditional_layers on this Street.\n            ', tunable=TunableReference(description='\n                A reference to a conditional layer that exists on this Street.\n                ', manager=services.get_instance_manager(sims4.resources.Types.CONDITIONAL_LAYER), pack_safe=True)), 'tested_conditional_layers': TunableList(description='\n            The list of conditional layers that will load in to the the street\n            if its tests pass.\n            \n            NOTE: Only a subset of tests are registered to listen for updates.\n            Check with your GPE if you expect to have the conditional layers update\n            with test events. Otherwise, they will only be tested on zone-load.\n            ', tunable=TunableTuple(description='\n                A list of all of the conditional_layers to load into this Street.\n                ', conditional_layer=TunableReference(description='\n                    A reference to a conditional layer that exists on this Street.\n                    ', manager=services.get_instance_manager(sims4.resources.Types.CONDITIONAL_LAYER), pack_safe=True), tests=TunableGlobalTestList(description='\n                    The tests that must pass in order for this conditional layer\n                    to be loaded in. If tests are empty, the conditional layer\n                    is considered valid.\n                    '), process_after_event_handled=Tunable(description='\n                     When True, conditional layer requests triggered by the test\n                     events will be ordered to prioritize client-layer requests\n                     first, then gameplay-layers.\n                     ', tunable_type=bool, default=False), test_on_managed_world_edit_mode_load=Tunable(description='\n                    By default, conditional layers are not tested and started when entering\n                    from Managed World Edit Mode.  Those layers which are enabled via this\n                    option will be tested and started even in Managed World Edit Mode.\n                    Generally these should be Client Only layers, but no such restriction is\n                    enforced.\n                    ', tunable_type=bool, default=False))), 'beaches': TunableList(description='\n            List of locations to place beaches.\n            ', tunable=TunableTuple(description='\n                Beach creation data.\n                ', position=TunableVector3(description='\n                    The position to create the beach at.\n                    ', default=TunableVector3.DEFAULT_ZERO), forward=TunableVector2(description='\n                    The forward vector of the beach object.\n                    ', default=Vector2.Y_AXIS())), unique_entries=True), 'civic_policy': StreetProvider.TunableFactory(description='\n            Tuning to control the civic policy voting and enactment process for\n            a street.\n            '), 'initial_street_eco_footprint_override': OptionalTunable(description='\n            If enabled, overrides the initial value of the street eco footprint\n            statistic.\n            ', tunable=Tunable(description='\n                The initial value of the street eco footprint statistic.\n                ', tunable_type=float, default=0))}
    ZONE_IDS_BY_STREET = None
    street_to_lot_id_to_zone_ids = {}

    @classmethod
    def _cls_repr(cls):
        return "Street: <class '{}.{}'>".format(cls.__module__, cls.__name__)

    @classmethod
    def get_lot_to_travel_to(cls):
        if cls is services.current_street():
            return
        import world.lot
        return world.lot.get_lot_id_from_instance_id(cls.travel_lot)

    @classmethod
    def has_conditional_layer(cls, conditional_layer):
        return conditional_layer in cls.valid_conditional_layers

    @classmethod
    def clear_caches(cls):
        Street.street_to_lot_id_to_zone_ids.clear()
        Street.ZONE_IDS_BY_STREET = None
Example #12
0
class ScriptObject(BaseObject,
                   HasStatisticComponent,
                   HasFootprintComponent,
                   metaclass=HashedTunedInstanceMetaclass,
                   manager=services.definition_manager()):
    __qualname__ = 'ScriptObject'
    INSTANCE_TUNABLES = {
        '_super_affordances':
        TunableList(
            description=
            '\n            Super affordances on this object.\n            ',
            tunable=TunableReference(
                description=
                '\n                A super affordance on this object.\n                ',
                manager=services.affordance_manager(),
                class_restrictions=('SuperInteraction', ),
                pack_safe=True)),
        '_part_data':
        TunableList(
            description=
            '\n            Use this to define parts for an object. Parts allow multiple Sims to\n            use an object in different or same ways, at the same time. The model\n            and the animations for this object will have to support parts.\n            Ensure this is the case with animation and modeling.\n           \n            There will be one entry in this list for every part the object has.\n           \n            e.g. The bed has six parts (two sleep parts, and four sit parts).\n              add two entries for the sleep parts add four entries for the sit\n              parts\n            ',
            tunable=TunableTuple(
                description=
                '\n                Data that is specific to this part.\n                ',
                part_definition=TunableReference(
                    description=
                    '\n                    The part definition data.\n                    ',
                    manager=services.object_part_manager()),
                subroot_index=OptionalTunable(
                    description=
                    '\n                    If enabled, this part will have a subroot index associated\n                    with it. This will affect the way Sims animate, i.e. they\n                    will animate relative to the position of the part, not\n                    relative to the object.\n                    ',
                    tunable=Tunable(
                        description=
                        '\n                        The subroot index/suffix associated to this part.\n                        ',
                        tunable_type=int,
                        default=0,
                        needs_tuning=False),
                    enabled_by_default=True),
                overlapping_parts=TunableList(
                    description=
                    "\n                    The indices of parts that are unusable when this part is in\n                    use. The index is the zero-based position of the part within\n                    the object's Part Data list.\n                    ",
                    tunable=int),
                adjacent_parts=OptionalTunable(
                    description=
                    '\n                    Define adjacent parts. If disabled, adjacent parts will be\n                    generated automatically based on indexing. If enabled,\n                    adjacent parts must be specified here.\n                    ',
                    tunable=TunableList(
                        description=
                        "\n                        The indices of parts that are adjacent to this part. The\n                        index is the zero-based position of the part within the\n                        object's Part Data list.\n                        \n                        An empty list indicates that no part is ajdacent to this\n                        part.\n                        ",
                        tunable=int)),
                is_mirrored=OptionalTunable(
                    description=
                    '\n                    Specify whether or not solo animations played on this part\n                    should be mirrored or not.\n                    ',
                    tunable=Tunable(
                        description=
                        '\n                        If checked, mirroring is enabled. If unchecked,\n                        mirroring is disabled.\n                        ',
                        tunable_type=bool,
                        default=False)),
                forward_direction_for_picking=TunableVector2(
                    description=
                    "\n                    When you click on the object this part belongs to, this\n                    offset will be applied to this part when determining which\n                    part is closest to where you clicked. By default, the\n                    object's forward vector will be used. It should only be\n                    necessary to tune this value if multiple parts overlap at\n                    the same location (e.g. the single bed).\n                    ",
                    default=sims4.math.Vector2(0, 1),
                    x_axis_name='x',
                    y_axis_name='z'),
                disable_sim_aop_forwarding=Tunable(
                    description=
                    '\n                    If checked, Sims using this specific part will never forward\n                    AOPs.\n                    ',
                    tunable_type=bool,
                    default=False),
                disable_child_aop_forwarding=Tunable(
                    description=
                    '\n                    If checked, objects parented to this specific part will\n                    never forward AOPs.\n                    ',
                    tunable_type=bool,
                    default=False),
                anim_overrides=TunableAnimationOverrides(
                    description='Animation overrides for this part.'))),
        'custom_posture_target_name':
        Tunable(
            description=
            '\n            An additional non-virtual actor to set for this object when used as\n            a posture target.\n            \n            This tunable is used when the object has parts. In most cases, the\n            state machines will only have one actor for the part that is\n            involved in animation. In that case, this field should not be set.\n            \n            e.g. The Sit posture requires the sitTemplate actor to be set, but\n            does not make a distinction between, for instance, Chairs and Sofas,\n            because no animation ever involves the whole object.\n            \n            However, there may be cases when, although we are dealing with\n            parts, the animation will need to also reference the entire object.\n            In that case, the ASM will have an extra actor to account for the\n            whole object, in addition to the part. Set this field to be that\n            actor name.\n            \n            e.g. The Sleep posture on the bed animates the Sim on one part.\n            However, the sheets and pillows need to animate on the entire bed.\n            In that case, we need to set this field on Bed so that the state\n            machine can have this actor set.\n            ',
            tunable_type=str,
            default=None),
        'posture_transition_target_tag':
        TunableEnumEntry(
            description=
            '\n            A tag to apply to this script object so that it is taken into\n            account for posture transition preference scoring.  For example, you\n            could tune this object (and others) to be a DINING_SURFACE.  Any SI\n            that is set up to have posture preference scoring can override the\n            score for any objects that are tagged with DINING_SURFACE.\n            \n            For a more detailed description of how posture preference scoring\n            works, see the posture_target_preference tunable field description\n            in SuperInteraction.\n            ',
            tunable_type=postures.PostureTransitionTargetPreferenceTag,
            default=postures.PostureTransitionTargetPreferenceTag.INVALID),
        '_anim_overrides':
        OptionalTunable(
            description=
            '\n            If enabled, specify animation overrides for this object.\n            ',
            tunable=TunableAnimationObjectOverrides()),
        '_focus_score':
        TunableEnumEntry(
            description=
            '\n            Determines how likely a Sim is to look at this object when focusing\n            ambiently.  A higher value means this object is more likely to draw\n            Sim focus.\n            ',
            tunable_type=FocusInterestLevel,
            default=FocusInterestLevel.LOW,
            needs_tuning=True),
        'social_clustering':
        OptionalTunable(
            description=
            '\n            If enabled, specify how this objects affects clustering for\n            preferred locations for socialization.\n            ',
            tunable=TunableTuple(is_datapoint=Tunable(
                description=
                '\n                     Whether or not this object is a data point for social\n                     clusters.\n                     ',
                tunable_type=bool,
                default=True))),
        '_should_search_forwarded_sim_aop':
        Tunable(
            description=
            "\n            If enabled, interactions on Sims using this object will appear in\n            this object's pie menu as long as they are also tuned to allow\n            forwarding.\n            ",
            tunable_type=bool,
            default=False),
        '_should_search_forwarded_child_aop':
        Tunable(
            description=
            "\n            If enabled, interactions on children of this object will appear in\n            this object's pie menu as long as they are also tuned to allow\n            forwarding.\n            ",
            tunable_type=bool,
            default=False),
        '_disable_child_footprint_and_shadow':
        Tunable(
            description=
            "\n            If checked, all objects parented to this object will have their\n            footprints and dropshadows disabled.\n            \n            Example Use: object_sim has this checked so when a Sim picks up a\n            plate of food, the plate's footprint and dropshadow turn off\n            temporarily.\n            ",
            tunable_type=bool,
            default=False),
        'disable_los_reference_point':
        Tunable(
            description=
            '\n            If checked, goal points for this interaction will not be discarded\n            if a ray-test from the object fails to connect without intersecting\n            walls or other objects.  The reason for allowing this, is for\n            objects like the door where we want to allow the sim to interact\n            with the object, but since the object doesnt have a footprint we\n            want to allow him to use the central point as a reference point and\n            not fail the LOS test.\n            ',
            tunable_type=bool,
            default=False),
        '_components':
        TunableTuple(
            description=
            '\n            The components that instances of this object should have.\n            ',
            tuning_group=GroupNames.COMPONENTS,
            affordance_tuning=OptionalTunable(
                AffordanceTuningComponent.TunableFactory()),
            autonomy=OptionalTunable(TunableAutonomyComponent()),
            canvas=OptionalTunable(CanvasComponent.TunableFactory()),
            carryable=OptionalTunable(TunableCarryableComponent()),
            censor_grid=OptionalTunable(TunableCensorGridComponent()),
            collectable=OptionalTunable(CollectableComponent.TunableFactory()),
            consumable=OptionalTunable(ConsumableComponent.TunableFactory()),
            crafting_station=OptionalTunable(
                CraftingStationComponent.TunableFactory()),
            fishing_location=OptionalTunable(
                FishingLocationComponent.TunableFactory()),
            flowing_puddle=OptionalTunable(
                FlowingPuddleComponent.TunableFactory()),
            game=OptionalTunable(TunableGameComponent()),
            gardening_component=TunableGardeningComponent(),
            idle_component=OptionalTunable(IdleComponent.TunableFactory()),
            inventory=OptionalTunable(
                ObjectInventoryComponent.TunableFactory()),
            inventory_item=OptionalTunable(
                InventoryItemComponent.TunableFactory()),
            lighting=OptionalTunable(LightingComponent.TunableFactory()),
            line_of_sight=OptionalTunable(TunableLineOfSightComponent()),
            live_drag_target=OptionalTunable(
                LiveDragTargetComponent.TunableFactory()),
            name=OptionalTunable(NameComponent.TunableFactory()),
            object_age=OptionalTunable(TunableObjectAgeComponent()),
            object_relationships=OptionalTunable(
                ObjectRelationshipComponent.TunableFactory()),
            object_teleportation=OptionalTunable(
                ObjectTeleportationComponent.TunableFactory()),
            ownable_component=OptionalTunable(
                OwnableComponent.TunableFactory()),
            proximity_component=OptionalTunable(
                ProximityComponent.TunableFactory()),
            spawner_component=OptionalTunable(
                SpawnerComponent.TunableFactory()),
            state=OptionalTunable(TunableStateComponent()),
            time_of_day_component=OptionalTunable(
                TimeOfDayComponent.TunableFactory()),
            tooltip_component=OptionalTunable(
                TooltipComponent.TunableFactory()),
            video=OptionalTunable(TunableVideoComponent()),
            welcome_component=OptionalTunable(
                WelcomeComponent.TunableFactory())),
        '_components_native':
        TunableTuple(
            description=
            '\n            Tuning for native components, those that an object will have even\n            if not tuned.\n            ',
            tuning_group=GroupNames.COMPONENTS,
            Slot=OptionalTunable(SlotComponent.TunableFactory())),
        '_persists':
        Tunable(
            description=
            '\n            Whether object should persist or not.\n            ',
            tunable_type=bool,
            default=True,
            tuning_filter=FilterTag.EXPERT_MODE),
        '_world_file_object_persists':
        Tunable(
            description=
            "\n            If object is from world file, check this if object state should\n            persist. \n            Example:\n                If grill is dirty, but this is unchecked and it won't stay\n                dirty when reloading the street. \n                If Magic tree has this checked, all object relationship data\n                will be saved.\n            ",
            tunable_type=bool,
            default=False,
            tuning_filter=FilterTag.EXPERT_MODE),
        '_object_state_remaps':
        TunableList(
            description=
            '\n            If this object is part of a Medator object suite, this list\n            specifies which object tuning file to use for each catalog object\n            state.\n            ',
            tunable=TunableReference(
                description=
                '\n                Current object state.\n                ',
                manager=services.definition_manager(),
                tuning_filter=FilterTag.EXPERT_MODE)),
        'environment_score_trait_modifiers':
        TunableMapping(
            description=
            '\n            Each trait can put modifiers on any number of moods as well as the\n            negative environment scoring.\n            \n            If tuning becomes a burden, consider making prototypes for many\n            objects and tuning the prototype.\n            \n            Example: A Sim with the Geeky trait could have a modifier for the\n            excited mood on objects like computers and tablets.\n            \n            Example: A Sim with the Loves Children trait would have a modifier\n            for the happy mood on toy objects.\n            \n            Example: A Sim that has the Hates Art trait could get an Angry\n            modifier, and should set modifiers like Happy to multiply by 0.\n            ',
            key_type=TunableReference(
                description=
                '\n                The Trait that the Sim must have to enable this modifier.\n                ',
                manager=services.get_instance_manager(
                    sims4.resources.Types.TRAIT)),
            value_type=TunableEnvironmentScoreModifiers.TunableFactory(
                description=
                '\n                The Environmental Score modifiers for a particular trait.\n                '
            ),
            key_name='trait',
            value_name='modifiers'),
        'slot_cost_modifiers':
        TunableMapping(
            description=
            "\n            A mapping of slot types to modifier values.  When determining slot\n            scores in the transition sequence, if the owning object of a slot\n            has a modifier for its type specified here, that slot will have the\n            modifier value added to its cost.  A positive modifier adds to the\n            cost of a path using this slot and means that a slot will be less\n            likely to be chosen.  A negative modifier subtracts from the cost\n            of a path using this slot and means that a slot will be more likely\n            to be chosen.\n            \n            ex: Both bookcases and toilets have deco slots on them, but you'd\n            rather a Sim prefer to put down an object in a bookcase than on the\n            back of a toilet.\n            ",
            key_type=SlotType.TunableReference(
                description=
                '\n                A reference to the type of slot to be given a score modifier\n                when considered for this object.\n                '
            ),
            value_type=Tunable(
                description=
                '\n                A tunable float specifying the score modifier for the\n                corresponding slot type on this object.\n                ',
                tunable_type=float,
                default=0)),
        'fire_retardant':
        Tunable(
            description=
            '\n            If an object is fire retardant then not only will it not burn, but\n            it also cannot overlap with fire, so fire will not spread into an\n            area occupied by a fire retardant object.\n            ',
            tunable_type=bool,
            default=False)
    }
    _commodity_flags = None
    additional_interaction_constraints = None

    def __init__(self, definition, **kwargs):
        super().__init__(definition,
                         tuned_native_components=self._components_native,
                         **kwargs)
        self._dynamic_commodity_flags_map = dict()
        for component_factory in self._components.values():
            while component_factory is not None:
                self.add_component(component_factory(self))
        self.item_location = ItemLocation.INVALID_LOCATION
        if self._persists:
            self._persistence_group = PersistenceGroups.OBJECT
        else:
            self._persistence_group = PersistenceGroups.NONE
        self._registered_transition_controllers = set()
        if self.definition.negative_environment_score != 0 or (
                self.definition.positive_environment_score != 0
                or self.definition.environment_score_mood_tags
        ) or self.environment_score_trait_modifiers:
            self.add_dynamic_component(
                objects.components.types.ENVIRONMENT_SCORE_COMPONENT.
                instance_attr)

    def on_reset_early_detachment(self, reset_reason):
        super().on_reset_early_detachment(reset_reason)
        for transition_controller in self._registered_transition_controllers:
            transition_controller.on_reset_early_detachment(self, reset_reason)

    def on_reset_get_interdependent_reset_records(self, reset_reason,
                                                  reset_records):
        super().on_reset_get_interdependent_reset_records(
            reset_reason, reset_records)
        for transition_controller in self._registered_transition_controllers:
            transition_controller.on_reset_add_interdependent_reset_records(
                self, reset_reason, reset_records)

    def on_reset_internal_state(self, reset_reason):
        if reset_reason == ResetReason.BEING_DESTROYED:
            self._registered_transition_controllers.clear()
        else:
            if not (self.parent is not None and self.parent.is_sim
                    and self.parent.posture_state.is_carrying(self)):
                if not CarryingObject.snap_to_good_location_on_floor(
                        self, self.parent.transform,
                        self.parent.routing_surface):
                    self.clear_parent(self.parent.transform,
                                      self.parent.routing_surface)
            self.location = self.location
        super().on_reset_internal_state(reset_reason)

    def register_transition_controller(self, controller):
        self._registered_transition_controllers.add(controller)

    def unregister_transition_controller(self, controller):
        self._registered_transition_controllers.discard(controller)

    @classmethod
    def _verify_tuning_callback(cls):
        for (i, part_data) in enumerate(cls._part_data):
            while part_data.forward_direction_for_picking.magnitude() != 1.0:
                logger.warn(
                    'On {}, forward_direction_for_picking is {} on part {}, which is not a normalized vector.',
                    cls,
                    part_data.forward_direction_for_picking,
                    i,
                    owner='bhill')
        for sa in cls._super_affordances:
            if sa.allow_user_directed and not sa.display_name:
                logger.error(
                    'Interaction {} on {} does not have a valid display name.',
                    sa.__name__, cls.__name__)
            while sa.consumes_object() or sa.contains_stat(
                    CraftingTuning.CONSUME_STATISTIC):
                logger.error(
                    'ScriptObject: Interaction {} on {} is consume affordance, should tune on ConsumableComponent of the object.',
                    sa.__name__,
                    cls.__name__,
                    owner='tastle/cjiang')

    @flexmethod
    def update_commodity_flags(cls, inst):
        commodity_flags = set()
        inst_or_cls = inst if inst is not None else cls
        for sa in inst_or_cls.super_affordances():
            commodity_flags |= sa.commodity_flags
        if commodity_flags:
            cls._commodity_flags = frozenset(commodity_flags)
        else:
            cls._commodity_flags = EMPTY_SET

    @flexproperty
    def commodity_flags(cls, inst):
        if cls._commodity_flags is None:
            if inst is not None:
                inst.update_commodity_flags()
            else:
                cls.update_commodity_flags()
        if inst is not None:
            dynamic_commodity_flags = set()
            for dynamic_commodity_flags_entry in inst._dynamic_commodity_flags_map.values(
            ):
                dynamic_commodity_flags.update(dynamic_commodity_flags_entry)
            return frozenset(cls._commodity_flags | dynamic_commodity_flags)
        return cls._commodity_flags

    def add_dynamic_commodity_flags(self, key, commodity_flags):
        self._dynamic_commodity_flags_map[key] = commodity_flags

    def remove_dynamic_commodity_flags(self, key):
        if key in self._dynamic_commodity_flags_map:
            del self._dynamic_commodity_flags_map[key]

    @classproperty
    def tuned_components(cls):
        return cls._components

    @flexproperty
    def allowed_hands(cls, inst):
        if inst is not None:
            carryable = inst.carryable_component
            if carryable is not None:
                return carryable.allowed_hands
            return ()
        carryable_tuning = cls._components.carryable
        if carryable_tuning is not None:
            return carryable_tuning.allowed_hands
        return ()

    @flexproperty
    def holster_while_routing(cls, inst):
        if inst is not None:
            carryable = inst.carryable_component
            if carryable is not None:
                return carryable.holster_while_routing
            return False
        carryable_tuning = cls._components.carryable
        if carryable_tuning is not None:
            return carryable_tuning.holster_while_routing
        return False

    def is_surface(self, *args, **kwargs):
        return False

    @classproperty
    def _anim_overrides_cls(cls):
        if cls._anim_overrides is not None:
            return cls._anim_overrides(None)

    @property
    def object_routing_surface(self):
        pass

    @property
    def _anim_overrides_internal(self):
        params = {
            'isParented': self.parent is not None,
            'heightAboveFloor':
            slots.get_surface_height_parameter_for_object(self)
        }
        if self.is_part:
            params['subroot'] = self.part_suffix
            params['isMirroredPart'] = True if self.is_mirrored() else False
        overrides = AnimationOverrides(params=params)
        for component_overrides in self.component_anim_overrides_gen():
            overrides = overrides(component_overrides())
        if self._anim_overrides is not None:
            return overrides(self._anim_overrides())
        return overrides

    @forward_to_components_gen
    def component_anim_overrides_gen(self):
        pass

    @property
    def parent(self):
        pass

    def ancestry_gen(self):
        obj = self
        while obj is not None:
            yield obj
            if obj.is_part:
                obj = obj.part_owner
            else:
                obj = obj.parent

    @property
    def parent_slot(self):
        pass

    def get_closest_parts_to_position(self,
                                      position,
                                      posture=None,
                                      posture_spec=None):
        best_parts = set()
        best_distance = MAX_FLOAT
        if position is not None and self.parts is not None:
            while True:
                for part in self.parts:
                    while (posture is None
                           or part.supports_posture_type(posture)) and (
                               posture_spec is None
                               or part.supports_posture_spec(posture_spec)):
                        dist = (part.position_with_forward_offset -
                                position).magnitude_2d_squared()
                        if dist < best_distance:
                            best_parts.clear()
                            best_parts.add(part)
                            best_distance = dist
                        elif dist == best_distance:
                            best_parts.add(part)
        return best_parts

    def num_valid_parts(self, posture):
        raise RuntimeError(
            '[bhill] This function is believed to be dead code and is scheduled for pruning. If this exception has been raised, the code is not dead and this exception should be removed.'
        )
        if self.parts is not None:
            return sum(
                part.supports_posture_type(posture.posture_type)
                for part in self.parts)
        return 0

    def is_same_object_or_part(self, obj):
        if not isinstance(obj, ScriptObject):
            return False
        if obj is self:
            return True
        if obj.is_part and obj.part_owner is self or self.is_part and self.part_owner is obj:
            return True
        return False

    def is_same_object_or_part_of_same_object(self, obj):
        if not isinstance(obj, ScriptObject):
            return False
        if self.is_same_object_or_part(obj):
            return True
        if self.is_part and obj.is_part and self.part_owner is obj.part_owner:
            return True
        return False

    def get_compatible_parts(self, posture, interaction=None):
        if posture is not None and posture.target is not None and posture.target.is_part:
            return (posture.target, )
        return self.get_parts_for_posture(posture, interaction)

    def get_parts_for_posture(self, posture, interaction=None):
        if self.parts is not None:
            return (part for part in self.parts if part.supports_posture_type(
                posture.posture_type, interaction))
        return ()

    def get_parts_for_affordance(self, affordance):
        raise RuntimeError(
            '[bhill] This function is believed to be dead code and is scheduled for pruning. If this exception has been raised, the code is not dead and this exception should be removed.'
        )
        if self.parts is not None and affordance is not None:
            return (part for part in self.parts
                    if part.supports_affordance(affordance))
        return ()

    def may_reserve(self, *args, **kwargs):
        return True

    def reserve(self, *args, **kwargs):
        pass

    def release(self, *args, **kwargs):
        pass

    @property
    def build_buy_lockout(self):
        return False

    @property
    def route_target(self):
        return (RouteTargetType.NONE, None)

    @flexmethod
    def super_affordances(cls, inst, context=None):
        from objects.base_interactions import BaseInteractionTuning
        inst_or_cls = inst if inst is not None else cls
        component_affordances_gen = inst.component_super_affordances_gen(
        ) if inst is not None else EMPTY_SET
        super_affordances = itertools.chain(
            inst_or_cls._super_affordances,
            BaseInteractionTuning.GLOBAL_AFFORDANCES,
            component_affordances_gen)
        super_affordances = list(super_affordances)
        shift_held = False
        if context is not None:
            shift_held = context.shift_held
        for sa in super_affordances:
            if shift_held:
                if sa.cheat:
                    yield sa
                elif sa.debug and __debug__:
                    yield sa
                elif sa.automation and paths.AUTOMATION_MODE:
                    yield sa
                    while not sa.debug and not sa.cheat:
                        yield sa
            else:
                while not sa.debug and not sa.cheat:
                    yield sa

    @forward_to_components_gen
    def component_super_affordances_gen(self):
        pass

    @caches.cached_generator
    def posture_interaction_gen(self):
        for affordance in self._super_affordances:
            while not affordance.debug:
                if affordance._provided_posture_type is not None:
                    while True:
                        for aop in affordance.potential_interactions(
                                self, None):
                            yield aop

    def supports_affordance(self, affordance):
        return True

    def potential_interactions(self,
                               context,
                               get_interaction_parameters=None,
                               allow_forwarding=True,
                               **kwargs):
        try:
            for affordance in self.super_affordances(context):
                if not self.supports_affordance(affordance):
                    pass
                if get_interaction_parameters is not None:
                    interaction_parameters = get_interaction_parameters(
                        affordance, kwargs)
                else:
                    interaction_parameters = kwargs
                for aop in affordance.potential_interactions(
                        self, context, **interaction_parameters):
                    yield aop
            for aop in self.potential_component_interactions(context):
                yield aop
            while allow_forwarding and (
                    self._should_search_forwarded_sim_aop
                    or self._should_search_forwarded_child_aop):
                for aop in self._search_forwarded_interactions(
                        context,
                        self._should_search_forwarded_sim_aop,
                        self._should_search_forwarded_child_aop,
                        get_interaction_parameters=get_interaction_parameters,
                        **kwargs):
                    yield aop
        except Exception:
            logger.exception(
                'Exception while generating potential interactions for {}:',
                self)

    def supports_posture_type(self, posture_type):
        for super_affordance in self._super_affordances:
            while super_affordance.provided_posture_type == posture_type:
                return True
        return False

    def _search_forwarded_interactions(self, context, search_sim_aops,
                                       search_child_aops, **kwargs):
        if search_sim_aops:
            for part_or_object in (self, ) if not self.parts else self.parts:
                user_list = part_or_object.get_users(sims_only=True)
                for user in user_list:
                    if part_or_object.is_part and part_or_object.disable_child_aop_forwarding:
                        pass
                    for aop in user.potential_interactions(context, **kwargs):
                        while aop.affordance.allow_forward:
                            yield aop
        if not search_child_aops:
            return
        for child in self.children:
            if child.parent.is_part and child.parent.disable_child_aop_forwarding:
                pass
            for aop in child.potential_interactions(context, **kwargs):
                while aop.affordance.allow_forward:
                    yield aop

    def add_dynamic_component(self, *args, **kwargs):
        result = super().add_dynamic_component(*args, **kwargs)
        if result:
            self.resend_interactable()
        return result

    @distributor.fields.Field(op=distributor.ops.SetInteractable,
                              default=False)
    def interactable(self):
        if self.build_buy_lockout:
            return False
        if self._super_affordances:
            return True
        for _ in self.component_interactable_gen():
            pass
        return False

    resend_interactable = interactable.get_resend()

    @forward_to_components_gen
    def component_interactable_gen(self):
        pass

    @caches.cached(maxsize=20)
    def check_line_of_sight(self, transform, verbose=False):
        top_level_parent = self
        while top_level_parent.parent is not None:
            top_level_parent = top_level_parent.parent
        if top_level_parent.wall_or_fence_placement:
            if verbose:
                return (routing.RAYCAST_HIT_TYPE_NONE, None)
            return (True, None)
        if self.is_in_inventory():
            if verbose:
                return (routing.RAYCAST_HIT_TYPE_NONE, None)
            return (True, None)
        slot_routing_location = self.get_routing_location_for_transform(
            transform)
        if verbose:
            ray_test = routing.ray_test_verbose
        else:
            ray_test = routing.ray_test
        return ray_test(slot_routing_location,
                        self.routing_location,
                        self.raycast_context(),
                        return_object_id=True)

    clear_check_line_of_sight_cache = check_line_of_sight.cache.clear

    def _create_raycast_context(self, *args, **kwargs):
        super()._create_raycast_context(*args, **kwargs)
        if not self.is_sim:
            self.clear_check_line_of_sight_cache()

    @property
    def connectivity_handles(self):
        routing_context = self.get_or_create_routing_context()
        return routing_context.connectivity_handles

    def _clear_connectivity_handles(self):
        if self._routing_context is not None:
            self._routing_context.connectivity_handles.clear()

    @property
    def focus_bone(self):
        return 0

    @forward_to_components
    def on_state_changed(self, state, old_value, new_value):
        pass

    @forward_to_components
    def on_post_load(self):
        pass

    @forward_to_components
    def on_finalize_load(self):
        pass

    @property
    def attributes(self):
        pass

    @property
    def flammable(self):
        return False

    @attributes.setter
    def attributes(self, value):
        logger.debug('PERSISTENCE: Attributes property on {0} were set', self)
        try:
            object_data = ObjectData()
            object_data.ParseFromString(value)
            self.load_object(object_data)
        except:
            logger.exception('Exception applying attributes to object {0}',
                             self)

    def load_object(self, object_data):
        save_data = protocols.PersistenceMaster()
        save_data.ParseFromString(object_data.attributes)
        self.load(save_data)
        self.on_post_load()

    def is_persistable(self):
        if self.persistence_group == objects.persistence_groups.PersistenceGroups.OBJECT:
            return True
        if self.item_location == ItemLocation.FROM_WORLD_FILE:
            return self._world_file_object_persists
        if self.item_location == ItemLocation.ON_LOT or self.item_location == ItemLocation.FROM_OPEN_STREET:
            return self._persists
        if self.persistence_group == objects.persistence_groups.PersistenceGroups.IN_OPEN_STREET and self.item_location == ItemLocation.INVALID_LOCATION:
            return self._persist
        return False

    def save_object(self,
                    object_list,
                    item_location=ItemLocation.ON_LOT,
                    container_id=0):
        if not self.is_persistable():
            return
        with ProtocolBufferRollback(object_list) as save_data:
            attribute_data = self.get_attribute_save_data()
            save_data.object_id = self.id
            if attribute_data is not None:
                save_data.attributes = attribute_data.SerializeToString()
            save_data.guid = self.definition.id
            save_data.loc_type = item_location
            save_data.container_id = container_id
            return save_data

    def get_attribute_save_data(self):
        attribute_data = protocols.PersistenceMaster()
        self.save(attribute_data)
        return attribute_data

    @forward_to_components
    def save(self, persistence_master_message):
        pass

    def load(self, persistence_master_message):
        component_priority_list = []
        for persistable_data in persistence_master_message.data:
            component_priority_list.append(
                (get_component_priority_and_name_using_persist_id(
                    persistable_data.type), persistable_data))
        component_priority_list.sort(key=lambda priority: priority[0][0],
                                     reverse=True)
        for ((_, (_, inst_comp)), persistable_data) in component_priority_list:
            while inst_comp:
                self.add_dynamic_component(inst_comp)
                if self.has_component(inst_comp):
                    getattr(self, inst_comp).load(persistable_data)

    def finalize(self, **kwargs):
        self.on_finalize_load()

    def clone(self, **kwargs):
        clone = objects.system.create_object(self.definition, **kwargs)
        object_list = file_serialization.ObjectList()
        save_data = self.save_object(object_list.objects)
        clone.load_object(save_data)
        return clone
Example #13
0
class Commodity(HasTunableReference, TunedContinuousStatistic, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(sims4.resources.Types.STATISTIC)):
    __qualname__ = 'Commodity'
    REMOVE_INSTANCE_TUNABLES = ('initial_value',)
    INSTANCE_TUNABLES = {'stat_name': TunableLocalizedString(description='\n                Localized name of this commodity.\n                ', export_modes=ExportModes.All), 'min_value_tuning': Tunable(description='\n                The minimum value for this stat.\n                ', tunable_type=float, default=-100, export_modes=ExportModes.All), 'max_value_tuning': Tunable(description='\n                The maximum value for this stat.', tunable_type=float, default=100, export_modes=ExportModes.All), 'ui_sort_order': TunableRange(description='\n                Order in which the commodity will appear in the motive panel.\n                Commodities sort from lowest to highest.\n                ', tunable_type=int, default=0, minimum=0, export_modes=ExportModes.All), 'ui_visible_distress_threshold': Tunable(description='\n                When current value of commodity goes below this value, commodity\n                will appear in the motive panel tab.\n                ', tunable_type=float, default=0, export_modes=ExportModes.All), '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), export_modes=ExportModes.All)), 'auto_satisfy_curve_tuning': TunableList(description='\n                A list of Vector2 points that define the auto-satisfy curve for\n                this commodity.\n                ', tunable=TunableVector2(description='\n                    Point on a Curve\n                    ', default=sims4.math.Vector2(0, 0))), 'auto_satisfy_curve_random_time_offset': TunableSimMinute(description='\n                An amount of time that when auto satisfy curves are being used\n                will modify the time current time being used to plus or minus\n                a random number between this value.\n                ', default=120), 'maximum_auto_satisfy_time': TunableSimMinute(description='\n                The maximum amount of time that the auto satisfy curves will\n                interpolate the values based on the current one before just\n                setting to the maximum value.\n                ', default=1440), 'initial_tuning': TunableTuple(description=' \n                The Initial value for this commodity. Can either be a single\n                value, range, or use auto satisfy curve to determine initial\n                value.  Use auto satisfy curve will take precedence over range\n                value and range value will take precedence over single value\n                range.\n                ', _use_auto_satisfy_curve_as_initial_value=Tunable(description="\n                    If checked, when we first add this commodity to a sim (sims only),\n                    the initial value of the commodity will be set according to\n                    the auto-satisfy curves defined by this commodity's tuning as\n                    opposed to the tuned initial value.    \n                    ", tunable_type=bool, needs_tuning=True, default=False), _value_range=OptionalTunable(description='\n                    If enabled then when we first add this commodity to a Sim the\n                    initial value of the commodity will be set to a random value\n                    within this interval.\n                    ', tunable=TunableInterval(description='\n                        An interval that will be used for the initial value of this\n                        commodity.\n                        ', tunable_type=int, default_lower=0, default_upper=100)), _value=Tunable(description='\n                    The initial value for this stat.', tunable_type=float, default=0.0)), 'weight': Tunable(description="\n                The weight of the Skill with regards to autonomy.  It's ignored \n                for the purposes of sorting stats, but it's applied when scoring \n                the actual statistic operation for the SI.\n                ", tunable_type=float, default=0.5), 'states': TunableList(description='\n                Commodity states based on thresholds.  This should be ordered\n                from worst state to best state.\n                ', tunable=TunableCommodityState()), 'commodity_distress': OptionalTunable(TunableCommodityDistress()), 'commodity_failure': OptionalTunable(TunableCommodityFailure()), 'remove_on_convergence': Tunable(description='\n                Commodity will be removed when convergence is met only if not\n                a core commodity.\n                ', tunable_type=bool, default=True), 'visible': Tunable(description='\n                Whether or not commodity should be sent to client.\n                ', tunable_type=bool, default=False, export_modes=ExportModes.All), '_add_if_not_in_tracker': Tunable(description="\n                If True, when we try to add or set the commodity, we will add\n                the commodity to the tracker if the tracker doesn't already have\n                it.\n                \n                e.g If a sim uses the toilet and we update bladder when that sim\n                doesn't have the bladder commodity in his/her tracker, we will\n                add the bladder commodity to that sim. \n                \n                Set this to false for the case of NPC behavior commodities like\n                Being a Maid or Being a Burglar.\n                ", tunable_type=bool, default=True), 'initial_as_default': Tunable(description='\n                Setting this to true will cause the default value returned during testing to be the \n                initial value tuned. This happens when a test is run on this commodity on a Sim that\n                does not have the commodity. Leaving this as false will instead return the convergence\n                value.\n                ', tunable_type=bool, default=False), 'arrow_data': TunableArrowData(description='\n                Used to determine when positive or negative arrows should show\n                up depending on the delta rate of the commodity.\n                ', export_modes=(ExportModes.ClientBinary,)), '_categories': TunableSet(description='\n                List of categories that this statistic is part of.\n                ', tunable=StatisticCategory), '_off_lot_simulation': OptionalTunable(TunableTuple(threshold=TunableThreshold(description='\n                    The threshold that will activate the increase in value\n                    when the commodity hits it.\n                    ', value=Tunable(description='\n                        The value that this threshold will trigger on.\n                        ', tunable_type=int, default=-50)), value=Tunable(description='\n                    The value that this commodity will increase by once it hits\n                    the tuned threshold while the sim is offlot.\n                    ', tunable_type=int, default=100), description='\n                Offlot simulation for this commodity.  The commodity will be\n                allowed to decay at a normal rate until it hits the tuned\n                threshold.  Once there it will then have its value added by the\n                tuned value.\n                ')), '_max_simulate_time_on_load': OptionalTunable(description="\n                If enabled, this commodity will only simulate for a max amount\n                of time when the player loads back into the lot with a new world\n                game time.\n                \n                By default, this is disabled. When disabled, the commodity will\n                simulate for however long between the lot's previous saved time\n                and the current world time. (Note: this is capped by PersistenceTuning.MAX_LOT_SIMULATE_ELAPSED_TIME)\n                ", tunable=TunableSimMinute(description="\n                    If set to > 0, on load, this object commodity will update its value to\n                    world time. And the commodity will simulate for the max amount of time\n                    specified in the tunable.\n                    EX: If tuned for the water commodity on plants to 6 hours --\n                    if the player leaves the lot for 4 hours and then comes back,\n                    the water commodity will update to what it should be 4 hours later.\n                    If the player leaves the lot for 8 hours and comes back, the water\n                    commodity will only update to what it should be 6 hours later.\n                    \n                    If set to 0, no matter how much time has elapsed since the\n                    player last visited the lot, this commodity's value will load\n                    to its last saved value.\n                    ", default=1440, minimum=0)), '_time_passage_fixup_type': TunableEnumEntry(description="\n            This is for commodities on SIMS only.\n            This option what we do with the commodity when the sim\n            gets instanced after time has elapsed since the last time the sim\n            was spawned.\n            \n            do not fixup: Means the commodity will stay the same value as it was\n                when the sim was last instantiated\n                \n            fixup using autosatisfy curve: The commodity's value will be set\n                based on its autosatisfy curve and the time between when the sim was\n                last saved. Note, this fixup will not occur for active household sims\n                if offlot simulation is enabled for this commodity.\n                \n            fixup using time elapsed: The commodity will decay linearly based on\n                when the sim was last saved. Use this for things like commodities\n                that control buff timers to make sure that the time remaining on\n                a buff remains consistent.\n            ", tunable_type=CommodityTimePassageFixupType, default=CommodityTimePassageFixupType.DO_NOT_FIXUP), 'use_stat_value_on_init': Tunable(description='\n            When set the initial value for the commodity will be set from the\n            commodity tuning.\n            If unchecked, the initial stat value will not be set on \n            initialization, but instead will use other systems (like the state)\n            to set its initial value.\n            ', tunable_type=bool, default=True), 'stat_asm_param': TunableStatAsmParam.TunableFactory(locked_args={'use_effective_skill_level': True})}
    initial_value = 0
    _auto_satisfy_curve = None
    use_autosatisfy_curve = True
    commodity_states = None

    @classmethod
    def _tuning_loaded_callback(cls):
        super()._tuning_loaded_callback()
        cls.initial_value = cls.initial_tuning._value
        cls._build_utility_curve_from_tuning_data(cls.ad_data)
        if cls.auto_satisfy_curve_tuning:
            point_list = [(point.x, point.y) for point in cls.auto_satisfy_curve_tuning]
            cls._auto_satisfy_curve = sims4.math.CircularUtilityCurve(point_list, 0, date_and_time.HOURS_PER_DAY)
        if cls.states:
            state_zero = cls.states[0]
            if state_zero.value < cls.min_value:
                logger.error('Worst state should not be lower than min value of commodity.  Please update tuning')
                cls.commodity_states = cls.states
            elif state_zero.value > cls.min_value:
                state = CommodityState(value=cls.min_value, buff=BuffReference())
                cls.commodity_states = (state,) + cls.states
            else:
                cls.commodity_states = cls.states
            previous_value = cls.max_value
            index = len(cls.commodity_states)
            for state in reversed(cls.commodity_states):
                index -= 1
                if state.value >= previous_value:
                    logger.error('{0} has a lower bound value of state at index:{1} that is higher than the previous state.  Please update tuning', cls, index)
                if state.buff_add_threshold is not None:
                    threshold_value = state.buff_add_threshold.value
                    if threshold_value < state.value or threshold_value > previous_value:
                        logger.error('{0} add buff threshold is out of range for state at index:{1}.  Please update tuning', cls, index)
                previous_value = state.value
                while state.buff is not None and state.buff.buff_type is not None:
                    state.buff.buff_type.add_owning_commodity(cls)

    @classmethod
    def _verify_tuning_callback(cls):
        if cls.visible and (cls.ui_visible_distress_threshold < cls.min_value or cls.ui_visible_distress_threshold > cls.max_value):
            logger.error('{} visible distress value {} is outside the min{} / max {} range.  Please update tuning', cls, cls.ui_visible_distress_threshold, cls.min_value, cls.max_value)

    def __init__(self, tracker, core=False):
        self._allow_convergence_callback_to_activate = False
        self._buff_handle = None
        super().__init__(tracker, self.get_initial_value())
        self._core = core
        self._buff_handle = None
        self._buff_threshold_callback = None
        self._current_state_index = None
        self._current_state_ge_callback_data = None
        self._current_state_lt_callback_data = None
        self._off_lot_callback_data = None
        self._distress_buff_handle = None
        self._exit_distress_callback_data = None
        self._distress_callback_data = None
        self._failure_callback_data = None
        self._convergence_callback_data = None
        self._suppress_client_updates = False
        self.force_apply_buff_on_start_up = False
        self.force_buff_reason = None
        if getattr(self.tracker.owner, 'is_simulating', True):
            activate_convergence_callback = self.default_value != self.get_value()
            self.on_initial_startup(from_init=True, activate_convergence_callback=activate_convergence_callback)

    @classproperty
    def initial_value_range(cls):
        return cls.initial_tuning._value_range

    @classproperty
    def use_auto_satisfy_curve_as_initial_value(cls):
        return cls.initial_tuning._use_auto_satisfy_curve_as_initial_value

    @classmethod
    def get_initial_value(cls):
        if cls.initial_value_range is None:
            return cls.initial_value
        return random.uniform(cls.initial_value_range.lower_bound, cls.initial_value_range.upper_bound)

    @classproperty
    def use_stat_value_on_initialization(cls):
        return cls.use_stat_value_on_init

    @property
    def core(self):
        return self._core

    @core.setter
    def core(self, value):
        self._core = value

    @property
    def is_visible(self):
        return self.visible

    def _setup_commodity_distress(self):
        if self.commodity_distress is not None:
            self._distress_callback_data = self.create_callback(Threshold(self.commodity_distress.threshold_value, operator.le), self._enter_distress)

    def _setup_commodity_failure(self):
        if self.commodity_failure is not None:
            self._failure_callback_data = self.create_callback(self.commodity_failure.threshold, self._commodity_fail)

    def on_initial_startup(self, from_init=False, activate_convergence_callback=True):
        self._setup_commodity_distress()
        if self._distress_callback_data is not None:
            self.add_callback_data(self._distress_callback_data)
        self._setup_commodity_failure()
        if self._failure_callback_data is not None:
            self.add_callback_data(self._failure_callback_data)
        if self.commodity_states:
            self._remove_state_callback()
            current_value = self.get_value()
            new_state_index = self._find_state_index(current_value)
            if self._current_state_index != new_state_index:
                self._set_state(new_state_index, current_value, from_init=from_init, send_client_update=False)
            self._add_state_callback()
        self.decay_enabled = not self.tracker.owner.is_locked(self)
        self.force_apply_buff_on_start_up = False
        if self.force_buff_reason is not None and self._buff_handle is not None:
            current_state = self.commodity_states[self._current_state_index]
            self.tracker.owner.set_buff_reason(current_state.buff.buff_type, self.force_buff_reason, use_replacement=True)
            self.force_buff_reason = None
        if self.remove_on_convergence and self._convergence_callback_data is None:
            self._convergence_callback_data = self.create_callback(Threshold(self.convergence_value, operator.eq), self._remove_self_from_tracker)
            if activate_convergence_callback:
                self.add_callback_data(self._convergence_callback_data)
                self._allow_convergence_callback_to_activate = False
            else:
                self._allow_convergence_callback_to_activate = True

    @contextlib.contextmanager
    def _suppress_client_updates_context_manager(self, from_load=False, is_rate_change=True):
        if self._suppress_client_updates:
            yield None
        else:
            self._suppress_client_updates = True
            try:
                yield None
            finally:
                self._suppress_client_updates = False
                if not from_load:
                    self.send_commodity_progress_msg(is_rate_change=is_rate_change)

    def _commodity_telemetry(self, hook, desired_state_index):
        if not self.tracker.owner.is_sim:
            return
        with telemetry_helper.begin_hook(writer, hook, sim=self.tracker.get_sim()) as hook:
            guid = getattr(self, 'guid64', None)
            if guid is not None:
                hook.write_guid('stat', self.guid64)
            else:
                logger.info('{} does not have a guid64', self)
            hook.write_int('oldd', self._current_state_index)
            hook.write_int('news', desired_state_index)

    def _update_state_up(self, stat_instance):
        with self._suppress_client_updates_context_manager():
            current_value = self.get_value()
            desired_state_index = self._find_state_index(current_value)
            if desired_state_index == self._current_state_index:
                desired_state_index = self._find_state_index(current_value + EPSILON)
            if desired_state_index != self._current_state_index:
                self._remove_state_callback()
                self._commodity_telemetry(TELEMETRY_HOOK_STATE_UP, desired_state_index)
                while self._current_state_index < desired_state_index:
                    next_index = self._current_state_index + 1
                    self._set_state(next_index, current_value, send_client_update=next_index == desired_state_index)
                self._update_state_callback(desired_state_index)
            else:
                logger.warn('{} update state up was called, but state did not change. current state_index:{}', self, self._current_state_index, owner='msantander')

    def _update_state_down(self, stat_instance):
        with self._suppress_client_updates_context_manager():
            current_value = self.get_value()
            desired_state_index = self._find_state_index(self.get_value())
            if desired_state_index == self._current_state_index:
                desired_state_index = self._find_state_index(current_value - EPSILON)
            if desired_state_index != self._current_state_index:
                self._remove_state_callback()
                self._commodity_telemetry(TELEMETRY_HOOK_STATE_DOWN, desired_state_index)
                while self._current_state_index > desired_state_index:
                    prev_index = self._current_state_index - 1
                    self._set_state(prev_index, current_value, send_client_update=prev_index == desired_state_index)
                self._update_state_callback(desired_state_index)
            else:
                logger.warn('{} update state down was called, but state did not change. current state_index:{}', self, self._current_state_index, owner='msantander')

    def _update_state_callback(self, desired_state_index):
        new_state_index = self._find_state_index(self.get_value())
        if new_state_index > desired_state_index:
            self._update_state_up(self)
        elif new_state_index < desired_state_index:
            self._update_state_down(self)
        else:
            self._add_state_callback()

    def _state_reset_callback(self, stat_instance, time):
        self._update_buff(self._get_change_rate_without_decay())

    def _remove_self_from_tracker(self, _):
        tracker = self._tracker
        if tracker is not None:
            tracker.remove_statistic(self.stat_type)

    def _off_lot_simulation_callback(self, _):
        self.add_value(self._off_lot_simulation.value)

    def start_low_level_simulation(self):
        if self._off_lot_simulation is None:
            self.decay_enabled = False
            return
        self._off_lot_callback_data = self.add_callback(self._off_lot_simulation.threshold, self._off_lot_simulation_callback)
        self.decay_enabled = True

    def stop_low_level_simulation(self):
        self.decay_enabled = False
        if self._off_lot_callback_data is not None:
            self.remove_callback(self._off_lot_callback_data)

    def stop_regular_simulation(self):
        self._remove_state_callback()
        self.decay_enabled = False
        if self._convergence_callback_data is not None:
            self.remove_callback(self._convergence_callback_data)
            self._convergence_callback_data = None
        if self._distress_callback_data is not None:
            self.remove_callback(self._distress_callback_data)
            self._distress_callback_data = None
        if self.commodity_distress is not None:
            self._exit_distress(self, True)
        if self._failure_callback_data is not None:
            self.remove_callback(self._failure_callback_data)
            self._failure_callback_data = None

    def _find_state_index(self, current_value):
        index = len(self.commodity_states) - 1
        while index >= 0:
            state = self.commodity_states[index]
            if current_value >= state.value:
                return index
            index -= 1
        return 0

    def _add_state_callback(self):
        next_state_index = self._current_state_index + 1
        if next_state_index < len(self.commodity_states):
            self._current_state_ge_callback_data = self.add_callback(Threshold(self.commodity_states[next_state_index].value, operator.ge), self._update_state_up, on_callback_alarm_reset=self._state_reset_callback)
        if self.commodity_states[self._current_state_index].value > self.min_value:
            self._current_state_lt_callback_data = self.add_callback(Threshold(self.commodity_states[self._current_state_index].value, operator.lt), self._update_state_down, on_callback_alarm_reset=self._state_reset_callback)

    def _remove_state_callback(self):
        if self._current_state_ge_callback_data is not None:
            self.remove_callback(self._current_state_ge_callback_data)
            self._current_state_ge_callback_data = None
        if self._current_state_lt_callback_data is not None:
            self.remove_callback(self._current_state_lt_callback_data)
            self._current_state_lt_callback_data = None
        if self._buff_threshold_callback is not None:
            self.remove_callback(self._buff_threshold_callback)
            self._buff_threshold_callback = None

    def _get_next_buff_commodity_decaying_to(self):
        transition_into_buff_id = 0
        if self._current_state_index is not None and self._current_state_index > 0:
            current_value = self.get_value()
            buff_tunable_ref = None
            if self.convergence_value <= current_value:
                buff_tunable_ref = self.commodity_states[self._current_state_index - 1].buff
            else:
                next_state_index = self._current_state_index + 1
                if next_state_index < len(self.commodity_states):
                    buff_tunable_ref = self.commodity_states[next_state_index].buff
            if buff_tunable_ref is not None:
                buff_type = buff_tunable_ref.buff_type
                if buff_type is not None and buff_type.visible:
                    transition_into_buff_id = buff_type.guid64
        return transition_into_buff_id

    def _add_buff_from_state(self, commodity_state):
        owner = self.tracker.owner
        if owner.is_sim:
            buff_tuning = commodity_state.buff
            transition_into_buff_id = self._get_next_buff_commodity_decaying_to() if buff_tuning.buff_type.visible else 0
            self._buff_handle = owner.add_buff(buff_tuning.buff_type, buff_reason=buff_tuning.buff_reason, commodity_guid=self.guid64, change_rate=self._get_change_rate_without_decay(), transition_into_buff_id=transition_into_buff_id)

    def _add_buff_callback(self, _):
        current_state = self.commodity_states[self._current_state_index]
        self.remove_callback(self._buff_threshold_callback)
        self._buff_threshold_callback = None
        self._add_buff_from_state(current_state)

    def _set_state(self, new_state_index, current_value, from_init=False, send_client_update=True):
        new_state = self.commodity_states[new_state_index]
        old_state_index = self._current_state_index
        self._current_state_index = new_state_index
        if self._buff_threshold_callback is not None:
            self.remove_callback(self._buff_threshold_callback)
            self._buff_threshold_callback = None
        if self._buff_handle is not None:
            self.tracker.owner.remove_buff(self._buff_handle)
            self._buff_handle = None
        if new_state.buff.buff_type:
            if new_state.buff_add_threshold is not None and not self.force_apply_buff_on_start_up and not new_state.buff_add_threshold.compare(current_value):
                self._buff_threshold_callback = self.add_callback(new_state.buff_add_threshold, self._add_buff_callback)
            else:
                self._add_buff_from_state(new_state)
        if (old_state_index is not None or from_init) and new_state.loot_list_on_enter is not None and self.tracker.owner.is_sim:
            resolver = event_testing.resolver.SingleSimResolver(self.tracker.owner)
            while True:
                for loot_action in new_state.loot_list_on_enter:
                    loot_action.apply_to_resolver(resolver)
        if send_client_update:
            self.send_commodity_progress_msg()

    def _enter_distress(self, stat_instance):
        if self.tracker.owner.get_sim_instance() is None:
            return
        if self.commodity_distress.buff.buff_type is not None:
            if self._distress_buff_handle is None:
                self._distress_buff_handle = self.tracker.owner.add_buff(self.commodity_distress.buff.buff_type, self.commodity_distress.buff.buff_reason, commodity_guid=self.guid64)
            else:
                logger.error('Distress Buff Handle is not none when entering Commodity Distress for {}.', self, owner='jjacobson')
        if self._exit_distress_callback_data is None:
            self._exit_distress_callback_data = self.add_callback(Threshold(self.commodity_distress.threshold_value, operator.gt), self._exit_distress)
        else:
            logger.error('Exit Distress Callback Data is not none when entering Commodity Distress for {}.', self, owner='jjacobson')
        self.tracker.owner.enter_distress(self)
        sim = self.tracker.owner.get_sim_instance()
        for si in itertools.chain(sim.si_state, sim.queue):
            while self.stat_type in si.commodity_flags:
                return
        context = interactions.context.InteractionContext(self.tracker.owner.get_sim_instance(), interactions.context.InteractionContext.SOURCE_AUTONOMY, interactions.priority.Priority.High, insert_strategy=QueueInsertStrategy.NEXT, bucket=interactions.context.InteractionBucketType.DEFAULT)
        self.tracker.owner.get_sim_instance().push_super_affordance(self.commodity_distress.distress_interaction, None, context)

    def _exit_distress(self, stat_instance, on_removal=False):
        if self._distress_buff_handle is not None:
            self.tracker.owner.remove_buff(self._distress_buff_handle)
            self._distress_buff_handle = None
        elif self.commodity_distress.buff.buff_type is not None and not on_removal:
            logger.error('Distress Buff Handle is none when exiting Commodity Distress for {}.', self, owner='jjacobson')
        if self._exit_distress_callback_data is not None:
            self.remove_callback(self._exit_distress_callback_data)
            self._exit_distress_callback_data = None
        elif not on_removal:
            logger.error('Exit distress called before exit distress callback has been setup for {}.', self, owner='jjacobson')
        self.tracker.owner.exit_distress(self)

    def _commodity_fail_object(self, stat_instance):
        context = interactions.context.InteractionContext(None, interactions.context.InteractionContext.SOURCE_SCRIPT, interactions.priority.Priority.Critical, bucket=interactions.context.InteractionBucketType.DEFAULT)
        owner = self.tracker.owner
        for failure_interaction in self.commodity_failure.failure_interactions:
            if not failure_interaction.immediate or not failure_interaction.simless:
                logger.error('Trying to use a non-immediate and/or non-simless\n                interaction as a commodity failure on an object. Object\n                commodity failures can only push immediate, simless\n                interactions. - trevor')
                break
            aop = interactions.aop.AffordanceObjectPair(failure_interaction, owner, failure_interaction, None)
            while aop.test_and_execute(context):
                break

    def _commodity_fail(self, stat_instance):
        owner = self.tracker.owner
        if not owner.is_sim:
            return self._commodity_fail_object(stat_instance)
        sim = owner.get_sim_instance()
        if sim is None:
            return
        context = interactions.context.InteractionContext(sim, interactions.context.InteractionContext.SOURCE_SCRIPT, interactions.priority.Priority.Critical, bucket=interactions.context.InteractionBucketType.DEFAULT)
        for failure_interaction in self.commodity_failure.failure_interactions:
            while sim.push_super_affordance(failure_interaction, None, context):
                break

    def fixup_on_sim_instantiated(self):
        sim = self.tracker.owner
        if self.time_passage_fixup_type() == CommodityTimePassageFixupType.FIXUP_USING_TIME_ELAPSED:
            time_sim_was_saved = sim.time_sim_was_saved
            if time_sim_was_saved is not None:
                if not sim.is_locked(self):
                    self.decay_enabled = True
                    self._last_update = time_sim_was_saved
                    self._update_value()
                    self.decay_enabled = False
        elif self.time_passage_fixup_type() == CommodityTimePassageFixupType.FIXUP_USING_AUTOSATISFY_CURVE and (sim.is_npc or self._off_lot_simulation is None):
            self.set_to_auto_satisfy_value()

    def set_to_auto_satisfy_value(self):
        if self.use_autosatisfy_curve and self._auto_satisfy_curve:
            now = services.time_service().sim_now
            time_sim_was_saved = self.tracker.owner.time_sim_was_saved
            if time_sim_was_saved is None and not self.use_auto_satisfy_curve_as_initial_value or time_sim_was_saved == now:
                return False
            random_time_offset = random.uniform(-1*self.auto_satisfy_curve_random_time_offset, self.auto_satisfy_curve_random_time_offset)
            now += interval_in_sim_minutes(random_time_offset)
            current_hour = now.hour() + now.minute()/date_and_time.MINUTES_PER_HOUR
            auto_satisfy_value = self._auto_satisfy_curve.get(current_hour)
            maximum_auto_satisfy_time = interval_in_sim_minutes(self.maximum_auto_satisfy_time)
            if time_sim_was_saved is None or time_sim_was_saved + maximum_auto_satisfy_time <= now:
                self._last_update = services.time_service().sim_now
                self.set_user_value(auto_satisfy_value)
                return True
            if time_sim_was_saved >= now:
                return False
            interpolation_time = (now - time_sim_was_saved).in_ticks()/maximum_auto_satisfy_time.in_ticks()
            current_value = self.get_user_value()
            new_value = (auto_satisfy_value - current_value)*interpolation_time + current_value
            self._last_update = services.time_service().sim_now
            self.set_user_value(new_value)
            return True
        return False

    def on_remove(self, on_destroy=False):
        super().on_remove(on_destroy=on_destroy)
        self.stop_regular_simulation()
        self.stop_low_level_simulation()
        if self._buff_handle is not None:
            self.tracker.owner.remove_buff(self._buff_handle, on_destroy=on_destroy)
            self._buff_handle = None
        if self._distress_buff_handle is not None:
            self.tracker.owner.remove_buff(self._distress_buff_handle, on_destroy=on_destroy)
            self._distress_buff_handle = None

    def _activate_convergence_callback(self):
        if self._allow_convergence_callback_to_activate:
            if self._convergence_callback_data is not None:
                self.add_callback_data(self._convergence_callback_data)
            self._allow_convergence_callback_to_activate = False

    def set_value(self, value, from_load=False, **kwargs):
        with self._suppress_client_updates_context_manager(from_load=from_load, is_rate_change=False):
            if not from_load:
                change = value - self.get_value()
                self._update_buff(change)
            super().set_value(value, from_load=from_load, **kwargs)
            if not from_load and self.visible:
                self.send_commodity_progress_msg(is_rate_change=False)
            self._update_buff(0)
            self._activate_convergence_callback()

    def _on_statistic_modifier_changed(self, notify_watcher=True):
        super()._on_statistic_modifier_changed(notify_watcher=notify_watcher)
        self.send_commodity_progress_msg()
        self._update_buff(self._get_change_rate_without_decay())
        self._update_callbacks()
        self._activate_convergence_callback()

    def _recalculate_modified_decay_rate(self):
        super()._recalculate_modified_decay_rate()
        if self._decay_rate_modifier > 1:
            self._update_buff(-self._decay_rate_modifier)
        else:
            self._update_buff(0)

    @property
    def buff_handle(self):
        return self._buff_handle

    def _update_buff(self, change_rate):
        if self._buff_handle is not None:
            self.tracker.owner.buff_commodity_changed(self._buff_handle, change_rate=change_rate)

    @property
    def state_index(self):
        return self._current_state_index

    @classmethod
    def get_state_index_matches_buff_type(cls, buff_type):
        if cls.commodity_states:
            for index in range(len(cls.commodity_states)):
                state = cls.commodity_states[index]
                if state.buff is None:
                    pass
                while state.buff.buff_type is buff_type:
                    return index

    @classproperty
    def max_value(cls):
        return cls.max_value_tuning

    @classproperty
    def min_value(cls):
        return cls.min_value_tuning

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

    @classproperty
    def default_value(cls):
        if not cls.initial_as_default:
            return cls._default_convergence_value
        return cls.initial_value

    @classproperty
    def is_skill(cls):
        return False

    @classproperty
    def add_if_not_in_tracker(cls):
        return cls._add_if_not_in_tracker

    @classproperty
    def max_simulate_time_on_load(cls):
        return cls._max_simulate_time_on_load

    def time_passage_fixup_type(self):
        return self._time_passage_fixup_type

    @classmethod
    def get_categories(cls):
        return cls._categories

    def send_commodity_progress_msg(self, is_rate_change=True):
        commodity_msg = self.create_commmodity_update_msg(is_rate_change=is_rate_change)
        if commodity_msg is None:
            return
        send_sim_commodity_progress_update_message(self.tracker.owner, commodity_msg)

    def create_commmodity_update_msg(self, is_rate_change=True):
        if self.tracker is None or not self.tracker.owner.is_sim:
            return
        if not self.visible:
            return
        if not self.commodity_states:
            return
        if self.state_index is None:
            return
        if self._suppress_client_updates:
            return
        commodity_msg = Commodities_pb2.CommodityProgressUpdate()
        commodity_msg.commodity_id = self.guid64
        commodity_msg.current_value = self.get_value()
        commodity_msg.rate_of_change = self.get_change_rate()
        commodity_msg.commodity_state_index = self.state_index
        commodity_msg.is_rate_change = is_rate_change
        return commodity_msg