Example #1
0
class SalesTableVendorSituationMixin:
    INSTANCE_TUNABLES = {
        'setup_state':
        SalesTableSetupState.TunableFactory(
            tuning_group=SituationComplexCommon.SITUATION_STATE_GROUP,
            display_name='01_setup_state'),
        'tend_state':
        TendSalesTableState.TunableFactory(
            tuning_group=SituationComplexCommon.SITUATION_STATE_GROUP,
            display_name='02_tend_state'),
        'teardown_state':
        SalesTableTeardownState.TunableFactory(
            tuning_group=SituationComplexCommon.SITUATION_STATE_GROUP,
            display_name='03_teardown_state'),
        'vendor_job_and_role_state':
        TunableSituationJobAndRoleState(
            description=
            '\n            Job and Role State for the vendor.\n            '),
        'sale_object_tags':
        TunableList(
            description=
            '\n            A list of tags that tell us the object comes from the vendor. We\n            use these tags to find objects and destroy them when the situation\n            ends or the sim is removed.\n            ',
            tunable=TunableEnumEntry(
                description=
                '\n                A tag that denotes the object comes from the craft sales vendor\n                and can be destroyed if the situation ends or the sim leaves.\n                ',
                tunable_type=Tag,
                default=Tag.INVALID)),
        'number_of_sale_objects':
        TunableInterval(description='\n            ',
                        tunable_type=int,
                        default_lower=7,
                        default_upper=10,
                        minimum=1,
                        maximum=15)
    }

    @classmethod
    def default_job(cls):
        pass

    @classmethod
    def _states(cls):
        return (SituationStateData(1,
                                   SalesTableSetupState,
                                   factory=cls.setup_state),
                SituationStateData(2,
                                   TendSalesTableState,
                                   factory=cls.tend_state),
                SituationStateData(3,
                                   SalesTableTeardownState,
                                   factory=cls.teardown_state))

    @classmethod
    def _get_tuned_job_and_default_role_state_tuples(cls):
        return [(cls.vendor_job_and_role_state.job,
                 cls.vendor_job_and_role_state.role_state)]

    def start_situation(self):
        super().start_situation()
        self._change_state(self.setup_state())
Example #2
0
 def __init__(self, default_lower=40, default_upper=60, **kwargs):
     super().__init__(
         start_delay=TunableSimMinute(
             description=
             '\n                    Delay in sim minutes before change starts.  Used if new weather is more\n                    severe than existing weather.\n                    ',
             default=1,
             minimum=0),
         start_rate=Tunable100ConvertRange(
             description=
             '\n                    Rate at which ramp up occurs.  Used if new weather is more\n                    severe than existing weather.\n                    ',
             default=3.3,
             minimum=0),
         end_delay=TunableSimMinute(
             description=
             '\n                    Delay in sim minutes before element ends.  Used if existing weather is more\n                    severe than new weather.\n                    ',
             default=1,
             minimum=0),
         end_rate=Tunable100ConvertRange(
             description=
             '\n                    Rate at which ramp doown occurs.  Used if existing weather is more\n                    severe than new weather.\n                    ',
             default=3.3,
             minimum=0),
         range=TunableInterval(
             description=
             '\n                    Range.\n                    ',
             tunable_type=Tunable100ConvertRange,
             minimum=0,
             maximum=100,
             default_lower=default_lower,
             default_upper=default_upper),
         **kwargs)
Example #3
0
class WaitAroundState(CommonSituationState):
    FACTORY_TUNABLES = {
        'timeout':
        TunableInterval(
            description=
            '\n            Time amount of time that must pass before switching into the next\n            state.\n            ',
            tunable_type=TunableSimMinute,
            default_lower=5,
            default_upper=10,
            minimum=0)
    }

    def __init__(self, *args, timeout=None, **kwargs):
        super().__init__(*args, **kwargs)
        self._timeout = timeout

    def on_activate(self, reader=None):
        super().on_activate(reader=reader)
        self._create_or_load_alarm(WAIT_AROUND_STATE_TIMEOUT,
                                   self._timeout.random_float(),
                                   self.timer_expired,
                                   should_persist=True,
                                   reader=reader)

    def timer_expired(self, _):
        self.owner._change_state(self.owner._leave_state())

    def _on_set_sim_role_state(self, sim, *args, **kwargs):
        super()._on_set_sim_role_state(sim, *args, **kwargs)
        self.owner._cancel_leave_interaction(sim)
Example #4
0
class _PreOrderCoffeeState(CommonSituationState):
    FACTORY_TUNABLES = {
        'wait_to_order_duration':
        TunableInterval(
            description=
            '\n            The duration in Sim minutes for the Sim to wait before ordering coffee when\n            they spawn at the Cafe. Any behavior can be tuned for ths Sim to\n            perform before ordering coffee.\n            ',
            tunable_type=TunableSimMinute,
            default_lower=10,
            default_upper=100,
            minimum=0)
    }

    def __init__(self, wait_to_order_duration, **kwargs):
        super().__init__(**kwargs)
        self._wait_to_order_duration = wait_to_order_duration

    def on_activate(self, reader=None):
        super().on_activate(reader)
        self._create_or_load_alarm(ORDER_COFFEE_TIMEOUT,
                                   self._wait_to_order_duration.random_float(),
                                   lambda _: self.timer_expired(),
                                   should_persist=True,
                                   reader=reader)

    def timer_expired(self):
        self.owner._change_state(self.owner.get_order_coffee_state())
Example #5
0
class LeaveState(CommonSituationState):
    FACTORY_TUNABLES = {
        'timeout':
        TunableInterval(
            description=
            '\n            Time amount of time in Sim Minutes that must pass before switching\n            into the next\n            state.\n            ',
            tunable_type=TunableSimMinute,
            default_lower=10,
            default_upper=100,
            minimum=0)
    }

    def __init__(self, *args, timeout=None, **kwargs):
        super().__init__(*args, **kwargs)
        self._timeout = timeout

    def on_activate(self, reader=None):
        super().on_activate(reader=reader)
        self._create_or_load_alarm(LEAVE_STATE_TIMEOUT,
                                   self._timeout.random_float(),
                                   self.timer_expired,
                                   should_persist=True,
                                   reader=reader)

    def timer_expired(self, _):
        if self.owner is None:
            return
        self.owner._change_state(self.owner._wait_a_bit_state())
Example #6
0
class _CustomIntervalTest(HasTunableSingletonFactory, AutoFactoryInit):
    FACTORY_TUNABLES = {
        'height_range':
        TunableInterval(tunable_type=float,
                        default_lower=-1000,
                        default_upper=1000,
                        minimum=-1000,
                        maximum=1000)
    }

    def evaluate(self, subject, water_height, wading_interval, negate,
                 tooltip):
        lower_bound = self.height_range.lower_bound
        upper_bound = self.height_range.upper_bound
        in_interval = lower_bound <= water_height <= upper_bound
        if in_interval:
            if negate:
                return TestResult(
                    False,
                    f'{subject} cannot go here. Water height {water_height} is between {lower_bound} and {upper_bound} and negate is True.',
                    tooltip=tooltip)
            return TestResult.TRUE
        elif negate:
            return TestResult.TRUE
        else:
            return TestResult(
                False,
                f'{subject} cannot go here. Water height {water_height} is not between {lower_bound} and {upper_bound}',
                tooltip=tooltip)
Example #7
0
class WeatherStartEventLootOp(BaseLootOperation):
    FACTORY_TUNABLES = {
        'weather_event':
        WeatherEvent.TunableReference(
            description=
            '\n            The weather event to start.\n            '),
        'duration':
        TunableInterval(
            description=
            '\n            How long the event should last, in hours.\n            ',
            tunable_type=float,
            minimum=1.0,
            default_lower=1.0,
            default_upper=2.0)
    }

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

    def _apply_to_subject_and_target(self, subject, target, resolver):
        weather_service = services.weather_service()
        if weather_service is not None:
            weather_service.start_weather_event(self.weather_event,
                                                self.duration.random_float())
Example #8
0
class SkillInterval(HasTunableSingletonFactory, AutoFactoryInit):
    FACTORY_TUNABLES = {
        'skill_interval':
        TunableInterval(
            description=
            '\n            The range (inclusive) a skill level must be in to pass this test.\n            ',
            tunable_type=int,
            default_lower=1,
            default_upper=10,
            minimum=0,
            maximum=statistics.skill.MAX_SKILL_LEVEL)
    }
    __slots__ = ('skill_interval', )

    @property
    def skill_range_min(self):
        return self.skill_interval.lower_bound

    @property
    def skill_range_max(self):
        return self.skill_interval.upper_bound

    def __call__(self, curr_value):
        if curr_value < self.skill_interval.lower_bound or curr_value > self.skill_interval.upper_bound:
            return TestResult(False, 'skill level not in desired range.')
        return TestResult.TRUE
Example #9
0
 def __init__(self, **kwargs):
     super().__init__(
         distance=TunableInterval(
             description=
             '\n                The distance range relative to origin point that \n                the generated point should be in.\n                ',
             tunable_type=float,
             minimum=0,
             default_lower=1,
             default_upper=5),
         angle=TunableAngle(
             description=
             '\n                The slice of the donut/circle in degrees.\n                ',
             default=TWO_PI),
         offset=TunableAngle(
             description=
             '\n                An offset (rotation) in degrees, affecting where the start\n                of the angle is.  This has no effect if angle is 360 degrees.\n                ',
             default=0),
         axis=TunableVariant(
             description=
             '\n                Around which axis the position will be located.\n                ',
             default='y',
             locked_args={
                 'x': Vector3.X_AXIS(),
                 'y': Vector3.Y_AXIS(),
                 'z': Vector3.Z_AXIS()
             }),
         **kwargs)
class BroadcasterEffectSelfLoot(_BroadcasterEffectTested):
    FACTORY_TUNABLES = {
        'broadcastee_count_interval':
        TunableInterval(
            description=
            '\n            If the number of objects within this broadcaster is in this\n            interval, loot will be awarded. Includes lower and upper.\n            ',
            tunable_type=int,
            default_lower=1,
            default_upper=2,
            minimum=0),
        'loot_list':
        TunableList(description=
                    '\n            A list of loot operations.\n            ',
                    tunable=TunableReference(
                        manager=services.get_instance_manager(
                            sims4.resources.Types.ACTION),
                        class_restrictions=('LootActions', ),
                        pack_safe=True)),
        'apply_loot_on_remove':
        Tunable(
            description=
            "\n            If enabled, determine whether or not we want to apply this broadcaster's\n            loot when the broadcaster is removed.\n            True means we will apply the loot on removal of the broadcaster\n            False means we will apply the loot as soon as enough sims enter the constraint\n            ",
            tunable_type=bool,
            default=True)
    }

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

    @classproperty
    def apply_when_linked(cls):
        return True

    @classproperty
    def apply_when_removed(cls):
        return True

    def _count_is_within_interval(self, broadcaster):
        object_count = len(self._observing_objs)
        return object_count in self.broadcastee_count_interval

    def apply_broadcaster_loot(self, broadcaster):
        if self.apply_loot_on_remove:
            self._try_apply_loot(broadcaster)
        self._observing_objs = set()

    def _apply_broadcaster_effect(self, broadcaster, affected_object):
        if self._should_apply_broadcaster_effect(broadcaster, affected_object):
            self._observing_objs.add(affected_object.id)
        if not self.apply_loot_on_remove:
            self._try_apply_loot(broadcaster)

    def _try_apply_loot(self, broadcaster):
        if self._count_is_within_interval(broadcaster):
            resolver = broadcaster.get_resolver(
                broadcaster.broadcasting_object)
            for loot_action in self.loot_list:
                loot_action.apply_to_resolver(resolver)
Example #11
0
class PersistenceTuning:
    __qualname__ = 'PersistenceTuning'
    SAVE_GAME_COOLDOWN = TunableRealSecond(0, minimum=0, description='Cooldown on the save game button to prevent users from saving too often.')
    MAX_LOT_SIMULATE_ELAPSED_TIME = TunableSimMinute(description='\n        When we load a lot that was saved in the past and world time has\n        elapsed, this is the max amount of time the lot will pretend time will\n        elapse. EX: lot was saved at 8:00am sunday. The player goes to another\n        lot at that point and plays until 8:00am Tuesday. If max simulated time\n        is set to 1440 mins (24 hours), the lot will load and realize more than\n        24 hours have passed between sunday and tuesday, so the lot will start\n        off at time 8:00am Tuesday - 24 hours = 8:00am Monday. And then the lot\n        will progress for 24 hours forwards to Tuesday.\n        ', default=1440, minimum=0)
    MINUTES_STAY_ON_LOT_BEFORE_GO_HOME = TunableInterval(description="\n        For all sims, when the sim is saved NOT on their home lot, we use this\n        interval to determine how many minutes they'll stay on that lot before\n        they go home. \n\n        Then, if we load up the non-home lot past this amount of time, that sim\n        will no longer be on that lot because that sim will have gone home.\n        \n        If the we load up on the sim's home lot -- if less than this amount of\n        time has passed, we set an alarm so that the sim will spawn into their\n        home lot at the saved time. If equal or more than this amount of time\n        has passed, that sim will be spawned in at zone load.\n        \n        The amount of time is a range. When loading, we'll randomly pick between\n        the upper and lower limit of the range.\n        ", tunable_type=TunableSimMinute, default_lower=180, default_upper=240, minimum=0)
    SAVE_FAILED_REASONS = TunableTuple(description='\n        Localized strings to display when the user cannot save.\n        ', generic=TunableLocalizedString(description='\n            Generic message for why game cannot be saved at the moment\n            '), on_cooldown=TunableLocalizedString(description='\n            The message to show when save game failed due to save being on cooldown\n            '), exception_occurred=TunableLocalizedStringFactory(description='\n            The message to show when save game failed due to an exception occuring during save\n            '))
    LOAD_ERROR_REQUEST_RESTART = ui.ui_dialog.UiDialogOk.TunableFactory(description='\n        The dialog that will be triggered when exception occurred during load of zone and ask user to restart game.\n        ')
    LOAD_ERROR = ui.ui_dialog.UiDialogOk.TunableFactory(description='\n        The dialog that will be triggered when exception occurred during load of zone.\n        ')
Example #12
0
class TunableStatAsmParam(HasTunableSingletonFactory, AutoFactoryInit):
    FACTORY_TUNABLES = {
        'level_ranges':
        TunableMapping(
            description=
            '\n            The value mapping of the stat range to stat value or user value. If\n            use_user_value is True, the range should be user value, otherwise\n            stat value.\n            ',
            key_type=Tunable(
                description=
                "\n                The asm parameter for Sim's stat level.\n                ",
                tunable_type=str,
                default=None,
                source_query=SourceQueries.SwingEnumNamePattern.format(
                    'statLevel')),
            value_type=TunableInterval(
                description=
                '\n                Stat value fall into the range (inclusive).\n                ',
                tunable_type=float,
                default_lower=1,
                default_upper=1)),
        'asm_param_name':
        Tunable(description='\n            The asm param name.\n            ',
                tunable_type=str,
                default='statLevel'),
        'use_user_value':
        Tunable(
            description=
            '\n            Whether use the user value or stat value to decide the asm_param.\n            ',
            tunable_type=bool,
            default=True),
        'use_effective_skill_level':
        Tunable(
            description=
            '\n            If true, the effective skill level of the Sim will be used for \n            the asm_param.\n            ',
            tunable_type=bool,
            default=True),
        'always_apply':
        Tunable(
            description=
            '\n            If checked, this parameter is always applied on any ASM involving the\n            owning Sim.\n            ',
            tunable_type=bool,
            default=False)
    }

    def get_asm_param(self, stat):
        stat_value = stat.get_user_value(
        ) if self.use_user_value else stat.get_value()
        if stat.is_skill:
            if self.use_effective_skill_level:
                stat_value = stat.tracker.owner.get_effective_skill_level(stat)
        asm_param_value = None
        for (range_key, stat_range) in self.level_ranges.items():
            if stat_value >= stat_range.lower_bound:
                if stat_value <= stat_range.upper_bound:
                    asm_param_value = range_key
                    break
        return (self.asm_param_name, asm_param_value)
Example #13
0
class _QuirkCountDynamic(HasTunableSingletonFactory, AutoFactoryInit):
    FACTORY_TUNABLES = {'interval': TunableInterval(description='\n            The Sim is going to receive between these many quirks from this set.\n            ', tunable_type=int, default_lower=0, default_upper=1, minimum=0), 'trait_modifiers': TunableMapping(description='\n            If the Sim is equipped with this trait, the available number of\n            quirks is modified accordingly.\n            \n            NOTE: You can specify negative values to subtract from the count.\n            ', key_type=TunableReference(description='\n                The Sim must have this trait in order for the modifier to be\n                applied.\n                ', manager=services.get_instance_manager(sims4.resources.Types.TRAIT), pack_safe=True), value_type=TunableTuple(lower_bound_modifier=Tunable(description='\n                    The lower bound of the available quirk count is modified by\n                    this amount.\n                    ', tunable_type=int, default=1), upper_bound_modifier=Tunable(description='\n                    The upper bound of the available quirk count is modified by\n                    this amount.\n                    ', tunable_type=int, default=1)))}

    def __call__(self, sim_info, random):
        interval = self.interval
        for (trait, modifier) in self.trait_modifiers.items():
            if sim_info.has_trait(trait):
                interval = TunedInterval(interval.lower_bound + modifier.lower_bound_modifier, interval.upper_bound + modifier.upper_bound_modifier)
        if interval.lower_bound > interval.upper_bound:
            return interval.lower_bound
        return random.randint(interval.lower_bound, interval.upper_bound)
Example #14
0
class BusinessSettingTest(HasTunableSingletonFactory, AutoFactoryInit,
                          test_base.BaseTest):
    FACTORY_TUNABLES = {
        'quality_setting':
        OptionalTunable(
            description=
            '\n            A test to see if the current business has certain settings set by the owner.\n            ',
            tunable=TunableWhiteBlackList(
                description=
                '\n                Use of this white/black list will check whether or not the \n                current on-lot business is set to certain quality settings.\n                ',
                tunable=TunableEnumEntry(
                    description=
                    '\n                    Business Quality Type from business settings.\n                    ',
                    tunable_type=BusinessQualityType,
                    default=BusinessQualityType.INVALID,
                    invalid_enums=(BusinessQualityType.INVALID, ))),
            disabled_name='ignore',
            enabled_name='test'),
        'star_rating':
        OptionalTunable(
            description=
            '\n            A test to see if the current business is within a star rating range.\n            ',
            tunable=TunableInterval(
                description=
                "\n                If the business's star rating is within this range, this test passes.\n                ",
                tunable_type=float,
                default_lower=0,
                default_upper=5,
                minimum=0),
            disabled_name='ignore',
            enabled_name='test')
    }

    def get_expected_args(self):
        return {}

    def __call__(self):
        business_manager = services.business_service(
        ).get_business_manager_for_zone()
        if business_manager is None:
            return TestResult(False, 'Not currently on a business lot.')
        if self.quality_setting is not None and not self.quality_setting.test_item(
                business_manager.quality_setting):
            return TestResult(
                False, 'Business is set to {}'.format(
                    business_manager.quality_setting))
        if self.star_rating is not None:
            business_star_rating = business_manager.get_star_rating()
            if business_star_rating not in self.star_rating:
                return TestResult(
                    False,
                    'Business star rating is {}'.format(business_star_rating))
        return TestResult.TRUE
Example #15
0
class TunableStatAsmParam(HasTunableSingletonFactory, AutoFactoryInit):
    __qualname__ = 'TunableStatAsmParam'
    FACTORY_TUNABLES = {
        'description':
        '\n            A tunable factory use the stat_value to decide tuple(asm_param_name, \n            asm_param_value) to set on the asm.\n            ',
        'level_ranges':
        TunableMapping(
            description=
            '\n            The value mapping of the stat range to stat value or user value. \n            If use_user_value is True, the range should be user value, \n            otherwise stat value.\n            ',
            key_type=Tunable(
                description=
                "The asm parameter for Sim's \n                 stat level.\n                 ",
                tunable_type=str,
                default=None,
                source_query=SourceQueries.SwingEnumNamePattern.format(
                    'statLevel')),
            value_type=TunableInterval(
                description=
                '\n                Stat value fall into the range (inclusive).\n                ',
                tunable_type=float,
                default_lower=1,
                default_upper=1)),
        'asm_param_name':
        Tunable(description='\n            The asm param name.\n            ',
                tunable_type=str,
                default='statLevel'),
        'use_user_value':
        Tunable(
            description=
            '\n            Whether use the user value or stat value to decide the asm_param.\n            ',
            tunable_type=bool,
            default=True),
        'use_effective_skill_level':
        Tunable(
            description=
            '\n            If true, the effective skill level of the Sim will be used for \n            the asm_param.\n            ',
            tunable_type=bool,
            default=True)
    }

    def get_asm_param(self, stat):
        stat_value = stat.get_user_value(
        ) if self.use_user_value else self.stat.get_value()
        if stat.is_skill and self.use_effective_skill_level:
            stat_value = stat.tracker.owner.get_effective_skill_level(stat)
        asm_param_value = None
        for (range_key, stat_range) in self.level_ranges.items():
            while stat_value >= stat_range.lower_bound and stat_value <= stat_range.upper_bound:
                asm_param_value = range_key
                break
        return (self.asm_param_name, asm_param_value)
Example #16
0
class TeamScorePoints(BaseGameLootOperation):
    def __init__(self, score_increase, score_increase_from_stat, **kwargs):
        super().__init__(**kwargs)
        self.score_increase = score_increase
        self.score_increase_from_stat = score_increase_from_stat

    @property
    def loot_type(self):
        return interactions.utils.LootType.TEAM_SCORE

    def _apply_to_subject_and_target(self, subject, target, resolver):
        (game, _) = get_game_references(resolver)
        if game is None:
            return False
        subject_obj = self._get_object_from_recipient(subject)
        if self.score_increase_from_stat is not None:
            stat = subject_obj.get_statistic(self.score_increase_from_stat)
            if stat is None:
                logger.error('Failed to find statistic {} from {}.',
                             self.score_increase_from_stat,
                             subject_obj,
                             owner='mkartika')
                return False
            score_increase = stat.get_value()
        else:
            score_increase = sims4.random.uniform(
                self.score_increase.lower_bound,
                self.score_increase.upper_bound)
        game.increase_score_by_points(subject_obj, score_increase)
        return True

    FACTORY_TUNABLES = {
        'score_increase':
        TunableInterval(
            description=
            '\n            An interval specifying the minimum and maximum score increases\n            from this loot. A random value in this interval will be\n            generated each time this loot is given.\n            ',
            tunable_type=int,
            default_lower=35,
            default_upper=50,
            minimum=0),
        'score_increase_from_stat':
        OptionalTunable(
            description=
            "\n            If enabled, the score will be increased by this statistic value\n            instead of by 'Score Increase' interval value.\n            ",
            tunable=TunableReference(
                description=
                '\n                The stat we are operating on.\n                ',
                manager=services.statistic_manager()))
    }
Example #17
0
class SituationJobChurn(HasTunableSingletonFactory, AutoFactoryInit):
    __qualname__ = 'SituationJobChurn'
    FACTORY_TUNABLES = {
        'min_duration':
        TunableSimMinute(
            description=
            '\n                Minimum amount of time a sim in this job will stay before they\n                might be churned out.\n                ',
            default=60),
        'auto_populate_by_time_of_day':
        TunableMapping(
            description=
            "\n                Each entry in the map has two columns.\n                The first column is the hour of the day (0-24) \n                that this entry begins to control the number of sims in the job.\n                The second column is the minimum and maximum desired number\n                of sims.\n                The entry with starting hour that is closest to, but before\n                the current hour will be chosen.\n                \n                Given this tuning: \n                    beginning_hour        desired_population\n                    6                     1-3\n                    10                    3-5\n                    14                    5-7\n                    20                    7-9\n                    \n                if the hour is 11, beginning_hour will be 10 and desired is 3-5.\n                if the hour is 19, beginning_hour will be 14 and desired is 5-7.\n                if the hour is 23, beginning_hour will be 20 and desired is 7-9.\n                if the hour is 2, beginning_hour will be 20 and desired is 7-9. (uses 20 tuning because it is not 6 yet)\n                \n                The entries will be automatically sorted by time on load, so you\n                don't have to put them in order (but that would be nutty)\n                ",
            key_type=Tunable(tunable_type=int, default=0),
            value_type=TunableInterval(tunable_type=int,
                                       default_lower=0,
                                       default_upper=0),
            key_name='beginning_hour',
            value_name='desired_population'),
        'chance_to_add_or_remove_sim':
        TunableRange(
            description=
            '\n                Periodically the churn system will re-evaluate the number of sims\n                currently in the job. If the number of sims is above or below\n                the range it will add/remove one sim as appropriate. \n                If the number of sims is within the tuned\n                range it will roll the dice to determine what it should do:\n                    nothing\n                    add a sim\n                    remove a sim\n                    \n                The chance tuned here (1-100) is the chance that it will do\n                something (add/remove), as opposed to nothing. \n                \n                When it is going to do something, the determination of \n                whether to add or remove is roughly 50/50 with additional\n                checks to stay within the range of desired sims and respect the\n                min duration.\n                ',
            tunable_type=int,
            default=20,
            minimum=0,
            maximum=100)
    }

    def get_auto_populate_interval(self, time_of_day=None):
        if not self.auto_populate_by_time_of_day:
            return AutoPopulateInterval(min=0, max=0)
        if time_of_day is None:
            time_of_day = services.time_service().sim_now
        auto_populate = []
        for (beginning_hour,
             interval) in self.auto_populate_by_time_of_day.items():
            auto_populate.append((beginning_hour, interval))
        auto_populate.sort(key=lambda entry: entry[0])
        hour_of_day = time_of_day.hour()
        entry = auto_populate[-1]
        interval = AutoPopulateInterval(min=entry[1].lower_bound,
                                        max=entry[1].upper_bound)
        for entry in auto_populate:
            if entry[0] <= hour_of_day:
                interval = AutoPopulateInterval(min=entry[1].lower_bound,
                                                max=entry[1].upper_bound)
            else:
                break
        return interval
Example #18
0
 def __init__(self, minimum_range=0, **kwargs):
     super().__init__(
         range=TunableInterval(
             description=
             '\n                    Range to return true.\n                    ',
             tunable_type=Tunable100ConvertRange,
             minimum=minimum_range,
             maximum=100,
             default_lower=0,
             default_upper=100),
         zero_is_true=Tunable(
             description=
             '\n                    If checked, will return True if amount is 0.\n                    If unchecked, will return False if amount is 0.\n                    \n                    Even if inside (or outside) specified range.\n                    ',
             tunable_type=bool,
             default=False),
         **kwargs)
class BroadcasterEffectSelfBuff(_BroadcasterEffectTested):
    FACTORY_TUNABLES = {
        'broadcastee_count_interval':
        TunableInterval(
            description=
            '\n            If the number of objects within this broadcaster is in this\n            interval, the buff will be applied. Includes lower and upper.\n            ',
            tunable_type=int,
            default_lower=1,
            default_upper=2,
            minimum=1,
            maximum=20),
        'buff':
        buffs.tunable.TunableBuffReference(
            description='\n            The buff to apply\n            ')
    }

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

    @classproperty
    def apply_when_linked(cls):
        return True

    def _count_is_within_interval(self, broadcaster):
        object_count = broadcaster.get_affected_object_count()
        return object_count in self.broadcastee_count_interval

    def _on_object_number_changed(self, broadcaster):
        if self._count_is_within_interval(broadcaster):
            if broadcaster not in self._buff_handles:
                broadcasting_object = broadcaster.broadcasting_object
                self._buff_handles[broadcaster] = broadcasting_object.add_buff(
                    self.buff.buff_type, buff_reason=self.buff.buff_reason)
        elif broadcaster in self._buff_handles:
            broadcasting_object = broadcaster.broadcasting_object
            if broadcasting_object is not None:
                broadcasting_object.remove_buff(
                    self._buff_handles[broadcaster])
            del self._buff_handles[broadcaster]

    def _apply_broadcaster_effect(self, broadcaster, affected_object):
        self._on_object_number_changed(broadcaster)

    def remove_broadcaster_effect(self, broadcaster, affected_object):
        self._on_object_number_changed(broadcaster)
Example #20
0
class StoryProgressionDemographicEmployment(
        _StoryProgressionDemographicWithFilter):
    FACTORY_TUNABLES = {
        'employment_rate':
        TunableInterval(
            description=
            '\n            The ideal employment rate. If the rate of employed Sims falls\n            outside of this interval, Sims will be hired/fired as necessary.\n            ',
            tunable_type=float,
            default_lower=0.6,
            default_upper=0.9,
            minimum=0,
            maximum=1)
    }

    def __init__(self, *args, **kwargs):
        self._workforce_count = 0
        self._employed_count = 0
        self._unemployed_count = 0
        super().__init__(*args, **kwargs)

    def get_demographic_error(self):
        employment_ratio = self._employed_count / self._workforce_count if self._workforce_count else 0
        if employment_ratio < self.employment_rate.lower_bound:
            return self.employment_rate.lower_bound - employment_ratio
        elif employment_ratio > self.employment_rate.upper_bound:
            return employment_ratio - self.employment_rate.upper_bound
        return 0

    def _add_sim_info_agent_internal(self, sim_info_agent):
        work_career = sim_info_agent.get_work_career()
        if work_career is None:
            self._unemployed_count += 1
        elif work_career.can_quit:
            self._employed_count += 1
        self._workforce_count += 1

    def _remove_sim_info_agent_internal(self, sim_info_agent):
        work_career = sim_info_agent.get_work_career()
        if work_career is None:
            self._unemployed_count -= 1
        elif work_career.can_quit:
            self._employed_count -= 1
        self._workforce_count -= 1
Example #21
0
class PoolSizeTest(HasTunableSingletonFactory, AutoFactoryInit,
                   test_base.BaseTest):
    FACTORY_TUNABLES = {
        'target':
        TunableEnumEntry(
            description='\n            The target of the test.\n            ',
            tunable_type=ParticipantType,
            default=ParticipantType.Object),
        'allowable_size':
        TunableInterval(
            description=
            '\n            The range (inclusive min, exclusive max) of pool sizes for which \n            this test will pass. Pool size is measured in half tiles.\n            ',
            tunable_type=float,
            default_lower=0,
            default_upper=0)
    }

    def get_expected_args(self):
        return {'targets': self.target}

    def __call__(self, targets):
        for target in targets:
            pool_size = build_buy.get_pool_size_at_location(
                target.location.world_transform.translation, target.level)
            if pool_size is None:
                if 0.0 < terrain.get_water_depth_at_location(target.location):
                    return TestResult.TRUE
                return TestResult(
                    False, 'PoolSizeTest: Target is not a pool or ocean')
            min_size = self.allowable_size.lower_bound
            max_size = self.allowable_size.upper_bound
            if not pool_size < min_size:
                if pool_size >= max_size:
                    return TestResult(
                        False,
                        f'PoolSizeTest: A pool size of {pool_size} is not within the allowable range of {min_size} to {max_size}'
                    )
            return TestResult(
                False,
                f'PoolSizeTest: A pool size of {pool_size} is not within the allowable range of {min_size} to {max_size}'
            )
        return TestResult.TRUE
Example #22
0
class OrgMemberJobAndRoles(HasTunableSingletonFactory, AutoFactoryInit):
    FACTORY_TUNABLES = {
        'org_member_jobs_and_roles':
        TunableMapping(
            description=
            "\n            A mapping between a situation's jobs and default role states.\n            ",
            key_type=TunableReference(
                description=
                '\n                A job created for this situation.\n                ',
                manager=services.situation_job_manager()),
            key_name='Member Situation Job',
            value_type=TunableTuple(
                role=TunableReference(
                    description=
                    '\n                    The role state that the sim of this job starts the situation with.\n                    ',
                    manager=services.get_instance_manager(
                        sims4.resources.Types.
                        ROLE_STATE)),
                organization=TunableReference(
                    description=
                    "\n                    The membership list of this organization fills in the situation's\n                    jobs.\n                    ",
                    manager=services.get_instance_manager(
                        sims4.resources.Types.SNIPPET),
                    class_restrictions='Organization',
                    tuning_group=GroupNames.SITUATION),
                number_of_members=TunableInterval(
                    description=
                    '\n                    The interval defines the range of number of members that need to \n                    fill in the situation job.\n                    ',
                    tunable_type=int,
                    default_lower=2,
                    default_upper=3,
                    minimum=1,
                    tuning_group=GroupNames.SITUATION),
                additional_filters=TunableList(tunable=FilterTermVariant(
                    description=
                    '\n                    Additional filters to be applied to the members request.\n                    \n                    If the existing members pool does not include sims that pass these\n                    filters, the org service will attempt to populate the list with\n                    more members that satisfy these filters.\n                    '
                ),
                                               tuning_group=GroupNames.
                                               SITUATION)),
            value_name='Member Role State and Organization Info')
    }
Example #23
0
 def __init__(self, **kwargs):
     super().__init__(
         level_range=OptionalTunable(
             TunableInterval(
                 description=
                 "\n                            Interval is used to clamp the sim's user facing\n                            skill level to determine how many point to give. If\n                            disabled, level passed to the dynamic skill loot\n                            will always be the current user facing skill level\n                            of sim. \n                            Example: if sim is level 7 in fitness but\n                            interaction skill level is only for 1 to 5 give the\n                            dynamic skill amount as if sim is level 5.\n                            ",
                 tunable_type=int,
                 default_lower=0,
                 default_upper=1,
                 minimum=0)),
         stat=TunableReference(
             description=
             '\n                             The statistic we are operating on.\n                             ',
             manager=services.get_instance_manager(
                 sims4.resources.Types.STATISTIC),
             class_restrictions=Skill),
         effectiveness=TunableEnumEntry(
             description=
             '\n                             Enum to determine which curve to use when giving\n                             points to sim.\n                             ',
             tunable_type=SkillEffectiveness,
             needs_tuning=True,
             default=None),
         **kwargs)
Example #24
0
class ContentSetWithOverrides(ContentSet):
    FACTORY_TUNABLES = {
        'balloon_overrides':
        OptionalTunable(
            TunableList(
                description=
                '\n            Balloon Overrides lets you override the mixer balloons.\n            EX: Each of the comedy routine performances have a set of balloons.\n            However, the animation/mixer content is the same. We want to play\n            the same mixer content, but just have the balloons be different.\n            ',
                tunable=TunableBalloon())),
        'additional_mixers_to_cache':
        TunableInterval(
            description=
            "\n            Additional number of mixers to cache during a subaction request. For\n            mixer autonomy, we cache mixer for performance reasons. The baseline\n            cache size is determined by the mixer_interaction_cache_size tunable\n            on the Sim's autonomy component.\n            \n            An example for reason to add more mixers to cache if there are\n            large number of mixers tuned in this content set such as socials,\n            you may need to increase this number.  \n            \n            Please talk to GPE if you are about to add additional mixers.\n            ",
            tunable_type=int,
            minimum=0,
            default_lower=0,
            default_upper=0)
    }

    def __init__(self, balloon_overrides, additional_mixers_to_cache, *args,
                 **kwargs):
        super().__init__(*args, **kwargs)
        self.balloon_overrides = balloon_overrides
        self.additional_mixers_to_cache = additional_mixers_to_cache
class MixerInteraction(Interaction):
    INSTANCE_TUNABLES = {
        'display_name_target':
        TunableLocalizedStringFactory(
            description=
            "\n                Display text of target of mixer interaction. Example: Sim A\n                queues 'Tell Joke', Sim B will see in their queue 'Be Told\n                Joke'\n                ",
            allow_none=True,
            tuning_group=GroupNames.UI),
        'sub_action':
        TunableTuple(
            description=
            "\n                Sub-Action scoring: base_weight is the base autonomy weight for\n                mixer interaction.\n                \n                If mixer is NOT from social super interaction, following formula is applied:\n                Formula: autonomy weight = base_weight * StaticCommodity.desires multiplied together\n                    autonomy_weight = cls.sub_action.base_weight\n                    for static_commodity_data in cls.static_commodities_data:\n                        if sim.get_stat_instance(static_commodity_data.static_commodity):\n                    autonomy_weight *= static_commodity_data.desire\n                \n                If mixer is from a social super interaction, following formula\n                is applied to get the autonomy weight.\n                Formula: autonomy weight = autonomy_weight(from above) * SubactionAutonomyContentScoreUtilityCurve(front_page_score).y\n                \n                SubactionAutonomyContentScoreUtilityCurve maps from\n                front_page_score to a score between 0-100\n                \n                if test_gender_preference and fails,\n                   front_page_base_score = SocialMixerInteraction.GENDER_PREF_CONTENT_SCORE_PENALTY\n                otherwise:\n                   fornt_page_base_score = (socialmixerinteraction.base_score) +\n                                   shot term preference score that satisify group +\n                                   mood preference score that can apply to sim + \n                                   sum of buff preference that can apply to sim +\n                                   sum of trait preference that can apply to sim +\n                                   sum of relationship bit preference that apply to sim +\n                \n                front_page_score = fornt_page_base_score + \n                                   sum of topic preference that can apply to sim to target +\n                                   sum of sim's buffs game modifier that can apply to mixer affordance + \n                                   sum of club front page bonus for mixer that can apply for sim and mixer affordance + \n                                   front page cooldown score if tuned and can be applied\n\n                If super interaction from mixer will cause sim to change posture following multiplier is applied\n                front_page_score = front_page_socre * ContentSetTuning.POSTURE_PENALTY_MULTIPLIER.\n                ",
            base_weight=TunableRange(
                description=
                '\n                    The base weight of the subaction.\n                    ',
                tunable_type=int,
                minimum=0,
                default=1),
            mixer_group=TunableEnumEntry(
                description=
                '\n                    The group this mixer belongs to.  This will directly affect\n                    the scoring of subaction autonomy.  When subaction autonomy\n                    runs and chooses the mixer provider for the sim to express,\n                    the sim will gather all mixers for that provider.  She will\n                    then choose one of the categories based on a weighted\n                    random, then score the mixers only in that group.  The\n                    weights are tuned in autonomy_modes with the\n                    SUBACTION_GROUP_WEIGHTING tunable mapping.\n                    \n                    Example: Say you have two groups: DEFAULT and IDLES.  You\n                    could set up the SUBACTION_GROUP_WEIGHTING mapping such\n                    that DEFAULT has a weight of 3 and IDLES has a weight of 7.\n                    When a sim needs to decide which set of mixers to pull\n                    from, 70% of the time she will choose mixers tagged with\n                    IDLES and 30% of the time she will choose mixers tagged\n                    with DEFAULT.\n                    ',
                tunable_type=interactions.MixerInteractionGroup,
                needs_tuning=True,
                default=interactions.MixerInteractionGroup.DEFAULT),
            tuning_group=GroupNames.AUTONOMY),
        'optional':
        Tunable(
            description=
            "\n                Most mixers are expected to always be valid.  Thus this should\n                be False. When setting to True, we will test this mixer for\n                compatibility with the current SIs the sim is in. This can be\n                used to ensure general tuning for things like socials can all\n                always be there, but a couple socials that won't work with the\n                treadmill will be tested out such that the player cannot choose\n                them.\n                ",
            tunable_type=bool,
            default=False,
            tuning_group=GroupNames.MIXER),
        'lock_out_time':
        OptionalTunable(
            description=
            '\n                Enable to prevent this mixer from being run repeatedly.\n                ',
            tunable=TunableTuple(
                interval=TunableInterval(
                    description=
                    '\n                        Time in sim minutes in which this affordance will not\n                        be valid for.\n                        ',
                    tunable_type=TunableSimMinute,
                    default_lower=1,
                    default_upper=1,
                    minimum=0),
                target_based_lock_out=Tunable(
                    bool,
                    False,
                    description=
                    '\n                        If True, this lock out time will be enabled on a per\n                        Sim basis. i.e. locking it out on Sim A will leave it\n                        available to Sim B.\n                        '
                )),
            tuning_group=GroupNames.MIXER),
        'lock_out_time_initial':
        OptionalTunable(
            description=
            '\n                Enable to prevent this mixer from being run immediately.\n                ',
            tunable=TunableInterval(
                description=
                '\n                    Time in sim minutes to delay before running this mixer for\n                    the first time.\n                    ',
                tunable_type=TunableSimMinute,
                default_lower=1,
                default_upper=1,
                minimum=0),
            tuning_group=GroupNames.MIXER),
        'lock_out_affordances':
        OptionalTunable(TunableList(
            description=
            '\n                Additional affordances that will be locked out if lock out time\n                has been set.\n                ',
            tunable=TunableReference(
                manager=services.get_instance_manager(
                    sims4.resources.Types.INTERACTION),
                class_restrictions=('MixerInteraction', ))),
                        tuning_group=GroupNames.MIXER),
        '_interruptible':
        OptionalTunable(
            description=
            '\n                If disabled, this Mixer Interaction will be interruptible if\n                the content is looping, and not if the content is one shot.  To\n                override this behavior, enable this tunable and set the bool.\n                ',
            tunable=Tunable(
                description=
                '\n                    This interaction represents idle-style behavior and can\n                    immediately be interrupted by more important interactions.\n                    Set this to True for passive, invisible mixer interactions\n                    like stand_Passive.\n                    ',
                tunable_type=bool,
                default=False),
            tuning_group=GroupNames.MIXER),
        'skip_safe_tests_on_execute':
        Tunable(
            description=
            '\n            Most mixers should skip safe tests on execute, and this should be\n            set to True. When set to False, we will reevaluate the result of\n            safe to skip tests when executing this mixer. This should be used\n            when mixers can be queued at the same time, but running one mixer\n            changes the result of a test of the following mixer such that it\n            is no longer valid to run.\n            ',
            tunable_type=bool,
            default=True,
            tuning_group=GroupNames.MIXER),
        'outcome':
        TunableOutcome(tuning_group=GroupNames.CORE)
    }

    def __init__(self,
                 target,
                 context,
                 *args,
                 push_super_on_prepare=False,
                 **kwargs):
        super().__init__(target, context, *args, **kwargs)
        self._target_sim_refs_to_remove_interaction = None
        self._push_super_on_prepare = push_super_on_prepare
        self.duration = None
        self.push_super_affordance_target = None

    def get_animation_context_liability(self):
        if self.super_interaction is not None:
            animation_liability = self.super_interaction.get_animation_context_liability(
            )
            return animation_liability
        raise RuntimeError(
            'Mixer Interaction {} has no associated Super Interaction. [tastle]'
            .format(self))

    @property
    def animation_context(self):
        animation_liability = self.get_animation_context_liability()
        return animation_liability.animation_context

    def register_additional_event_handlers(self, animation_context):
        if self.super_interaction is not None:
            self.super_interaction.register_additional_event_handlers(
                animation_context)
        else:
            raise RuntimeError(
                'Mixer Interaction {} has no associated Super Interaction. [tastle]'
                .format(self))

    def store_event_handler(self, callback, handler_id=None):
        if self.super_interaction is not None:
            self.super_interaction.store_event_handler(callback,
                                                       handler_id=handler_id)
        else:
            raise RuntimeError(
                'Mixer Interaction {} has no associated Super Interaction. [tastle]'
                .format(self))

    @property
    def carry_target(self):
        carry_target = super().carry_target
        if carry_target is None:
            if self.super_interaction is not None:
                carry_target = self.super_interaction.carry_target
        return carry_target

    @flexmethod
    def skip_test_on_execute(cls, inst):
        inst_or_cls = inst if inst is not None else cls
        return inst_or_cls.skip_safe_tests_on_execute

    @flexproperty
    def stat_from_skill_loot_data(cls, inst):
        if inst is None or cls.skill_loot_data.stat is not None:
            return cls.skill_loot_data.stat
        elif inst.super_interaction is not None:
            return inst.super_interaction.stat_from_skill_loot_data

    @flexproperty
    def skill_effectiveness_from_skill_loot_data(cls, inst):
        if inst is None or cls.skill_loot_data.effectiveness is not None:
            return cls.skill_loot_data.effectiveness
        elif inst.super_interaction is not None:
            return inst.super_interaction.skill_effectiveness_from_skill_loot_data

    @flexproperty
    def level_range_from_skill_loot_data(cls, inst):
        if inst is None or cls.skill_loot_data.level_range is not None:
            return cls.skill_loot_data.level_range
        elif inst.super_interaction is not None:
            return inst.super_interaction.level_range_from_skill_loot_data

    @classmethod
    def _test(cls, target, context, **kwargs):
        if cls.optional and not cls.is_mixer_compatible(
                context.sim, target, participant_type=ParticipantType.Actor):
            return TestResult(
                False,
                'Optional MixerInteraction ({}) was not compatible with current posture ({})',
                cls, context.sim.posture_state)
        return super()._test(target, context, **kwargs)

    @classmethod
    def potential_interactions(cls, target, sa, si, **kwargs):
        yield AffordanceObjectPair(cls, target, sa, si, **kwargs)

    @classmethod
    def filter_mixer_targets(cls,
                             super_interaction,
                             potential_targets,
                             actor,
                             affordance=None):
        if cls.target_type & TargetType.ACTOR:
            targets = (None, )
        elif cls.target_type & TargetType.TARGET or cls.target_type & TargetType.OBJECT:
            targets = [
                x for x in potential_targets if x is not actor
                if not actor.is_sub_action_locked_out(affordance, target=x)
                if x.supports_affordance(cls)
            ]
        elif cls.target_type & TargetType.GROUP:
            targets = [x for x in potential_targets if x if not x.is_sim]
            if not targets:
                targets = (None, )
        else:
            targets = (None, )
        return targets

    @classmethod
    def calculate_autonomy_weight(cls, sim):
        final_weight = cls.sub_action.base_weight
        for static_commodity_data in cls.static_commodities_data:
            if sim.get_stat_instance(static_commodity_data.static_commodity):
                final_weight *= static_commodity_data.desire
        return final_weight

    @classproperty
    def interruptible(cls):
        if cls._interruptible is not None:
            return cls._interruptible
        return False

    @classproperty
    def involves_carry(cls):
        return False

    @classmethod
    def get_mixer_key_override(cls, target):
        pass

    def should_cancel_on_si_cancel(self, interaction):
        if self.interruptible:
            return True
        elif self.super_interaction is interaction:
            return self.looping
        return False

    def should_insert_in_queue_on_append(self):
        if self.super_interaction is not None:
            return True
        return False

    def _must_push_super_interaction(self):
        if not self._push_super_on_prepare or self.super_interaction is not None:
            return False
        for interaction in self.sim.running_interactions_gen(
                self.super_affordance):
            if interaction.is_finishing:
                continue
            if not self.target is interaction.target:
                if self.target in interaction.get_potential_mixer_targets():
                    self.super_interaction = interaction
                    self.sim.ui_manager.set_interaction_super_interaction(
                        self, self.super_interaction.id)
                    return False
            self.super_interaction = interaction
            self.sim.ui_manager.set_interaction_super_interaction(
                self, self.super_interaction.id)
            return False
        return True

    def notify_queue_head(self):
        if self.is_finishing:
            return
        super().notify_queue_head()
        if self._must_push_super_interaction():
            self._push_super_on_prepare = False
            context = InteractionContext(
                self.sim,
                self.source,
                self.priority,
                insert_strategy=QueueInsertStrategy.FIRST,
                bucket=self.context.bucket,
                preferred_objects=self.context.preferred_objects)
            interaction_parameters = dict()
            for interaction_parameter_key in ('picked_item_ids',
                                              'associated_club'):
                if interaction_parameter_key in self.interaction_parameters:
                    interaction_parameters[
                        interaction_parameter_key] = self.interaction_parameters[
                            interaction_parameter_key]
            if self.is_social:
                picked_object = self.picked_object
            else:
                picked_object = None
            result = self.sim.push_super_affordance(
                self.super_affordance,
                self.target or self.push_super_affordance_target,
                context,
                picked_object=picked_object,
                **interaction_parameters)
            if result:
                self.super_interaction = result.interaction
                guaranteed_lock_liability = LockGuaranteedOnSIWhileRunning(
                    self.super_interaction)
                self.add_liability(LOCK_GUARANTEED_ON_SI_WHILE_RUNNING,
                                   guaranteed_lock_liability)
                self.sim.ui_manager.set_interaction_super_interaction(
                    self, self.super_interaction.id)
            else:
                self.cancel(
                    FinishingType.KILLED,
                    'Failed to push the SI associated with this mixer!')

    def prepare_gen(self, timeline):
        if not self.allow_with_unholsterable_carries and not self.cancel_incompatible_carry_interactions(
                can_defer_putdown=False):
            return InteractionQueuePreparationStatus.NEEDS_DERAIL
            yield
        return InteractionQueuePreparationStatus.SUCCESS
        yield

    def _get_required_sims(self, *args, **kwargs):
        sims = set()
        if self.target_type & TargetType.GROUP:
            sims.update(
                self.get_participants(ParticipantType.AllSims,
                                      listener_filtering_enabled=True))
        elif self.target_type & TargetType.TARGET:
            sims.update(self.get_participants(ParticipantType.Actor))
            sims.update(self.get_participants(ParticipantType.TargetSim))
        elif self.target_type & TargetType.ACTOR or self.target_type & TargetType.OBJECT:
            sims.update(self.get_participants(ParticipantType.Actor))
        return sims

    def get_asm(self, *args, **kwargs):
        if self.super_interaction is not None:
            return self.super_interaction.get_asm(*args, **kwargs)
        return super().get_asm(*args, **kwargs)

    def on_added_to_queue(self, *args, **kwargs):
        super().on_added_to_queue(*args, **kwargs)
        if self._aop:
            self._aop.lifetime_in_steps = 0

    def build_basic_elements(self, sequence=()):
        sequence = super().build_basic_elements(sequence=sequence)
        for sim in self.required_sims():
            for social_group in sim.get_groups_for_sim_gen():
                sequence = social_group.with_social_focus(
                    self.sim, social_group._group_leader, (sim, ), sequence)
        suspended_modifiers_dict = self._generate_suspended_modifiers_dict()
        if gsi_handlers.interaction_archive_handlers.is_archive_enabled(self):
            start_time = services.time_service().sim_now
        else:
            start_time = None

        def interaction_start(_):
            self._suspend_modifiers(suspended_modifiers_dict)
            self.apply_interaction_cost()
            performance.counters.add_counter('PerfNumSubInteractions', 1)
            self._add_interaction_to_targets()
            if gsi_handlers.interaction_archive_handlers.is_archive_enabled(
                    self):
                gsi_handlers.interaction_archive_handlers.archive_interaction(
                    self.sim, self, 'Start')

        def interaction_end(_):
            if start_time is not None:
                self.duration = (services.time_service().sim_now -
                                 start_time).in_minutes()
            self._remove_interaction_from_targets()
            self.sim.update_last_used_interaction(self)
            self._resume_modifiers(suspended_modifiers_dict)

        return build_critical_section_with_finally(interaction_start, sequence,
                                                   interaction_end)

    def _generate_suspended_modifiers_dict(self):
        suspended_modifiers_dict = {}
        for sim in self.required_sims():
            for (handle, autonomy_modifier_entry
                 ) in sim.sim_info.get_statistic_modifiers_gen():
                autonomy_modifier = autonomy_modifier_entry.autonomy_modifier
                if autonomy_modifier.exclusive_si and autonomy_modifier.exclusive_si is not self.super_interaction:
                    if sim.sim_info not in suspended_modifiers_dict:
                        suspended_modifiers_dict[sim.sim_info] = []
                    suspended_modifiers_dict[sim.sim_info].append(
                        (autonomy_modifier.exclusive_si, handle))
        return suspended_modifiers_dict

    def _suspend_modifiers(self, modifiers_dict):
        for (sim_info, handle_list) in modifiers_dict.items():
            for (si, handle) in handle_list:
                (result, reason) = sim_info.suspend_statistic_modifier(handle)
                if not result and reason is not None:
                    logger.error(
                        'Failed to suspend modifier of exclusive si: {}\n   On Sim: {}\n   Running: {}\n   Reason: {}',
                        si,
                        sim_info,
                        self,
                        reason,
                        owner='msantander')

    def _resume_modifiers(self, modifiers_dict):
        for (sim_info, handle_list) in modifiers_dict.items():
            for (_, handle) in handle_list:
                sim_info.resume_statistic_modifier(handle)

    def apply_interaction_cost(self):
        pass

    def cancel(self, finishing_type, cancel_reason_msg, **kwargs):
        if hasattr(self.super_interaction, 'context_handle'):
            context_handle = self.super_interaction.context_handle
            ret = super().cancel(finishing_type, cancel_reason_msg, **kwargs)
            if ret:
                from server_commands import interaction_commands
                interaction_commands.send_reject_response(
                    self.sim.client, self.sim, context_handle, protocols.
                    ServerResponseFailed.REJECT_CLIENT_SELECT_MIXERINTERACTION)
            return ret
        return super().cancel(finishing_type, cancel_reason_msg, **kwargs)

    def cancel_parent_si_for_participant(self, participant_type,
                                         finishing_type, cancel_reason_msg,
                                         **kwargs):
        self.super_interaction.cancel(finishing_type, cancel_reason_msg,
                                      **kwargs)

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

    def _pre_perform(self):
        result = super()._pre_perform()
        if self.is_user_directed:
            self._update_autonomy_timer()
        return result

    @flexmethod
    def is_mixer_compatible(cls,
                            inst,
                            sim,
                            target,
                            error_on_fail=False,
                            participant_type=DEFAULT):
        posture_state = sim.posture_state
        inst_or_cls = inst if inst is not None else cls
        si = inst.super_interaction if inst is not None else None
        mixer_constraint_tentative = inst_or_cls.constraint_intersection(
            sim=sim,
            target=target,
            posture_state=None,
            participant_type=participant_type)
        with posture_manifest.ignoring_carry():
            mixer_constraint = mixer_constraint_tentative.apply_posture_state(
                posture_state,
                inst_or_cls.get_constraint_resolver(posture_state,
                                                    sim=sim,
                                                    target=target))
            posture_state_constraint = posture_state.constraint_intersection
            no_geometry_posture_state = posture_state_constraint.generate_alternate_geometry_constraint(
                None)
            no_geometry_mixer_state = mixer_constraint.generate_alternate_geometry_constraint(
                None)
            test_intersection = no_geometry_posture_state.intersect(
                no_geometry_mixer_state)
            ret = test_intersection.valid
        if not ret and error_on_fail and no_geometry_posture_state.valid:
            si_constraint_list = ''.join('\n        ' + str(c)
                                         for c in no_geometry_posture_state)
            mi_constraint_list = ''.join('\n        ' + str(c)
                                         for c in mixer_constraint_tentative)
            mx_constraint_list = ''.join('\n        ' + str(c)
                                         for c in no_geometry_mixer_state)
            to_constraint_list = ''.join('\n        ' + str(c)
                                         for c in test_intersection)
            logger.error(
                "{} more restrictive than {}!\n                The mixer interaction's constraint is more restrictive than its\n                Super Interaction. Since this mixer is not tuned to be optional,\n                this is a tuning or animation error as the interaction's\n                animation may not play correctly or at all. \n                \n                If it is okay for this mixer to only be available part of the\n                time, set Optional to True.\n\n                SI constraints No Geometry: \t{} \n\n                Effective Mixer constraints No Geometry: \t{} \n\n                Original Mixer constraints: \t{} \n\n                Total constraints: \t{}\n                ",
                type(inst_or_cls).__name__,
                type(si).__name__,
                si_constraint_list,
                mx_constraint_list,
                mi_constraint_list,
                to_constraint_list,
                trigger_breakpoint=True)
        return ret

    def _validate_posture_state(self):
        for sim in self.required_sims():
            participant_type = self.get_participant_type(sim)
            if participant_type is None:
                continue
            constraint_tentative = self.constraint_intersection(
                sim=sim, participant_type=participant_type)
            resolver = self.get_constraint_resolver(
                sim.posture_state, participant_type=participant_type)
            constraint = constraint_tentative.apply_posture_state(
                sim.posture_state, resolver)
            sim_transform_constraint = interactions.constraints.Transform(
                sim.transform, routing_surface=sim.routing_surface)
            geometry_intersection = constraint.intersect(
                sim_transform_constraint)
            if not geometry_intersection.valid:
                containment_transform = None
                if isinstance(constraint, RequiredSlotSingle):
                    containment_transform = constraint.containment_transform.translation
                    if sims4.math.vector3_almost_equal_2d(
                            sim.transform.translation,
                            containment_transform,
                            epsilon=ANIMATION_SLOT_EPSILON):
                        continue
                else:
                    logger.error(
                        "Interaction Constraint Error: Interaction's constraint is incompatible with the Sim's current position \n                    Interaction: {}\n                    Sim: {}, \n                    Constraint: {}\n                    Sim Position: {}\n                    Interaction Target Position: {},\n                    Target Containment Transform: {}",
                        self,
                        sim,
                        constraint,
                        sim.position,
                        self.target.position
                        if self.target is not None else None,
                        containment_transform,
                        owner='MaxR',
                        trigger_breakpoint=True)
                    return False
        return True

    def pre_process_interaction(self):
        self.sim.ui_manager.transferred_to_si_state(self)

    def post_process_interaction(self):
        self.sim.ui_manager.remove_from_si_state(self)

    def perform_gen(self, timeline):
        with gsi_handlers.sim_timeline_handlers.archive_sim_timeline_context_manager(
                self.sim, 'Mixer', 'Perform', self):
            result = yield from super().perform_gen(timeline)
            return result
            yield

    def _add_interaction_to_targets(self):
        if not self.visible_as_interaction:
            return
        social_group = self.social_group
        if social_group is not None:
            icon_info = self.get_icon_info()
            if icon_info.icon_resource is None:
                if icon_info.obj_instance is not None:
                    icon_info._replace(obj_instance=self.sim)
                else:
                    icon_info.obj_instance = self.sim
            for target_sim in self.required_sims():
                if target_sim == self.sim:
                    continue
                target_si = social_group.get_si_registered_for_sim(target_sim)
                if target_si is None:
                    continue
                name = self.display_name_target(target_sim, self.sim)
                target_sim.ui_manager.add_running_mixer_interaction(
                    target_si.id, self, icon_info, name)
                if gsi_handlers.interaction_archive_handlers.is_archive_enabled(
                        self):
                    gsi_handlers.interaction_archive_handlers.archive_interaction(
                        target_sim, self, 'Start')
                if self._target_sim_refs_to_remove_interaction is None:
                    self._target_sim_refs_to_remove_interaction = weakref.WeakSet(
                    )
                self._target_sim_refs_to_remove_interaction.add(target_sim)

    def _remove_interaction_from_targets(self):
        if self._target_sim_refs_to_remove_interaction:
            for target_sim in self._target_sim_refs_to_remove_interaction:
                target_sim.ui_manager.remove_from_si_state(self)
                if gsi_handlers.interaction_archive_handlers.is_archive_enabled(
                        self):
                    gsi_handlers.interaction_archive_handlers.archive_interaction(
                        target_sim, self, 'Complete')
class RetailCustomerSituation(BusinessSituationMixin, SituationComplexCommon):
    INSTANCE_TUNABLES = {
        'customer_job':
        SituationJob.TunableReference(
            description=
            '\n            The situation job for the customer.\n            '),
        'role_state_go_to_store':
        RoleState.TunableReference(
            description=
            '\n            The role state for getting the customer inside the store. This is\n            the default role state and will be run first before any other role\n            state can start.\n            '
        ),
        'role_state_browse':
        OptionalTunable(
            description=
            '\n            If enabled, the customer will be able to browse items.\n            ',
            tunable=TunableTuple(
                role_state=RoleState.TunableReference(
                    description=
                    '\n                    The role state for the customer browsing items.\n                    '
                ),
                browse_time_min=TunableSimMinute(
                    description=
                    '\n                    The minimum amount of time, in sim minutes, the customer\n                    will browse before moving on to the next state. When the\n                    customer begins browsing, a random time will be chosen\n                    between the min and max browse time.\n                    ',
                    default=10),
                browse_time_max=TunableSimMinute(
                    description=
                    '\n                    The maximum amount of time, in sim minutes, the customer\n                    will browse before moving on to the next state. When the\n                    customer begins browsing, a random time will be chosen\n                    between the min and max browse time.\n                    ',
                    default=20),
                browse_time_extension_tunables=OptionalTunable(
                    TunableTuple(
                        description=
                        '\n                    A set of tunables related to browse time extensions.\n                    ',
                        extension_perk=TunableReference(
                            description=
                            '\n                        Reference to a perk that, if unlocked, will increase\n                        browse time by a set amount.\n                        ',
                            manager=services.get_instance_manager(
                                sims4.resources.Types.BUCKS_PERK)),
                        time_extension=TunableSimMinute(
                            description=
                            '\n                        The amount of time, in Sim minutes, that browse time\n                        will be increased by if the specified "extension_perk"\n                        is unlocked.\n                        ',
                            default=30))))),
        'role_state_buy':
        OptionalTunable(
            description=
            '\n            If enabled, the customer will be able to buy items.\n            ',
            tunable=TunableTuple(
                role_state=RoleState.TunableReference(
                    description=
                    '\n                    The role state for the customer buying items.\n                    '
                ),
                price_range=TunableInterval(
                    description=
                    '\n                    The minimum and maximum price of items this customer will\n                    buy.\n                    ',
                    tunable_type=int,
                    default_lower=1,
                    default_upper=100,
                    minimum=1))),
        'role_state_loiter':
        RoleState.TunableReference(
            description=
            '\n            The role state for the customer loitering. If Buy Role State and\n            Browse Role State are both disabled, the Sim will fall back to\n            loitering until Total Shop Time runs out.\n            '
        ),
        'go_to_store_interaction':
        TunableInteractionOfInterest(
            description=
            '\n            The interaction that, when run by a customer, will switch the\n            situation state to start browsing, buying, or loitering.\n            '
        ),
        'total_shop_time_max':
        TunableSimMinute(
            description=
            "\n            The maximum amount of time, in sim minutes, a customer will shop.\n            This time starts when they enter the store. At the end of this\n            time, they'll finish up whatever their current interaction is and\n            leave.\n            ",
            default=30),
        'total_shop_time_min':
        TunableSimMinute(
            description=
            "\n            The minimum amount of time, in sim minutes, a customer will shop.\n            This time starts when they enter the store. At the end of this\n            time, they'll finish up whatever their current interaction is and\n            leave.\n            ",
            default=1),
        'buy_interaction':
        TunableInteractionOfInterest(
            description=
            '\n            The interaction that, when run by a customer, buys an object.\n            '
        ),
        'initial_purchase_intent':
        TunableInterval(
            description=
            "\n            The customer's purchase intent statistic is initialized to a random\n            value in this interval when they enter the store.\n            ",
            tunable_type=int,
            default_lower=0,
            default_upper=100),
        'purchase_intent_extension_tunables':
        OptionalTunable(
            TunableTuple(
                description=
                '\n            A set of tunables related to purchase intent extensions.\n            ',
                extension_perk=TunableReference(
                    description=
                    '\n                Reference to a perk that, if unlocked, will increase purchase\n                intent by a set amount.\n                ',
                    manager=services.get_instance_manager(
                        sims4.resources.Types.BUCKS_PERK)),
                purchase_intent_extension=TunableRange(
                    description=
                    '\n                The amount to increase the base purchase intent statistic by if\n                the specified "extension_perk" is unlocked.\n                ',
                    tunable_type=int,
                    default=5,
                    minimum=0,
                    maximum=100))),
        'purchase_intent_empty_notification':
        TunableUiDialogNotificationSnippet(
            description=
            '\n            Notification shown by customer when purchase intent hits bottom and\n            the customer leaves.\n            '
        ),
        'nothing_in_price_range_notification':
        TunableUiDialogNotificationSnippet(
            description=
            "\n            Notification shown by customers who are ready to buy but can't find\n            anything in their price range.\n            "
        ),
        '_situation_start_tests':
        TunableCustomerSituationInitiationSet(
            description=
            '\n            A set of tests that will be run when determining if this situation\n            can be chosen to start. \n            '
        )
    }
    CONTINUE_SHOPPING_THRESHOLD = TunableSimMinute(
        description=
        "\n        If the customer has this much time or more left in their total shop\n        time, they'll start the browse/buy process over again after purchasing\n        something. If they don't have this much time remaining, they'll quit\n        shopping.\n        ",
        default=30)
    PRICE_RANGE = TunableTuple(
        description=
        '\n        Statistics that are set to the min and max price range statistics.\n        These are automatically added to the customer in this situation and\n        will be updated accordingly.\n        \n        The stats should not be persisted -- the situation will readd them\n        on load.\n        ',
        min=Statistic.TunablePackSafeReference(),
        max=Statistic.TunablePackSafeReference())
    PURCHASE_INTENT_STATISTIC = Statistic.TunablePackSafeReference(
        description=
        "\n        A statistic added to customers that track their intent to purchase\n        something. At the minimum value they will leave, and at max value they\n        will immediately try to buy something. Somewhere in between, there's a\n        chance for them to not buy something when they go to the buy state.\n        "
    )
    PURCHASE_INTENT_CHANCE_CURVE = TunableCurve(
        description=
        '\n        A mapping of Purchase Intent Statistic value to the chance (0-1) that\n        the customer will buy something during the buy state.\n        ',
        x_axis_name='Purchase Intent',
        y_axis_name='Chance')
    REMOVE_INSTANCE_TUNABLES = Situation.NON_USER_FACING_REMOVE_INSTANCE_TUNABLES

    @classmethod
    def can_start_situation(cls, resolver):
        return cls._situation_start_tests.run_tests(resolver)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._customer = None
        self._showing_purchase_intent = False
        reader = self._seed.custom_init_params_reader
        if reader is None:
            self._saved_purchase_intent = None
        else:
            self._saved_purchase_intent = reader.read_int64(
                'purchase_intent', None)
        self._min_price_range_multiplier = 1
        self._max_price_range_multiplier = 1
        self._total_shop_time_multiplier = 1
        self._purchase_intent_watcher_handle = None

    def _save_custom_situation(self, writer):
        super()._save_custom_situation(writer)
        if self._customer is not None:
            purchase_intent = self._customer.get_stat_value(
                self.PURCHASE_INTENT_STATISTIC)
            writer.write_int64('purchase_intent', int(purchase_intent))

    @classmethod
    def _states(cls):
        return (SituationStateData(1, _GoToStoreState),
                SituationStateData(2, _BrowseState),
                SituationStateData(3, _BuyState),
                SituationStateData(4, _LoiterState))

    @classmethod
    def _get_tuned_job_and_default_role_state_tuples(cls):
        return [(cls.customer_job, cls.role_state_go_to_store)]

    @classmethod
    def default_job(cls):
        return cls.customer_job

    def start_situation(self):
        super().start_situation()
        self._change_state(_GoToStoreState())

    @classmethod
    def get_sims_expected_to_be_in_situation(cls):
        return 1

    @classproperty
    def situation_serialization_option(cls):
        return situations.situation_types.SituationSerializationOption.LOT

    def validate_customer(self, sim_info):
        if self._customer is None:
            return False
        return self._customer.sim_info is sim_info

    def _on_set_sim_job(self, sim, job_type):
        super()._on_set_sim_job(sim, job_type)
        self._customer = sim
        self._update_price_range_statistics()
        self._initialize_purchase_intent()

    def _on_remove_sim_from_situation(self, sim):
        sim_job = self.get_current_job_for_sim(sim)
        super()._on_remove_sim_from_situation(sim)
        self._remove_purchase_intent()
        self._customer = None
        services.get_zone_situation_manager().add_sim_to_auto_fill_blacklist(
            sim.id, sim_job)
        self._self_destruct()

    def _situation_timed_out(self, *args, **kwargs):
        if not isinstance(self._cur_state, _BuyState):
            super()._situation_timed_out(*args, **kwargs)

    def adjust_browse_time(self, multiplier):
        if type(self._cur_state) is _BrowseState:
            self._cur_state.adjust_timeout(multiplier)

    def adjust_total_shop_time(self, multiplier):
        if multiplier == 0:
            self._self_destruct()
        elif type(self._cur_state) is _GoToStoreState:
            self._total_shop_time_multiplier *= multiplier
        else:
            remaining_minutes = self._get_remaining_time_in_minutes()
            remaining_minutes *= multiplier
            self.change_duration(remaining_minutes)

    def adjust_price_range(self, min_multiplier=1, max_multiplier=1):
        if self.role_state_buy is None:
            return
        self._min_price_range_multiplier *= min_multiplier
        self._max_price_range_multiplier *= max_multiplier
        self._update_price_range_statistics()

    def _update_price_range_statistics(self):
        (min_price, max_price) = self._get_min_max_price_range()
        if self.PRICE_RANGE.min is not None:
            min_stat = self._customer.get_statistic(self.PRICE_RANGE.min)
            min_stat.set_value(min_price)
        if self.PRICE_RANGE.max is not None:
            max_stat = self._customer.get_statistic(self.PRICE_RANGE.max)
            max_stat.set_value(max_price)

    def _get_min_max_price_range(self):
        price_range = self.role_state_buy.price_range
        return (max(0, price_range.lower_bound *
                    self._min_price_range_multiplier),
                max(1, price_range.upper_bound *
                    self._max_price_range_multiplier))

    def _initialize_purchase_intent(self):
        if self.role_state_buy is None:
            return
        if self._saved_purchase_intent is None:
            purchase_intent = random.randint(
                self.initial_purchase_intent.lower_bound,
                self.initial_purchase_intent.upper_bound)
            if self.purchase_intent_extension_tunables is not None:
                active_household = services.active_household()
                if active_household is not None:
                    if active_household.bucks_tracker.is_perk_unlocked(
                            self.purchase_intent_extension_tunables.
                            extension_perk):
                        purchase_intent += self.purchase_intent_extension_tunables.purchase_intent_extension
            purchase_intent = sims4.math.clamp(
                self.PURCHASE_INTENT_STATISTIC.min_value + 1, purchase_intent,
                self.PURCHASE_INTENT_STATISTIC.max_value - 1)
        else:
            purchase_intent = self._saved_purchase_intent
        tracker = self._customer.get_tracker(self.PURCHASE_INTENT_STATISTIC)
        tracker.set_value(self.PURCHASE_INTENT_STATISTIC,
                          purchase_intent,
                          add=True)
        self._purchase_intent_watcher_handle = tracker.add_watcher(
            self._purchase_intent_watcher)
        if self._on_social_group_changed not in self._customer.on_social_group_changed:
            self._customer.on_social_group_changed.append(
                self._on_social_group_changed)

    def _remove_purchase_intent(self):
        if self._customer is not None:
            if self._purchase_intent_watcher_handle is not None:
                tracker = self._customer.get_tracker(
                    self.PURCHASE_INTENT_STATISTIC)
                tracker.remove_watcher(self._purchase_intent_watcher_handle)
                self._purchase_intent_watcher_handle = None
                tracker.remove_statistic(self.PURCHASE_INTENT_STATISTIC)
            if self._on_social_group_changed in self._customer.on_social_group_changed:
                self._customer.on_social_group_changed.remove(
                    self._on_social_group_changed)
            self._set_purchase_intent_visibility(False)

    def _on_social_group_changed(self, sim, group):
        if self._customer in group:
            if self._on_social_group_members_changed not in group.on_group_changed:
                group.on_group_changed.append(
                    self._on_social_group_members_changed)
        elif self._on_social_group_members_changed in group.on_group_changed:
            group.on_group_changed.remove(
                self._on_social_group_members_changed)

    def _on_social_group_members_changed(self, group):
        if self._customer is not None:
            employee_still_in_group = False
            business_manager = services.business_service(
            ).get_business_manager_for_zone()
            if self._customer in group:
                for sim in group:
                    if not business_manager.is_household_owner(
                            sim.household_id):
                        if business_manager.is_employee(sim.sim_info):
                            employee_still_in_group = True
                            break
                    employee_still_in_group = True
                    break
            if employee_still_in_group:
                self._set_purchase_intent_visibility(True)
            else:
                self._set_purchase_intent_visibility(False)

    def on_sim_reset(self, sim):
        super().on_sim_reset(sim)
        if isinstance(self._cur_state, _BuyState) and self._customer is sim:
            new_buy_state = _BuyState()
            new_buy_state.object_id = self._cur_state.object_id
            self._change_state(new_buy_state)

    def _set_purchase_intent_visibility(self, toggle):
        if self._showing_purchase_intent is not toggle and (
                not toggle or isinstance(self._cur_state, _BrowseState)):
            self._showing_purchase_intent = toggle
            stat = self._customer.get_statistic(self.PURCHASE_INTENT_STATISTIC,
                                                add=False)
            if stat is not None:
                value = stat.get_value()
                self._send_purchase_intent_message(stat.stat_type, value,
                                                   value, toggle)

    def _purchase_intent_watcher(self, stat_type, old_value, new_value):
        if stat_type is not self.PURCHASE_INTENT_STATISTIC:
            return
        self._send_purchase_intent_message(stat_type, old_value, new_value,
                                           self._showing_purchase_intent)
        if new_value == self.PURCHASE_INTENT_STATISTIC.max_value:
            self._on_purchase_intent_max()
        elif new_value == self.PURCHASE_INTENT_STATISTIC.min_value:
            self._on_purchase_intent_min()

    def _send_purchase_intent_message(self, stat_type, old_value, new_value,
                                      toggle):
        business_manager = services.business_service(
        ).get_business_manager_for_zone()
        if business_manager is not None and business_manager.is_owner_household_active:
            op = PurchaseIntentUpdate(
                self._customer.sim_id,
                stat_type.convert_to_normalized_value(old_value),
                stat_type.convert_to_normalized_value(new_value), toggle)
            distributor.system.Distributor.instance().add_op(
                self._customer, op)

    def _on_purchase_intent_max(self):
        if isinstance(self._cur_state, _BuyState):
            return
        if isinstance(self._cur_state, _GoToStoreState):
            self._set_shop_duration()
        self._change_state(_BuyState())

    def _on_purchase_intent_min(self):
        resolver = SingleSimResolver(self._customer)
        dialog = self.purchase_intent_empty_notification(
            self._customer, resolver)
        dialog.show_dialog()
        self._self_destruct()

    def _choose_starting_state(self):
        if self.role_state_browse is not None:
            return _BrowseState()
        if self.role_state_buy is not None:
            return _BuyState()
        return _LoiterState()

    def _choose_post_browse_state(self):
        if self._customer is None:
            return
        if self.role_state_buy is not None:
            stat = self._customer.get_statistic(self.PURCHASE_INTENT_STATISTIC,
                                                add=False)
            if stat is not None:
                value = stat.get_value()
                chance = self.PURCHASE_INTENT_CHANCE_CURVE.get(value)
                if random.random() > chance:
                    return _BrowseState()
            self._set_purchase_intent_visibility(False)
            return _BuyState()
        return _LoiterState()

    def _choose_post_buy_state(self):
        minutes_remaining = self._get_remaining_time_in_minutes()
        if minutes_remaining < self.CONTINUE_SHOPPING_THRESHOLD:
            return
        if self.role_state_browse is not None:
            return _BrowseState()
        return _LoiterState()

    def _set_shop_duration(self):
        shop_time = random.randint(self.total_shop_time_min,
                                   self.total_shop_time_max)
        shop_time *= self._total_shop_time_multiplier
        self.change_duration(shop_time)
class TunedContinuousStatistic(statistics.continuous_statistic.ContinuousStatistic):
    INSTANCE_SUBCLASSES_ONLY = True
    INSTANCE_TUNABLES = {'decay_rate': sims4.tuning.tunable.TunableRange(description='\n            The decay rate for this stat (per sim minute).\n            ', tunable_type=float, default=0.001, minimum=0.0, tuning_group=GroupNames.CORE), '_decay_rate_overrides': TunableList(description='\n            A list of decay rate overrides.  Whenever the value of the stat\n            falls into this range, the decay rate is overridden with the value\n            specified. This overrides the base decay, so all decay modifiers\n            will still apply. The ranges are inclusive on the lower bound and\n            exclusive on the upper bound.  Overlapping values are not allowed\n            and will behave in an undefined manner.\n            ', tunable=TunableTuple(description='\n                The interval/decay_override pair.\n                ', interval=TunableInterval(description='\n                    The range at which this override will apply.  It is inclusive\n                    on the lower bound and exclusive on the upper bound.\n                    ', tunable_type=float, default_lower=-100, default_upper=100), decay_override=Tunable(description='\n                    The value that the base decay will be overridden with.\n                    ', tunable_type=float, default=0.0)), tuning_group=GroupNames.CORE), 'delayed_decay_rate': OptionalTunable(description="\n            When enabled contains the tuning for delayed decay. Delayed decay\n            is decay that happens if the value of the commodity hasn't changed\n            in some time.\n            ", tunable=TunableTuple(description='\n                All of the tuning for delayed decay rate.\n                ', initial_delay=TunableSimMinute(description='\n                    Time, in sim minutes, before the warning that a decay will\n                    start will be shown.\n                    ', default=30, minimum=0), final_delay=TunableSimMinute(description='\n                    Tim, in sim minutes, after the warning is shown that the\n                    decay will actually begin.\n                    ', default=30, minimum=0), delayed_decay_rate=sims4.tuning.tunable.TunableRange(description='\n                    The decay rate for this stat that starts after a delayed\n                    amount of time where the value of the skill does not change.\n                    ', tunable_type=float, default=0.001, minimum=0.0), decay_warning=OptionalTunable(description='\n                    If enabled, the notification to show to warn the user that\n                    a specific statistic is about to start decaying.\n                    ', tunable=TunableUiDialogNotificationSnippet(), enabled_by_default=True), decay_rate_overrides=TunableList(description='\n                    A list of decay rate overrides.  Whenever the value of the \n                    stat falls into this range, the decay rate is overridden \n                    with the value specified. This overrides the base decay, \n                    so all decay modifiers will still apply. The ranges are \n                    inclusive on the lower bound and exclusive on the upper \n                    bound.  Overlapping values are not allowed and will behave \n                    in an undefined manner.\n                    ', tunable=TunableTuple(description='\n                        The interval/decay_override pair.\n                        ', interval=TunableInterval(description='\n                            The range at which this override will apply.  It is \n                            inclusive on the lower bound and exclusive on the \n                            upper bound.\n                            ', tunable_type=float, default_lower=-100, default_upper=100), decay_override=Tunable(description='\n                            The value that the base decay will be overridden with.\n                            ', tunable_type=float, default=0.0), initial_delay_override=TunableSimMinute(description='\n                            The override for how long, in Sim Minutes, the \n                            initial delay is before the warning is given about \n                            decay starting.\n                            ', default=30, minimum=0), final_delay_override=TunableSimMinute(description='\n                            The override for how long, in Sim Minutes, the \n                            final delay is before the actual decay begins, \n                            after displaying the warning.\n                            ', default=30, minimum=0))), npc_decay=Tunable(description="\n                    By default decay doesn't happen for NPC sims. If enabled\n                    this will turn on decay of this statistic for NPC sims.\n                    ", tunable_type=bool, default=False)), tuning_group=GroupNames.CORE), '_default_convergence_value': Tunable(description='\n            The value toward which the stat decays.\n            ', tunable_type=float, default=0.0, tuning_group=GroupNames.CORE), 'stat_asm_param': statistics.tunable.TunableStatAsmParam.TunableFactory(tuning_group=GroupNames.SPECIAL_CASES), 'min_value_tuning': Tunable(description='\n            The minimum value for this stat.\n            ', tunable_type=float, default=-100, tuning_group=GroupNames.CORE), 'max_value_tuning': Tunable(description='\n            The maximum value for this stat.\n            ', tunable_type=float, default=100, tuning_group=GroupNames.CORE), 'initial_value': Tunable(description='\n            The initial value for this stat.\n            ', tunable_type=float, default=0.0, tuning_group=GroupNames.CORE), 'persisted_tuning': Tunable(description="\n            Whether this statistic will persist when saving a Sim or an object.\n            For example, a Sims's SI score statistic should never persist.\n            ", tunable_type=bool, default=True, tuning_group=GroupNames.SPECIAL_CASES), 'communicable_by_interaction_tag': OptionalTunable(description='\n            List of Tag and loot pairs that will trigger if either the actor or\n            target of an interaction has this statistic to give the first loot\n            whose tag matches any tag on the interaction.\n            \n            So you could do one loot for high risk socials, (tagged as such) a\n            different loot for low risk socials (tagged as such) a third loot\n            for high risk object interactions (licking bowl, maybe?), and\n            fourth loot for low risk object interaction\n            "generically using an object".\n            ', tunable=TunableList(tunable=TunableTuple(tag=TunableEnumEntry(description='\n                        Tag on interaction required to apply this loot.\n                        ', tunable_type=Tag, default=Tag.INVALID), loot=TunableReference(description='\n                        The loot to give.\n                        ', manager=services.get_instance_manager(sims4.resources.Types.ACTION), class_restrictions=('LootActions',)))), tuning_group=GroupNames.SPECIAL_CASES), 'gallery_load_behavior': TunableEnumEntry(description="\n            When owner of commodity is loaded from the gallery, tune this to\n            determine if commodity should be loaded or not.\n            \n            DONT_LOAD = Don't load statistic when owner is coming from gallery\n            \n            LOAD_ONLY_FOR_OBJECT = Load only if statistic is being added to an\n            object.  If this statistic is tuned as a linked stat to a state,\n            make sure the state is also marked as gallery persisted. i.e.\n            Statistics like fish_freshness or gardening_groth. Switching on\n            this bit has performance implications when downloading a lot from\n            the gallery. Please discuss with a GPE when setting this tunable.\n    \n            LOAD_ONLY_FOR_SIM = Load only if statistic is being added to a sim.\n            LOAD_FOR_ALL = Always load commodity.  This has the same\n            ramifications as LOAD_ONLY_FOR_OBJECT if owner is an object.\n            ", tunable_type=GalleryLoadBehavior, default=GalleryLoadBehavior.LOAD_ONLY_FOR_SIM, tuning_group=GroupNames.SPECIAL_CASES)}

    @classmethod
    def _verify_tuning_callback(cls):
        if cls._decay_rate_overrides and cls.delayed_decay_rate is not None:
            logger.error('A Continous Statistic ({}) has tuned decay overrides \n            and tuned delayed decay rates. This is not supported. The override \n            will always be used and the delayed decay rate will never work. \n            Please choose one or the other or see a GPE if you really need this\n            to work for some reason. rfleig ', cls)

    def __init__(self, tracker, initial_value):
        super().__init__(tracker, initial_value)
        self._decay_override_calllback_handles = None
        if not self._tracker.suppress_callback_setup_during_load:
            self._create_new_override_callbacks()

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

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

    @sims4.utils.classproperty
    def best_value(cls):
        return cls.max_value

    def get_asm_param(self):
        return self.stat_asm_param.get_asm_param(self)

    @sims4.utils.classproperty
    def persisted(cls):
        return cls.persisted_tuning

    @sims4.utils.classproperty
    def persists_across_gallery_for_state(cls):
        if cls.gallery_load_behavior == GalleryLoadBehavior.LOAD_FOR_ALL or cls.gallery_load_behavior == GalleryLoadBehavior.LOAD_ONLY_FOR_OBJECT:
            return True
        return False

    @classmethod
    def _tuning_loaded_callback(cls):
        cls._decay_override_list = cls._initialize_decay_override_list(cls._decay_rate_overrides, cls.decay_rate)
        if cls.delayed_decay_rate:
            cls._delayed_decay_override_list = cls._initialize_decay_override_list(cls.delayed_decay_rate.decay_rate_overrides, cls.delayed_decay_rate.delayed_decay_rate, delay=True)

    @classmethod
    def _initialize_decay_override_list(cls, override_tuning, default_decay, delay=False):
        if not override_tuning:
            return ()
        if delay:
            decay_override_list = [_DecayOverrideNode(override_data.interval.lower_bound, override_data.interval.upper_bound, override_data.decay_override, override_data.initial_delay_override, override_data.final_delay_override) for override_data in override_tuning]
        else:
            decay_override_list = [_DecayOverrideNode(override_data.interval.lower_bound, override_data.interval.upper_bound, override_data.decay_override) for override_data in override_tuning]
        decay_override_list.sort(key=lambda node: node.lower_bound)
        final_decay_override_list = []
        last_lower_bound = cls.max_value + 1
        for node in reversed(decay_override_list):
            if last_lower_bound > node.upper_bound:
                default_node = _DecayOverrideNode(node.upper_bound, last_lower_bound, default_decay)
                final_decay_override_list.insert(0, default_node)
            elif last_lower_bound < node.upper_bound:
                logger.error('Tuning error: two nodes are overlapping in continuous statistic decay overrides: {}', cls)
                node.upper_bound = last_lower_bound
            final_decay_override_list.insert(0, node)
            last_lower_bound = node.lower_bound
        if final_decay_override_list and final_decay_override_list[0].lower_bound > cls.min_value:
            default_node = _DecayOverrideNode(cls.min_value, final_decay_override_list[0].lower_bound, default_decay)
            final_decay_override_list.insert(0, default_node)
        return tuple(final_decay_override_list)

    def fixup_callbacks_during_load(self):
        super().fixup_callbacks_during_load()
        self._remove_decay_override_callbacks()
        self._create_new_override_callbacks()

    def _add_decay_override_callbacks(self, override_data, callback):
        if not override_data:
            return
        self._decay_override_calllback_handles = []
        value = self.get_value()
        for override in override_data:
            if value >= override.lower_bound:
                if value < override.upper_bound:
                    threshold = Threshold(override.lower_bound, operator.lt)
                    self._decay_override_calllback_handles.append(self.create_and_add_callback_listener(threshold, callback))
                    threshold = Threshold(override.upper_bound, operator.ge)
                    self._decay_override_calllback_handles.append(self.create_and_add_callback_listener(threshold, callback))
                    break

    def _remove_decay_override_callbacks(self):
        if not self._decay_override_calllback_handles:
            return
        for callback_listener in self._decay_override_calllback_handles:
            self.remove_callback_listener(callback_listener)
        self._decay_override_calllback_handles.clear()

    def _on_decay_rate_override_changed(self, _):
        value = self.get_value()
        self._remove_decay_override_callbacks()
        for override in self._decay_override_list:
            if value >= override.lower_bound:
                if value < override.upper_bound:
                    self._decay_rate_override = override.decay_override
                    self._create_new_override_callbacks()
                    return
        logger.error('No node found for stat value of {} on {}', value, self)

    def _on_delayed_decay_rate_override_changed(self, _):
        value = self.get_value()
        self._remove_decay_override_callbacks()
        for override in self._delayed_decay_override_list:
            if value >= override.lower_bound:
                if value < override.upper_bound:
                    self._delayed_decay_rate_override = override.decay_override
                    self._initial_delay_override = override.initial_delay_override
                    self._final_delay_override = override.final_delay_override
                    self._create_new_override_callbacks()
                    return
        logger.error('No node found for stat value of {} on {}', value, self)

    def _create_new_override_callbacks(self):
        if self._decay_rate_overrides:
            self._add_decay_override_callbacks(self._decay_override_list, self._on_decay_rate_override_changed)
        if self.delayed_decay_rate is not None and self.delayed_decay_rate.decay_rate_overrides:
            self._add_decay_override_callbacks(self._delayed_decay_override_list, self._on_delayed_decay_rate_override_changed)
        self._update_callback_listeners(resort_list=False)
class _SicknessMatchingCritera(HasTunableSingletonFactory, AutoFactoryInit):

    @staticmethod
    def _verify_tunable_callback(instance_class, tunable_name, source, value):
        if value.tags is None and value.difficulty_range is None:
            logger.error('_SicknessMatchingCritera: {} has a sickness criteria {} that sets no criteria.', source, tunable_name)

    FACTORY_TUNABLES = {'tags': OptionalTunable(description='\n            Optionally, only sicknesses that share any of the tags specified are considered. \n            ', tunable=TunableTags(filter_prefixes=('Sickness',))), 'difficulty_range': OptionalTunable(description="\n            Optionally define the difficulty rating range that is required\n            for the Sim's sickness.\n            ", tunable=TunableInterval(description="\n                The difficulty rating range, this maps to 'difficulty_rating'\n                values in Sickness tuning.\n                ", tunable_type=float, default_lower=0, default_upper=10, minimum=0, maximum=10)), 'verify_tunable_callback': _verify_tunable_callback}

    def give_sickness(self, subject):
        sickness_criteria = CallableTestList()
        if self.tags is not None:
            sickness_criteria.append(lambda s: self.tags & s.sickness_tags)
        if self.difficulty_range is not None:
            sickness_criteria.append(lambda s: s.difficulty_rating in self.difficulty_range)
        services.get_sickness_service().make_sick(subject, criteria_func=sickness_criteria, only_auto_distributable=False)
class _WaypointGeneratorEllipse(_WaypointGeneratorBase):
    FACTORY_TUNABLES = {
        'x_radius_interval':
        TunableInterval(
            description=
            '\n            The min and max radius of the x axis. Make the interval 0 to\n            get rid of variance.\n            ',
            tunable_type=float,
            default_lower=1.0,
            default_upper=1.0,
            minimum=1.0),
        'z_radius_interval':
        TunableInterval(
            description=
            '\n            The min and max radius of the z axis. Make the interval 0 to\n            get rid of variance.\n            ',
            tunable_type=float,
            default_lower=1.0,
            default_upper=1.0,
            minimum=1.0),
        'offset':
        TunableVector3(
            description=
            "\n            The offset of the ellipse relative to the target's position.\n            ",
            default=TunableVector3.DEFAULT_ZERO),
        'orientation':
        TunableAngle(
            description=
            "\n            The orientation of the ellipse relative to the target's\n            orientation. The major axis is X if the angle is 0. If the angle is\n            90 degrees, then the major axis is Z.\n            ",
            default=0)
    }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        ellipse_transform = sims4.math.Transform(
            self.offset, sims4.math.angle_to_yaw_quaternion(self.orientation))
        self.ellipse_transform = sims4.math.Transform.concatenate(
            ellipse_transform, self._target.transform)
        self.start_angle = self.get_start_angle()
        self._start_constraint = None
        self._waypoint_constraints = []

    def transform_point(self, point):
        return self.ellipse_transform.transform_point(point)

    def get_start_angle(self):
        sim_vec = self._context.sim.intended_position - self.ellipse_transform.translation
        return sims4.math.vector3_angle(
            sims4.math.vector_normalize_2d(sim_vec))

    def get_random_ellipse_point_at_angle(self, theta):
        a = self.x_radius_interval.random_float()
        b = self.z_radius_interval.random_float()
        x = a * math.sin(theta)
        z = b * math.cos(theta)
        y = services.terrain_service.terrain_object().get_height_at(x, z)
        return self.transform_point(sims4.math.Vector3(x, y, z))

    def get_ellipse_point_constraint(self, theta):
        position = self.get_random_ellipse_point_at_angle(theta)
        geometry = sims4.geometry.RestrictedPolygon(
            sims4.geometry.CompoundPolygon(sims4.geometry.Polygon(
                (position, ))), ())
        return SmallAreaConstraint(geometry=geometry,
                                   debug_name='EllipsePoint',
                                   routing_surface=self._routing_surface)

    def get_start_constraint(self):
        if self._start_constraint is None:
            self._start_constraint = self.get_ellipse_point_constraint(
                self.start_angle)
            self._start_constraint = self._start_constraint.intersect(
                self.get_water_constraint())
        return self._start_constraint

    def get_waypoint_constraints_gen(self, routing_agent, waypoint_count):
        if not self._waypoint_constraints:
            delta_theta = sims4.math.TWO_PI / waypoint_count
            theta = self.start_angle
            for _ in range(waypoint_count):
                waypoint_constraint = self.get_ellipse_point_constraint(theta)
                self._waypoint_constraints.append(waypoint_constraint)
                theta += delta_theta
            self._waypoint_constraints = self.apply_water_constraint(
                self._waypoint_constraints)
        yield from self._waypoint_constraints
Example #30
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)