class ScholarshipTuning:
    APPLICATION_RESPONSE_TUNING = TunableTuple(
        description=
        '\n        Loot actions to be run on scholarship acceptance or rejection.\n        \n        Reward object(s) given through the loot actions must have the\n        Scholarship Letter Component enabled in order to store information about\n        the resolved scholarship and sim applicant on the object.\n        ',
        value_threshold=Tunable(
            description=
            "\n            A value threshold that when exceeds runs the 'accepted_beyond_value_\n            threshold' loot action.\n            ",
            tunable_type=int,
            default=1),
        accepted_beyond_value_threshold=LootActions.TunablePackSafeReference(
            description=
            '\n            Loot action to be called when a Sim is accepted to\n            a scholarship and the value earned exceeds the tuned value threshold.\n            '
        ),
        accepted_below_value_threshold=LootActions.TunablePackSafeReference(
            description=
            '\n            Loot action to be called when a Sim is accepted to a scholarship and\n            the value does not exceed the tuned value threshold.\n            '
        ),
        rejected=LootActions.TunablePackSafeReference(
            description=
            '\n            Loot action to be called when a Sim is rejected from a scholarship.\n            '
        ))
    FULL_RIDE_LOOT = LootActions.TunablePackSafeReference(
        description=
        '\n        Loot to run if a sim has applied for scholarships and successfully\n        earned a full ride to attend university.\n        '
    )
    MERIT_SCHOLARSHIP = TunablePackSafeReference(
        description=
        '\n        The merit scholarship to evaluate on enrollment. One Merit Scholarship\n        is earned at enrollment if any prestige degrees are earned.\n        ',
        manager=services.get_instance_manager(sims4.resources.Types.SNIPPET),
        class_restrictions='Scholarship')
Ejemplo n.º 2
0
class ChefTuning:
    CHEF_STATION_POT_OBJECT = TunablePackSafeReference(
        description=
        "\n        The pot object to create at the chef's station.\n        ",
        manager=services.definition_manager())
    CHEF_STATION_PAN_OBJECT = TunablePackSafeReference(
        description=
        "\n        The pan object to create at the chef's station.\n        ",
        manager=services.definition_manager())
    CHEF_STATION_CUTTING_BOARD_OBJECT = TunablePackSafeReference(
        description=
        "\n        The cutting board object to create at the chef's station.\n        ",
        manager=services.definition_manager())
    CHEF_STATION_PAN_SLOT = Tunable(
        description=
        '\n        The name of the slot in which the pan object should be placed.\n        ',
        tunable_type=str,
        default='_ctnm_SimInteraction_1')
    CHEF_STATION_POT_SLOT = Tunable(
        description=
        '\n        The name of the slot in which the pot object should be placed.\n        ',
        tunable_type=str,
        default='_ctnm_SimInteraction_2')
    CHEF_STATION_CUTTING_BOARD_SLOT = Tunable(
        description=
        '\n        The name of the slot in which the cutting board object should be placed.\n        ',
        tunable_type=str,
        default='_ctnm_SimInteraction_4')
    CHEF_STATION_SERVE_SLOT_TYPE = TunableReference(
        description=
        '\n        The slot type of the serve slots on the chef station.\n        ',
        manager=services.get_instance_manager(sims4.resources.Types.SLOT_TYPE))
    CHEF_STATION_SERVING_PLATTER_OBJECT = TunablePackSafeReference(
        description=
        "\n        The serving platter object the chef will create and place when they're\n        done cooking an order.\n        ",
        manager=services.definition_manager())
    CHEF_HAS_ORDER_BUFF = TunablePackSafeReference(
        description=
        '\n        The buff a chef should get when they have an order. This should drive\n        them to do the active cooking animations.\n        ',
        manager=services.buff_manager())
    CHEF_COMPLIMENT_LOOT = LootActions.TunablePackSafeReference(
        description=
        "\n        The loot action to trigger when a customer compliments a chef. This\n        won't happen until the waitstaff deliver the compliment.\n        \n        The customer Sim will be the Actor and the Chef will be TargetSim.\n        "
    )
    CHEF_INSULT_LOOT = LootActions.TunablePackSafeReference(
        description=
        "\n        The loot action to trigger when a customer insults a chef. This won't\n        happen until the waitstaff deliver the insult.\n        \n        The customer Sim will be the Actor and the Chef will be TargetSim.\n        "
    )
    PICK_UP_ORDER_INTERACTION = TunablePackSafeReference(
        description=
        '\n        The interaction the sim will run when they pick their order up from the\n        Chef Station. This is only used when a Sim places an order directly at\n        the chef station.\n        ',
        manager=services.get_instance_manager(
            sims4.resources.Types.INTERACTION))
Ejemplo n.º 3
0
 def _derive_consumption_operations(self):
     new_statistics = []
     for stat in self.consumption_statistics:
         amount = stat._amount / self.consumption_turns
         stat_change = StatisticChangeOp(amount=amount,
                                         stat=stat._stat,
                                         subject=stat._subject,
                                         tests=stat._tests)
         new_statistics.append(stat_change)
     if self.fitness_info.consumption_effect != ConsumptionEffects.NO_EFFECT:
         if self.fitness_info.consumption_effect == ConsumptionEffects.CALORIE_GAIN:
             amount = self.fitness_info.calories
         else:
             amount = -self.fitness_info.calories
         amount = amount / self.calorie_modifier
         amount /= self.consumption_turns
         stat_change = StatisticChangeOp(amount=amount,
                                         stat=self.FAT_COMMODITY,
                                         subject=ParticipantType.Actor)
         new_statistics.append(stat_change)
     if not debug_consumables_are_infinite:
         commodity_range = self.CONSUMABLE_COMMODITY.max_value_tuning - self.CONSUMABLE_COMMODITY.min_value_tuning
         amount = commodity_range / self.consumption_turns
         stat_change = StatisticChangeOp(amount=-amount,
                                         stat=self.CONSUMABLE_COMMODITY,
                                         subject=ParticipantType.Object)
         new_statistics.append(stat_change)
     loot_actions = LootActions(run_test_first=False,
                                loot_actions=new_statistics)
     self._loot_list = [loot_actions]
Ejemplo n.º 4
0
 def __init__(self,
              start_description=None,
              end_description=None,
              **kwargs):
     super().__init__(
         key_type=TunableEnumEntry(
             description=
             '\n                    The weather type we are interested in.\n                    ',
             tunable_type=WeatherType,
             default=WeatherType.UNDEFINED),
         value_type=TunableTuple(
             start_loot=TunableList(
                 description=start_description,
                 tunable=LootActions.TunableReference(
                     description=
                     '\n                            The loot action applied.\n                            ',
                     pack_safe=True)),
             end_loot=TunableList(
                 description=end_description,
                 tunable=LootActions.
                 TunableReference(
                     description=
                     '\n                            The loot action applied.\n                            ',
                     pack_safe=True))),
         **kwargs)
     self.cache_key = 'TunableWeatherAwareMapping'
class _PickerDisplaySnippet(HasTunableSingletonFactory, AutoFactoryInit):
    FACTORY_TUNABLES = {
        'display_snippet':
        TunableReference(
            description=
            '\n            A display snippet that holds the display data that will\n            populate the row in the picker.\n            ',
            manager=services.get_instance_manager(
                sims4.resources.Types.SNIPPET),
            class_restrictions='DisplaySnippet',
            allow_none=False),
        'loot_on_selected':
        TunableList(
            description=
            '\n            A list of loot actions that will be applied to the subject Sim.\n            ',
            tunable=LootActions.TunableReference(
                description=
                '\n                A loot action applied to the subject Sim.\n                '
            )),
        'tests':
        TunableTestSetWithTooltip(
            description=
            '\n            Test set that must pass for this snippet to be available.\n            NOTE: A tooltip test result will take priority over any\n            instance display tooltip tuned in the display snippet.\n            \n            ID of the snippet will be the PickedItemID participant\n            '
        ),
        'display_snippet_text_tokens':
        LocalizationTokens.TunableFactory(
            description=
            '\n            Localization tokens passed into the display snippet text fields.\n            These will be appended to the list of tokens when evaluating \n            strings for this snippet. \n            ',
            tuning_group=GroupNames.PICKERTUNING)
    }

    def test(self, resolver):
        return self.tests.run_tests(resolver, search_for_tooltip=True)
class ObjectRoutingBehaviorActionDestroyObjects(ObjectRoutingBehaviorAction):
    FACTORY_TUNABLES = {
        'animation_success':
        OptionalTunable(
            description=
            '\n            If enabled, the animation to play if there are objects to destroy.\n            ',
            tunable=_ObjectRoutingActionAnimation.TunableFactory()),
        'animation_failure':
        OptionalTunable(
            description=
            '\n            If enabled, the animation to play if there are no objects to destroy.\n            ',
            tunable=_ObjectRoutingActionAnimation.TunableFactory()),
        'loot_success':
        TunableList(
            description=
            '\n            For each destroyed object, apply this loot between the routing\n            object (Actor) and the destroyed object (Object).\n            ',
            tunable=LootActions.TunableReference()),
        'object_selection_method':
        TunableVariant(tags=_DestroyObjectSelectionRuleTags.TunableFactory(),
                       target_object=_DestroyObjectSelectionRuleTargetObject.
                       TunableFactory(),
                       default='tags')
    }

    def run_action_gen(self, timeline, obj, target):
        objects = self.object_selection_method.get_objects(obj, target)
        if not objects:
            if self.animation_failure is not None:
                result = yield from self.animation_failure(
                    timeline, obj, target)
                return result
                yield
            return True
            yield

        def _callback(_):
            for o in objects:
                resolver = DoubleObjectResolver(obj, o)
                for loot_action in self.loot_success:
                    loot_action.apply_to_resolver(resolver)
                o.remove_from_client(fade_duration=obj.FADE_DURATION)
                o.destroy(
                    source=self,
                    cause=
                    'Object being destroyed by ObjectRoutingBehaviorActionDestroyObjects'
                )

        if self.animation_success is not None:
            result = yield from self.animation_success(timeline,
                                                       obj,
                                                       target,
                                                       callback=_callback)
            if not result:
                return result
                yield
        else:
            _callback(timeline)
        return True
        yield
Ejemplo n.º 7
0
class SituationJobReward(HasTunableSingletonFactory, AutoFactoryInit):
    __qualname__ = 'SituationJobReward'
    FACTORY_TUNABLES = {
        'reward': Reward.TunableReference(),
        'loot': LootActions.TunableReference()
    }

    def apply(self, sim):
        if self.loot is not None:
            self.loot.apply_to_resolver(sim.get_resolver())
        if self.reward is not None:
            self.reward.give_reward(sim.sim_info)
Ejemplo n.º 8
0
class SicknessTuning:
    SICKNESS_BUFFS_PLAYER_FACED = TunableList(
        description=
        "\n        List of buffs that define if a sim is sick from what the player can \n        see.  The way sickness work, a sim might be sick but it may not be \n        visible to the player, so on this list we should only tune the buff's\n        that would make the sim sick on the players perspective.\n        i.e. buffs that would make a child sim take a day of school.\n        ",
        tunable=TunableReference(manager=services.buff_manager(),
                                 pack_safe=True))
    LOOT_ACTIONS_ON_CHILD_CAREER_AUTO_SICK = TunableList(
        description=
        '\n        Loot actions to test and apply on the event its time to go to work \n        and the child sim is sick.\n        i.e. notification...  \n        ',
        tunable=LootActions.TunableReference(pack_safe=True))

    @classmethod
    def is_child_sim_sick(cls, sim_info):
        if not sim_info.is_child:
            return False
        return any(
            sim_info.has_buff(buff_type)
            for buff_type in SicknessTuning.SICKNESS_BUFFS_PLAYER_FACED)
Ejemplo n.º 9
0
class ScheduledLoot(HasTunableFactory, AutoFactoryInit):
    FACTORY_TUNABLES = {
        'loot':
        TunableList(
            description=
            '\n            Loot applied when the effect is enacted.\n            ',
            tunable=LootActions.TunableReference(
                description=
                '\n                Loot applied when the effect is enacted.\n                ',
                pack_safe=True)),
        'schedule_data':
        WeeklySchedule.TunableFactory(
            description=
            '\n            The information to schedule points during the week that\n            the Street Policy Effect, if enacted, will award loot.\n            '
        )
    }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._loot_schedule = None
        self._resolver_gen = None

    def set_resolver_gen(self, resolver_gen):
        self._resolver_gen = resolver_gen

    def start_loot_schedule(self):
        if self._loot_schedule is not None:
            self._loot_schedule.destroy()
        self._loot_schedule = self.schedule_data(
            start_callback=self._handle_scheduled_loot_action,
            schedule_immediate=True)

    def stop_loot_schedule(self):
        self._loot_schedule.destroy()
        self._loot_schedule = None

    def _handle_scheduled_loot_action(self, scheduler, alarm_data, extra_data):
        if self._resolver_gen is None:
            return
        for resolver in self._resolver_gen():
            for loot in self.loot:
                loot.apply_to_resolver(resolver)
class _NeighborWaitToBeGreetedState(CommonInteractionCompletedSituationState):
    FACTORY_TUNABLES = {
        'early_exit_loot':
        TunableList(
            description=
            '\n            A list of loot to apply between the neighbor and the active\n            household Sims if this stiuation state times out.\n            ',
            tunable=LootActions.TunableReference(
                description=
                '\n                A loot action applied to all of the active household Sims if this\n                situation state times out.\n                '
            )),
        'early_exit_notification':
        TunableUiDialogNotificationSnippet(
            description=
            '\n            Notification that will be shown when this situation state times\n            out.\n            '
        )
    }

    def __init__(self,
                 *args,
                 early_exit_loot=tuple(),
                 early_exit_notification=None,
                 **kwargs):
        super().__init__(*args, **kwargs)
        self._early_exit_loot = early_exit_loot
        self._early_exit_notification = early_exit_notification

    def _on_interaction_of_interest_complete(self, **kwargs):
        self._change_state(self.owner._hangout_state())

    def timer_expired(self):
        for sim_info in services.active_household():
            resolver = DoubleSimResolver(sim_info,
                                         self.owner._neighbor_sim.sim_info)
            for loot_action in self._early_exit_loot:
                loot_action.apply_to_resolver(resolver)
        resolver = DoubleSimResolver(services.active_sim_info(),
                                     self.owner._neighbor_sim.sim_info)
        early_exit_notification = self._early_exit_notification(
            services.active_sim_info(), resolver=resolver)
        early_exit_notification.show_dialog()
        self.owner._self_destruct()
class ObjectRoutingBehaviorActionApplyLoot(ObjectRoutingBehaviorAction):
    FACTORY_TUNABLES = {
        'loot_actions':
        TunableList(
            description=
            "\n            Loot to apply.\n            Participant type 'Actor' refers to the object that is routing (ie, the 'bot').\n            Participant type 'Object' refers to the target object the bot is acting upon.\n            ",
            tunable=LootActions.TunableReference())
    }

    def run_action_gen(self, timeline, obj, target):
        if self.loot_actions is None:
            return True
            yield
        if target is None:
            resolver = SingleObjectResolver(obj)
        else:
            resolver = DoubleObjectResolver(obj, target)
        for loot_action in self.loot_actions:
            loot_action.apply_to_resolver(resolver)
        return True
        yield
Ejemplo n.º 12
0
class Gig(HasTunableReference,
          _GigDisplayMixin,
          PrepTaskTrackerMixin,
          metaclass=HashedTunedInstanceMetaclass,
          manager=services.get_instance_manager(
              sims4.resources.Types.CAREER_GIG)):
    INSTANCE_TUNABLES = {
        'career':
        TunableReference(
            description=
            '\n            The career this gig is associated with.\n            ',
            manager=services.get_instance_manager(
                sims4.resources.Types.CAREER)),
        'gig_time':
        WeeklySchedule.TunableFactory(
            description=
            '\n            A tunable schedule that will determine when you have to be at work.\n            ',
            export_modes=ExportModes.All),
        'gig_prep_time':
        TunableTimeSpan(
            description=
            '\n            The amount of time between when a gig is selected and when it\n            occurs.\n            ',
            default_hours=5),
        'gig_prep_tasks':
        TunableList(
            description=
            '\n            A list of prep tasks the Sim can do to improve their performance\n            during the gig. \n            ',
            tunable=PrepTask.TunableFactory()),
        'loots_on_schedule':
        TunableList(
            description=
            '\n            Loot actions to apply when a sim gets a gig.\n            ',
            tunable=LootActions.TunableReference()),
        'audio_on_prep_task_completion':
        OptionalTunable(
            description=
            '\n            A sting to play at the time a prep task completes.\n            ',
            tunable=TunablePlayAudio(
                locked_args={
                    'immediate_audio': True,
                    'joint_name_hash': None,
                    'play_on_active_sim_only': True
                })),
        'gig_pay':
        TunableVariant(
            description=
            '\n            Base amount of pay for this gig. Can be either a flat amount or a\n            range.\n            ',
            range=TunableInterval(tunable_type=int,
                                  default_lower=0,
                                  default_upper=100,
                                  minimum=0),
            flat_amount=TunableIntervalLiteral(tunable_type=int,
                                               default=0,
                                               minimum=0),
            default='range'),
        'additional_pay_per_overmax_level':
        OptionalTunable(
            description=
            '\n            If checked, overmax levels will be considered when calculating pay\n            for this gig. The actual implementation of this may vary by gig\n            type.\n            ',
            tunable=TunableRange(tunable_type=int, default=0, minimum=0)),
        'result_based_gig_pay_multipliers':
        OptionalTunable(
            description=
            '\n            A set of multipliers for gig pay. The multiplier used depends on the\n            GigResult of the gig. The meanings of each GigResult may vary by\n            gig type.\n            ',
            tunable=TunableMapping(
                description=
                '\n                A map between the result type of the gig and the additional pay\n                the sim will receive.\n                ',
                key_type=TunableEnumEntry(tunable_type=GigResult,
                                          default=GigResult.SUCCESS),
                value_type=TunableMultiplier.TunableFactory())),
        'initial_result_based_career_performance':
        OptionalTunable(
            description=
            "\n            A mapping between the GigResult for this gig and the initial\n            career performance for the Sim's first gig.\n            ",
            tunable=TunableMapping(
                description=
                '\n                A map between the result type of the gig and the initial career\n                performance the Sim will receive.\n                ',
                key_type=TunableEnumEntry(
                    description=
                    '\n                    The GigResult enum that represents the outcome of the Gig.\n                    ',
                    tunable_type=GigResult,
                    default=GigResult.SUCCESS),
                value_type=Tunable(
                    description=
                    '\n                    The initial performance value that will be applied.\n                    ',
                    tunable_type=float,
                    default=0))),
        'result_based_career_performance':
        OptionalTunable(
            description=
            '\n            A mapping between the GigResult for this gig and the change in\n            career performance for the sim.\n            ',
            tunable=TunableMapping(
                description=
                '\n                A map between the result type of the gig and the career\n                performance the sim will receive.\n                ',
                key_type=TunableEnumEntry(
                    description=
                    '\n                    The GigResult enum that represents the outcome of the Gig.\n                    ',
                    tunable_type=GigResult,
                    default=GigResult.SUCCESS),
                value_type=Tunable(
                    description=
                    '\n                    The performance modification.\n                    ',
                    tunable_type=float,
                    default=0))),
        'result_based_career_performance_multiplier':
        OptionalTunable(
            description=
            '\n            A mapping between the GigResult and the multiplier for the career \n            performance awarded.\n            ',
            tunable=TunableMapping(
                description=
                '\n                A map between the result type of the gig and the career\n                performance multiplier.\n                ',
                key_type=TunableEnumEntry(
                    description=
                    '\n                    The GigResult enum that represents the outcome of the Gig.\n                    ',
                    tunable_type=GigResult,
                    default=GigResult.SUCCESS),
                value_type=TunableMultiplier.TunableFactory(
                    description=
                    '\n                    The performance modification multiplier.\n                    '
                ))),
        'result_based_loots':
        OptionalTunable(
            description=
            '\n            A mapping between the GigResult for this gig and a loot list to\n            optionally apply. The resolver for this loot list is either a\n            SingleSimResolver of the working sim or a DoubleSimResolver with the\n            target being the customer if there is a customer sim.\n            ',
            tunable=TunableMapping(
                description=
                '\n                A map between the result type of the gig and the loot list.\n                ',
                key_type=TunableEnumEntry(tunable_type=GigResult,
                                          default=GigResult.SUCCESS),
                value_type=TunableList(
                    description=
                    '\n                    Loot actions to apply.\n                    ',
                    tunable=LootActions.TunableReference(
                        description=
                        '\n                        The loot action applied.\n                        ',
                        pack_safe=True)))),
        'payout_stat_data':
        TunableMapping(
            description=
            '\n            Stats, and its associated information, that are gained (or lost) \n            when sim finishes this gig.\n            ',
            key_type=TunableReference(
                description=
                '\n                Stat for this payout.\n                ',
                manager=services.get_instance_manager(
                    sims4.resources.Types.STATISTIC)),
            value_type=TunableTuple(
                description=
                '\n                Data about this payout stat. \n                ',
                base_amount=Tunable(
                    description=
                    '\n                    Base amount (pre-modifiers) applied to the sim at the end\n                    of the gig.\n                    ',
                    tunable_type=float,
                    default=0.0),
                medal_to_payout=TunableMapping(
                    description=
                    '\n                    Mapping of medal -> stat multiplier.\n                    ',
                    key_type=TunableEnumEntry(
                        description=
                        '\n                        Medal achieved in this gig.\n                        ',
                        tunable_type=SituationMedal,
                        default=SituationMedal.TIN),
                    value_type=TunableMultiplier.TunableFactory(
                        description=
                        '\n                        Mulitiplier on statistic payout if scorable situation\n                        ends with the associate medal.\n                        '
                    )),
                ui_threshold=TunableList(
                    description=
                    '\n                    Thresholds and icons we use for this stat to display in \n                    the end of day dialog. Tune in reverse of highest threshold \n                    to lowest threshold.\n                    ',
                    tunable=TunableTuple(
                        description=
                        '\n                        Threshold and icon for this stat and this gig.\n                        ',
                        threshold_icon=TunableIcon(
                            description=
                            '\n                            Icon if the stat is of this threshold.\n                            '
                        ),
                        threshold_description=TunableLocalizedStringFactory(
                            description=
                            '\n                            Description to use with icon\n                            '
                        ),
                        threshold=Tunable(
                            description=
                            '\n                            Threshold that the stat must >= to.\n                            ',
                            tunable_type=float,
                            default=0.0))))),
        'career_events':
        TunableList(
            description=
            '\n             A list of available career events for this gig.\n             ',
            tunable=TunableReference(manager=services.get_instance_manager(
                sims4.resources.Types.CAREER_EVENT))),
        'gig_cast_rel_bit_collection_id':
        TunableEnumEntry(
            description=
            '\n            If a rel bit is applied to the cast member, it must be of this collection id.\n            We use this to clear the rel bit when the gig is over.\n            ',
            tunable_type=RelationshipBitCollectionUid,
            default=RelationshipBitCollectionUid.Invalid,
            invalid_enums=(RelationshipBitCollectionUid.All, )),
        'gig_cast':
        TunableList(
            description=
            '\n            This is the list of sims that need to spawn for this gig. \n            ',
            tunable=TunableTuple(
                description=
                '\n                Data for cast members. It contains a test which tests against \n                the owner of this gig and spawn the necessary sims. A bit\n                may be applied through the loot action to determine the type \n                of cast members (costars, directors, etc...) \n                ',
                filter_test=TunableTestSet(
                    description=
                    '\n                    Test used on owner sim.\n                    '
                ),
                sim_filter=TunableSimFilter.TunableReference(
                    description=
                    '\n                    If filter test is passed, this sim is created and stored.\n                    '
                ),
                cast_member_rel_bit=OptionalTunable(
                    description=
                    '\n                    If tuned, this rel bit will be applied on the spawned cast \n                    member.\n                    ',
                    tunable=RelationshipBit.TunableReference(
                        description=
                        '\n                        Rel bit to apply.\n                        '
                    )))),
        'end_of_gig_dialog':
        OptionalTunable(
            description=
            '\n            A results dialog to show. This dialog allows a list\n            of icons with labels. Stats are added at the end of this icons.\n            ',
            tunable=UiDialogLabeledIcons.TunableFactory()),
        'disabled_tooltip':
        OptionalTunable(
            description=
            '\n            If tuned, the tooltip when this row is disabled.\n            ',
            tunable=TunableLocalizedStringFactory(),
            tuning_group=GroupNames.UI),
        'end_of_gig_notifications':
        OptionalTunable(
            description=
            '\n            If enabled, a notification to show at the end of the gig instead of\n            a normal career message. Tokens are:\n            * 0: The Sim owner of the career\n            * 1: The level name (e.g. Chef)\n            * 2: The career name (e.g. Culinary)\n            * 3: The company name (e.g. Maids United)\n            * 4: The pay for the gig\n            * 5: The gratuity for the gig\n            * 6: The customer (sim) of the gig, if there is a customer.\n            * 7: A bullet list of loots and payments as a result of this gig.\n                 This list uses the text tuned on the loots themselves to create\n                 bullets for each loot. Those texts will generally have tokens 0\n                 and 1 be the subject and target sims (of the loot) but may\n                 have additional tokens depending on the type of loot.\n            ',
            tunable=TunableMapping(
                description=
                '\n                A map between the result type of the gig and the post-gig\n                notification.\n                ',
                key_type=TunableEnumEntry(tunable_type=GigResult,
                                          default=GigResult.SUCCESS),
                value_type=_get_career_notification_tunable_factory()),
            tuning_group=GroupNames.UI),
        'end_of_gig_overmax_notification':
        OptionalTunable(
            description=
            '\n            If tuned, the notification that will be used if the sim gains an\n            overmax level during this gig. Will override the overmax\n            notification in career messages. The following tokens are provided:\n            * 0: The Sim owner of the career\n            * 1: The level name (e.g. Chef)\n            * 2: The career name (e.g. Culinary)\n            * 3: The company name (e.g. Maids United)\n            * 4: The overmax level\n            * 5: The pay for the gig\n            * 6: Additional pay tuned at additional_pay_per_overmax_level \n            * 7: The overmax rewards in a bullet-point list, in the form of a\n                 string. These are tuned on the career_track\n            ',
            tunable=_get_career_notification_tunable_factory(),
            tuning_group=GroupNames.UI),
        'end_of_gig_overmax_rewardless_notification':
        OptionalTunable(
            description=
            '\n            If tuned, the notification that will be used if the sim gains an\n            overmax level with no reward during this gig. Will override the\n            overmax rewardless notification in career messages.The following\n            tokens are provided:\n            * 0: The Sim owner of the career\n            * 1: The level name (e.g. Chef)\n            * 2: The career name (e.g. Culinary)\n            * 3: The company name (e.g. Maids United)\n            * 4: The overmax level\n            * 5: The pay for the gig\n            * 6: Additional pay tuned at additional_pay_per_overmax_level \n            ',
            tunable=_get_career_notification_tunable_factory(),
            tuning_group=GroupNames.UI),
        'end_of_gig_promotion_text':
        OptionalTunable(
            description=
            '\n            A string that, if enabled, will be pre-pended to the bullet\n            list of results in the promotion notification. Tokens are:\n            * 0 : The Sim owner of the career\n            ',
            tunable=TunableLocalizedStringFactory(),
            tuning_group=GroupNames.UI),
        'end_of_gig_demotion_text':
        OptionalTunable(
            description=
            '\n            A string that, if enabled, will be pre-pended to the bullet\n            list of results in the promotion notification. Tokens are:\n            * 0 : The Sim owner of the career\n            ',
            tunable=TunableLocalizedStringFactory(),
            tuning_group=GroupNames.UI),
        'odd_job_tuning':
        OptionalTunable(
            description=
            '\n            Tuning specific to odd jobs. Leave untuned if this gig is not an\n            odd job.\n            ',
            tunable=TunableTuple(
                customer_description=TunableLocalizedStringFactory(
                    description=
                    '\n                    The description of the odd job written by the customer.\n                    Token 0 is the customer sim.\n                    '
                ),
                use_customer_description_as_gig_description=Tunable(
                    description=
                    '\n                    If checked, the customer description will be used as the\n                    gig description. This description is used as the tooltip\n                    for the gig icon in the career panel.\n                    ',
                    tunable_type=bool,
                    default=False),
                result_based_gig_gratuity_multipliers=TunableMapping(
                    description=
                    '\n                    A set of multipliers for the gig gratuity.  This maps the\n                    result type of the gig and the gratuity multiplier (a \n                    percentage).  The base pay will be multiplied by this \n                    multiplier in order to determine the actual gratuity \n                    amount.\n                    ',
                    key_type=TunableEnumEntry(
                        description=
                        '\n                        The GigResult enum that represents the outcome of the \n                        Gig.\n                        ',
                        tunable_type=GigResult,
                        default=GigResult.SUCCESS),
                    value_type=TunableMultiplier.TunableFactory(
                        description=
                        '\n                        The gratuity multiplier to be calculated for this \n                        GigResult.\n                        '
                    )),
                result_based_gig_gratuity_chance_multipliers=TunableMapping(
                    description=
                    '\n                    A set of multipliers for determining the gig gratuity \n                    chance (i.e., the probability the Sim will receive gratuity \n                    in addition to the base pay).  The gratuity chance depends \n                    on the GigResult of the gig.  This maps the result type of \n                    the gig and the gratuity chance/percentage.  If this map\n                    (or a GigResult) is left untuned, then no gratuity is \n                    added.\n                    ',
                    key_type=TunableEnumEntry(
                        description=
                        '\n                        The GigResult enum that represents the outcome of the \n                        Gig.\n                        ',
                        tunable_type=GigResult,
                        default=GigResult.SUCCESS),
                    value_type=TunableMultiplier.TunableFactory(
                        description=
                        '\n                        The multiplier to be calculated for this GigResult.  \n                        This represents the percentage chance the Sim will \n                        receive gratuity.  If the Sim is to not receive \n                        gratuity, the base value should be 0 (without further\n                        tests).  If this Sim is guaranteed to receive gratuity,\n                        the base value should be 1 (without further tests).\n                        '
                    )),
                gig_gratuity_bullet_point_text=OptionalTunable(
                    description=
                    '\n                    If enabled, the gig gratuity will be a bullet point in the\n                    bullet pointed list of loots and money supplied to the end\n                    of gig notification (this is token 7 of that notification).\n                    If disabled, gratuity will be omitted from that list.\n                    Tokens:\n                    * 0: The sim owner of the career\n                    * 1: The customer\n                    * 2: the gratuity amount \n                    ',
                    tunable=TunableLocalizedStringFactory()))),
        'tip':
        OptionalTunable(
            description=
            '\n            A tip that is displayed with the gig in pickers and in the career\n            panel. Can produce something like "Required Skill: Fitness 2".\n            ',
            tunable=TunableTuple(
                tip_title=TunableLocalizedStringFactory(
                    description=
                    '\n                    The title string of the tip. Could be something like "Required\n                    Skill.\n                    '
                ),
                tip_text=TunableLocalizedStringFactory(
                    description=
                    '\n                    The text string of the tip. Could be something like "Fitness 2".\n                    '
                ),
                tip_icon=OptionalTunable(tunable=TunableIcon(
                    description=
                    '\n                        An icon to show along with the tip.\n                        '
                )))),
        'critical_failure_test':
        OptionalTunable(
            description=
            '\n            The tests for checking whether or not the Sim should receive the \n            CRITICAL_FAILURE outcome.  This will override other GigResult \n            behavior.\n            ',
            tunable=TunableTestSet(
                description=
                '\n                The tests to be performed on the Sim (and any customer).  If \n                the tests pass, the outcome will be CRITICAL_FAILURE.  \n                '
            ))
    }

    def __init__(self, owner, customer=None, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._owner = owner
        self._customer_id = customer.id if customer is not None else None
        self._upcoming_gig_time = None
        self._gig_result = None
        self._gig_pay = None
        self._gig_gratuity = None
        self._loot_strings = None
        self._gig_attended = False

    @classmethod
    def get_aspiration(cls):
        pass

    @classmethod
    def get_time_until_next_possible_gig(cls, starting_time):
        required_prep_time = cls.gig_prep_time()
        start_considering_prep = starting_time + required_prep_time
        (time_until, _) = cls.gig_time().time_until_next_scheduled_event(
            start_considering_prep)
        if not time_until:
            return
        return time_until + required_prep_time

    def register_aspiration_callbacks(self):
        aspiration = self.get_aspiration()
        if aspiration is None:
            return
        aspiration.register_callbacks()
        aspiration_tracker = self._owner.aspiration_tracker
        if aspiration_tracker is None:
            return
        aspiration_tracker.validate_and_return_completed_status(aspiration)
        aspiration_tracker.process_test_events_for_aspiration(aspiration)

    def notify_gig_attended(self):
        self._gig_attended = True

    def has_attended_gig(self):
        return self._gig_attended

    def notify_canceled(self):
        self._gig_result = GigResult.CANCELED
        self._send_gig_telemetry(TELEMETRY_GIG_PROGRESS_CANCEL)

    def get_career_performance(self, first_gig=False):
        if not self.result_based_career_performance:
            return 0
        if self.initial_result_based_career_performance is not None and first_gig and self._gig_result in self.initial_result_based_career_performance:
            return self.initial_result_based_career_performance[
                self._gig_result]
        performance_modifier = 1
        if self.result_based_career_performance_multiplier:
            if self._gig_result in self.result_based_career_performance_multiplier:
                resolver = self.get_resolver_for_gig()
                performance_modifier = self.result_based_career_performance_multiplier[
                    self._gig_result].get_multiplier(resolver)
        return self.result_based_career_performance.get(
            self._gig_result, 0) * performance_modifier

    def treat_work_time_as_due_date(self):
        return False

    @classmethod
    def create_picker_row(cls,
                          description=None,
                          scheduled_time=None,
                          owner=None,
                          gig_customer=None,
                          enabled=True,
                          **kwargs):
        tip = cls.tip
        description = cls.gig_picker_localization_format(
            cls.gig_pay.lower_bound, cls.gig_pay.upper_bound, scheduled_time,
            tip.tip_title(), tip.tip_text(), gig_customer)
        if not enabled and cls.disabled_tooltip is not None:
            row_tooltip = lambda *_: cls.disabled_tooltip(owner)
        elif cls.display_description is None:
            row_tooltip = None
        else:
            row_tooltip = lambda *_: cls.display_description(owner)
        if cls.odd_job_tuning is not None:
            customer_description = cls.odd_job_tuning.customer_description(
                gig_customer)
            row = OddJobPickerRow(customer_id=gig_customer.id,
                                  customer_description=customer_description,
                                  tip_title=tip.tip_title(),
                                  tip_text=tip.tip_text(),
                                  tip_icon=tip.tip_icon,
                                  name=cls.display_name(owner),
                                  icon=cls.display_icon,
                                  row_description=description,
                                  row_tooltip=row_tooltip,
                                  is_enable=enabled)
        else:
            row = ObjectPickerRow(name=cls.display_name(owner),
                                  icon=cls.display_icon,
                                  row_description=description,
                                  row_tooltip=row_tooltip,
                                  is_enable=enabled)
        return row

    def get_gig_time(self):
        return self._upcoming_gig_time

    def get_gig_customer(self):
        return self._customer_id

    def clean_up_gig(self):
        if self.gig_prep_tasks:
            self.prep_task_cleanup()

    def save_gig(self, gig_proto_buff):
        gig_proto_buff.gig_type = self.guid64
        gig_proto_buff.gig_time = self._upcoming_gig_time
        if hasattr(gig_proto_buff, 'gig_attended'):
            gig_proto_buff.gig_attended = self._gig_attended
        if self._customer_id:
            gig_proto_buff.customer_sim_id = self._customer_id

    def load_gig(self, gig_proto_buff):
        self._upcoming_gig_time = DateAndTime(gig_proto_buff.gig_time)
        if self.gig_prep_tasks:
            self.prep_time_start(self._owner,
                                 self.gig_prep_tasks,
                                 self.guid64,
                                 self.audio_on_prep_task_completion,
                                 from_load=True)
        if gig_proto_buff.HasField('customer_sim_id'):
            self._customer_id = gig_proto_buff.customer_sim_id
        if gig_proto_buff.HasField('gig_attended'):
            self._gig_attended = gig_proto_buff.gig_attended

    def set_gig_time(self, upcoming_gig_time):
        self._upcoming_gig_time = upcoming_gig_time

    def get_resolver_for_gig(self):
        if self._customer_id is not None:
            customer_sim_info = services.sim_info_manager().get(
                self._customer_id)
            if customer_sim_info is not None:
                return DoubleSimResolver(self._owner, customer_sim_info)
        return SingleSimResolver(self._owner)

    def set_up_gig(self):
        if self.gig_prep_tasks:
            self.prep_time_start(self._owner, self.gig_prep_tasks, self.guid64,
                                 self.audio_on_prep_task_completion)
        if self.loots_on_schedule:
            resolver = self.get_resolver_for_gig()
            for loot_actions in self.loots_on_schedule:
                loot_actions.apply_to_resolver(resolver)
        self._send_gig_telemetry(TELEMETRY_GIG_PROGRESS_STARTED)

    def collect_rabbit_hole_rewards(self):
        pass

    def _get_additional_loots(self):
        if self.result_based_loots is not None and self._gig_result is not None:
            loots = self.result_based_loots.get(self._gig_result)
            if loots is not None:
                return loots
        return ()

    def collect_additional_rewards(self):
        loots = self._get_additional_loots()
        if loots:
            self._loot_strings = []
            resolver = self.get_resolver_for_gig()
            for loot_actions in loots:
                self._loot_strings.extend(
                    loot_actions.apply_to_resolver_and_get_display_texts(
                        resolver))

    def _determine_gig_outcome(self):
        raise NotImplementedError

    def get_pay(self, overmax_level=None, **kwargs):
        self._determine_gig_outcome()
        pay = self.gig_pay.lower_bound
        if self.additional_pay_per_overmax_level:
            pay = pay + overmax_level * self.additional_pay_per_overmax_level
        resolver = self.get_resolver_for_gig()
        if self.result_based_gig_pay_multipliers:
            if self._gig_result in self.result_based_gig_pay_multipliers:
                multiplier = self.result_based_gig_pay_multipliers[
                    self._gig_result].get_multiplier(resolver)
                pay = int(pay * multiplier)
        gratuity = 0
        if self.odd_job_tuning:
            if self.odd_job_tuning.result_based_gig_gratuity_multipliers:
                if self.odd_job_tuning.result_based_gig_gratuity_chance_multipliers:
                    gratuity_multiplier = 0
                    gratuity_chance = 0
                    if self._gig_result in self.odd_job_tuning.result_based_gig_gratuity_chance_multipliers:
                        gratuity_chance = self.odd_job_tuning.result_based_gig_gratuity_chance_multipliers[
                            self._gig_result].get_multiplier(resolver)
                    if random.random() <= gratuity_chance:
                        if self._gig_result in self.odd_job_tuning.result_based_gig_gratuity_multipliers:
                            gratuity_multiplier = self.odd_job_tuning.result_based_gig_gratuity_multipliers[
                                self._gig_result].get_multiplier(resolver)
                            gratuity = int(pay * gratuity_multiplier)
        self._gig_pay = pay
        self._gig_gratuity = gratuity
        return pay + gratuity

    def get_promotion_evaluation_result(self,
                                        reward_text,
                                        *args,
                                        first_gig=False,
                                        **kwargs):
        if self._gig_result is not None and self.end_of_gig_notifications is not None:
            notification = self.end_of_gig_notifications.get(
                self._gig_result, None)
            if notification:
                customer_sim_info = services.sim_info_manager().get(
                    self._customer_id)
                if self.end_of_gig_promotion_text and not first_gig:
                    results_list = self.get_results_list(
                        self.end_of_gig_promotion_text(self._owner))
                else:
                    results_list = self.get_results_list()
                return EvaluationResult(Evaluation.PROMOTED, notification,
                                        self._gig_pay, self._gig_gratuity,
                                        customer_sim_info, results_list)

    def get_demotion_evaluation_result(self, *args, first_gig=False, **kwargs):
        if self._gig_result is not None and self.end_of_gig_notifications is not None:
            notification = self.end_of_gig_notifications.get(
                self._gig_result, None)
            if notification:
                customer_sim_info = services.sim_info_manager().get(
                    self._customer_id)
                if self.end_of_gig_demotion_text and not first_gig:
                    results_list = self.get_results_list(
                        self.end_of_gig_demotion_text(self._owner))
                else:
                    results_list = self.get_results_list()
                return EvaluationResult(Evaluation.DEMOTED, notification,
                                        self._gig_pay, self._gig_gratuity,
                                        customer_sim_info, results_list)

    def get_overmax_evaluation_result(self, overmax_level, reward_text, *args,
                                      **kwargs):
        if reward_text and self.end_of_gig_overmax_notification:
            return EvaluationResult(Evaluation.PROMOTED,
                                    self.end_of_gig_overmax_notification,
                                    overmax_level, self._gig_pay,
                                    self.additional_pay_per_overmax_level,
                                    reward_text)
        elif self.end_of_gig_overmax_rewardless_notification:
            return EvaluationResult(
                Evaluation.PROMOTED,
                self.end_of_gig_overmax_rewardless_notification, overmax_level,
                self._gig_pay, self.additional_pay_per_overmax_level)

    def _get_strings_for_results_list(self):
        strings = []
        if self._gig_pay is not None:
            strings.append(LocalizationHelperTuning.MONEY(self._gig_pay))
        if self.odd_job_tuning is not None and self._gig_gratuity:
            gratuity_text_factory = self.odd_job_tuning.gig_gratuity_bullet_point_text
            if gratuity_text_factory is not None:
                customer_sim_info = services.sim_info_manager().get(
                    self._customer_id)
                gratuity_text = gratuity_text_factory(self._owner,
                                                      customer_sim_info,
                                                      self._gig_gratuity)
                strings.append(gratuity_text)
        if self._loot_strings:
            strings.extend(self._loot_strings)
        return strings

    def get_results_list(self, *additional_tokens):
        return LocalizationHelperTuning.get_bulleted_list(
            None, *additional_tokens, *self._get_strings_for_results_list())

    def get_end_of_gig_evaluation_result(self, **kwargs):
        if self._gig_result is not None and self.end_of_gig_notifications is not None:
            notification = self.end_of_gig_notifications.get(
                self._gig_result, None)
            if notification:
                customer_sim_info = services.sim_info_manager().get(
                    self._customer_id)
                return EvaluationResult(Evaluation.ON_TARGET, notification,
                                        self._gig_pay, self._gig_gratuity,
                                        customer_sim_info,
                                        self.get_results_list())

    @classmethod
    def _get_base_pay_for_gig_owner(cls, owner):
        overmax_pay = cls.additional_pay_per_overmax_level
        if overmax_pay is not None:
            career = owner.career_tracker.get_career_by_uid(cls.career.guid64)
            if career is not None:
                overmax_pay *= career.overmax_level
                return (cls.gig_pay.lower_bound + overmax_pay,
                        cls.gig_pay.upper_bound + overmax_pay)
        return (cls.gig_pay.lower_bound, cls.gig_pay.upper_bound)

    @classmethod
    def build_gig_msg(cls, msg, sim, gig_time=None, gig_customer=None):
        msg.gig_type = cls.guid64
        msg.gig_name = cls.display_name(sim)
        (pay_lower, pay_upper) = cls._get_base_pay_for_gig_owner(sim)
        msg.min_pay = pay_lower
        msg.max_pay = pay_upper
        msg.gig_icon = ResourceKey()
        msg.gig_icon.instance = cls.display_icon.instance
        msg.gig_icon.group = cls.display_icon.group
        msg.gig_icon.type = cls.display_icon.type
        if cls.odd_job_tuning is not None and cls.odd_job_tuning.use_customer_description_as_gig_description and gig_customer is not None:
            customer_sim_info = services.sim_info_manager().get(gig_customer)
            if customer_sim_info is not None:
                msg.gig_description = cls.odd_job_tuning.customer_description(
                    customer_sim_info)
        else:
            msg.gig_description = cls.display_description(sim)
        if gig_time is not None:
            msg.gig_time = gig_time
        if gig_customer is not None:
            msg.customer_id = gig_customer
        if cls.tip is not None:
            msg.tip_title = cls.tip.tip_title()
            if cls.tip.tip_icon is not None or cls.tip.tip_text is not None:
                build_icon_info_msg(
                    IconInfoData(icon_resource=cls.tip.tip_icon),
                    None,
                    msg.tip_icon,
                    desc=cls.tip.tip_text())

    def send_prep_task_update(self):
        if self.gig_prep_tasks:
            self._prep_task_tracker.send_prep_task_update()

    def _apply_payout_stat(self, medal, payout_display_data=None):
        owner_sim = self._owner
        resolver = SingleSimResolver(owner_sim)
        payout_stats = self.payout_stat_data
        for stat in payout_stats.keys():
            stat_tracker = owner_sim.get_tracker(stat)
            if not owner_sim.get_tracker(stat).has_statistic(stat):
                continue
            stat_data = payout_stats[stat]
            stat_multiplier = 1.0
            if medal in stat_data.medal_to_payout:
                multiplier = stat_data.medal_to_payout[medal]
                stat_multiplier = multiplier.get_multiplier(resolver)
            stat_total = stat_data.base_amount * stat_multiplier
            stat_tracker.add_value(stat, stat_total)
            if payout_display_data is not None:
                for threshold_data in stat_data.ui_threshold:
                    if stat_total >= threshold_data.threshold:
                        payout_display_data.append(threshold_data)
                        break

    def _send_gig_telemetry(self, progress):
        with telemetry_helper.begin_hook(gig_telemetry_writer,
                                         TELEMETRY_HOOK_GIG_PROGRESS,
                                         sim_info=self._owner) as hook:
            hook.write_int(TELEMETRY_CAREER_ID, self.career.guid64)
            hook.write_int(TELEMETRY_GIG_ID, self.guid64)
            hook.write_int(TELEMETRY_GIG_PROGRESS_NUMBER, progress)
Ejemplo n.º 13
0
class UniversityMajor(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(Types.UNIVERSITY_MAJOR)):
    INSTANCE_TUNABLES = {'courses': TunableList(description='\n            List of courses, in order, for this major\n            ', tunable=UniversityCourseData.TunableReference(), minlength=1), 'acceptance_score': TunableTuple(description='\n            Score requirement to be accepted in this major as prestige degree.\n            ', score=TunableMultiplier.TunableFactory(description='\n                Define the base score and multiplier to calculate acceptance\n                score of a Sim.\n                '), score_threshold=TunableThreshold(description='\n                The threshold to perform against the score to see if a Sim \n                can be accepted in this major.\n                ')), 'display_name': TunableLocalizedString(description="\n            The major's name.\n            ", tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'display_description': TunableLocalizedString(description="\n            The major's description.\n            ", tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'icons': TunableTuple(description='\n            Display icons for this major.\n            ', icon=TunableIcon(description="\n                The major's icon.\n                "), icon_prestige=TunableIcon(description="\n                The major's prestige icon.\n                "), icon_high_res=TunableIcon(description="\n                The major's high resolution icon.\n                "), icon_prestige_high_res=TunableIcon(description="\n                The major's prestige high resolution icon.\n                "), export_class_name='UniversityMajorIconTuple', tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'major_benefit_map': TunableMapping(description='\n            University specific major benefit description. Each university can \n            have its own description defined for this University Major.\n            ', key_type=TunableReference(manager=services.get_instance_manager(Types.UNIVERSITY)), value_type=TunableLocalizedString(description='\n                Major benefit description.\n                '), tuple_name='UniversityMajorBenefitMapping', tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'graduation_reward': TunableMapping(description='\n            Loot on graduation at each university for each GPA threshold\n            ', key_type=TunableReference(manager=services.get_instance_manager(Types.UNIVERSITY)), value_type=TunableList(description='\n                Loot for each GPA range (lower bound inclusive, upper bound\n                exclusive.\n                ', tunable=TunableTuple(gpa_range=TunableInterval(description='\n                        GPA range to receive this loot.\n                        Lower bound inclusive, upper bound exclusive.\n                        ', tunable_type=float, default_lower=0, default_upper=10), loot=TunableList(tunable=LootActions.TunableReference(description='\n                            The loot action applied.\n                            ', pack_safe=True))))), 'career_tracks': TunableList(description='\n            List of career tracks for which the UI will indicate this major\n            will provide benefit.  Is not used to actually provide said benefit.\n            ', tunable=TunableReference(description='\n                These are the career tracks that will benefit from this major.\n                ', manager=services.get_instance_manager(sims4.resources.Types.CAREER_TRACK), pack_safe=True), tuning_group=GroupNames.UI, export_modes=ExportModes.ClientBinary, unique_entries=True)}

    @classmethod
    def graduate(cls, sim_info, university, gpa):
        resolver = SingleSimResolver(sim_info)
        if university in cls.graduation_reward:
            for grad_reward in cls.graduation_reward[university]:
                if grad_reward.gpa_range.lower_bound <= gpa < grad_reward.gpa_range.upper_bound:
                    for loot_action in grad_reward.loot:
                        loot_action.apply_to_resolver(resolver)

    @classmethod
    def get_sim_acceptance_score(cls, sim_info):
        resolver = SingleSimResolver(sim_info)
        return cls.acceptance_score.score.get_multiplier(resolver)

    @classmethod
    def can_sim_be_accepted(cls, sim_info):
        sim_score = cls.get_sim_acceptance_score(sim_info)
        return cls.acceptance_score.score_threshold.compare(sim_score)
Ejemplo n.º 14
0
class ScarecrowSituation(ObjectBoundSituationMixin, SituationComplexCommon):
    INSTANCE_TUNABLES = {
        '_situation_job':
        SituationJob.TunableReference(
            description=
            '\n            The situation job for the Sim.\n            \n            This job should define a spawn affordance that will trigger\n            a continuation targeting the object the Sim spawns at.\n            ',
            tuning_group=GroupNames.SITUATION),
        '_do_stuff_state':
        _DoStuffState.TunableFactory(
            description=
            '\n            The state for the Sim doing stuff.\n            \n            This is the initial state after the Sim spawns onto the lot.\n\n            Any on-activate affordances run in this role will target\n            the object the Sim spawned near.\n            ',
            display_name='1. Do Stuff',
            tuning_group=GroupNames.STATE),
        '_leave_state':
        _LeaveState.TunableFactory(
            description=
            '\n            The state for the Sim leaving.\n            \n            Any on-activate affordances run in this role will target\n            the object the Sim spawned near.\n            ',
            display_name='2. Leave',
            tuning_group=GroupNames.STATE),
        '_spawn_object_targeting_affordance':
        TunableInteractionOfInterest(
            description=
            "\n            Affordance that runs targeting the object that the object that the\n            Sim had spawned at. This allows the situation to 'remember' that\n            object and when that object is destroyed, the situation will\n            be destroyed as well. \n            ",
            tuning_group=GroupNames.SITUATION),
        '_spawn_object_reset_loots':
        LootActions.TunableReference(
            description=
            '\n            Loots used to reset the object from which the scarecrow spawned from,\n            to handle cases for when the scarecrow Sim is not on lot during load.\n            ',
            tuning_group=GroupNames.SITUATION)
    }
    REMOVE_INSTANCE_TUNABLES = Situation.NON_USER_FACING_REMOVE_INSTANCE_TUNABLES

    @classmethod
    def _states(cls):
        return (SituationStateData(
            1, _DoStuffState,
            partial(cls._do_stuff_state, situation_job=cls._situation_job)),
                SituationStateData(
                    2, _LeaveState,
                    partial(cls._leave_state,
                            situation_job=cls._situation_job)))

    @classmethod
    def _get_tuned_job_and_default_role_state_tuples(cls):
        return [(cls._situation_job, None)]

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

    @classmethod
    def get_sims_expected_to_be_in_situation(cls):
        return 1

    def _get_role_state_overrides(self, sim, job_type, role_state_type,
                                  role_affordance_target):
        return (role_state_type,
                services.object_manager().get(self.bound_object_id))

    def start_situation(self):
        super().start_situation()
        for custom_key in self._spawn_object_targeting_affordance.custom_keys_gen(
        ):
            self._register_test_event(TestEvent.InteractionStart, custom_key)
        self._change_state(
            self._do_stuff_state(situation_job=self._situation_job))

    def load_situation(self):
        scarecrow_guest_info = next(
            iter(self._guest_list.get_persisted_sim_guest_infos()))
        if scarecrow_guest_info is None:
            self._reset_scarecrow_object()
            return False
        scarecrow_sim_info = services.sim_info_manager().get(
            scarecrow_guest_info.sim_id)
        if scarecrow_sim_info is None or scarecrow_sim_info.zone_id != services.current_zone_id(
        ):
            self._reset_scarecrow_object()
            return False
        return super().load_situation()

    def _reset_scarecrow_object(self):
        scarecrow_object = services.object_manager().get(self._bound_object_id)
        resolver = SingleObjectResolver(scarecrow_object)
        self._spawn_object_reset_loots.apply_to_resolver(resolver)

    def handle_event(self, sim_info, event, resolver):
        if event == TestEvent.InteractionStart:
            if self.is_sim_info_in_situation(sim_info) and resolver(
                    self._spawn_object_targeting_affordance):
                target = resolver.get_participant(ParticipantType.Object)
                if target is None:
                    logger.error(
                        '{}: {} target is None, cannot find the object for this situation to bind to!',
                        self, resolver)
                    self._self_destruct()
                    return
                self.bind_object(target)
        else:
            super().handle_event(sim_info, event, resolver)

    def go_to_leave_state(self):
        self._change_state(
            self._leave_state(situation_job=self._situation_job))

    def _gsi_additional_data_gen(self):
        if isinstance(self._cur_state, _DoStuffState):
            yield ('Time till Leave State',
                   str(self._cur_state.get_time_remaining()))
Ejemplo n.º 15
0
class AuditionDramaNode(BaseDramaNode):
    INSTANCE_TUNABLES = {
        'gig':
        Gig.TunableReference(
            description='\n            Gig this audition is for.\n            '
        ),
        'audition_prep_time':
        TunableTimeSpan(
            description=
            '\n            Amount of time between the seed of the potential audition node\n            to the start of the audition time. \n            ',
            default_hours=5),
        'audition_prep_recommendation':
        TunableLocalizedStringFactory(
            description=
            '\n            String that gives the player more information on how to succeed\n            in this audition.\n            '
        ),
        'audition_prep_icon':
        OptionalTunable(
            description=
            '\n            If enabled, this icon will be displayed with the audition preparation.\n            ',
            tunable=TunableIcon(
                description=
                '\n                Icon for audition preparation.\n                '
            )),
        'audition_outcomes':
        TunableList(
            description=
            '\n            List of loot and multipliers which are for audition outcomes.\n            ',
            tunable=TunableTuple(
                description=
                '\n                The information needed to determine whether or not the sim passes\n                or fails this audition. We cannot rely on the outcome of the \n                interaction because we need to run this test on uninstantiated \n                sims as well. This is similar to the fallback outcomes in \n                interactions.\n                ',
                loot_list=TunableList(
                    description=
                    '\n                    Loot applied if this outcome is chosen\n                    ',
                    tunable=LootActions.TunableReference(pack_safe=True)),
                weight=TunableMultiplier.TunableFactory(
                    description=
                    '\n                    A tunable list of tests and multipliers to apply to the \n                    weight of the outcome.\n                    '
                ),
                is_success=Tunable(
                    description=
                    '\n                    Whether or not this is considered a success outcome.\n                    ',
                    tunable_type=bool,
                    default=False))),
        'audition_rabbit_hole':
        RabbitHole.TunableReference(
            description=
            '\n            Data required to put sim in rabbit hole.\n            '
        ),
        'skip_audition':
        OptionalTunable(
            description=
            '\n            If enabled, we can skip auditions if sim passes tuned tests.\n            ',
            tunable=TunableTuple(
                description=
                '\n                Data related to whether or not this audition can be skipped.\n                ',
                skip_audition_tests=TunableTestSet(
                    description=
                    '\n                    Test to see if sim can skip this audition.\n                    '
                ),
                skipped_audition_loot=TunableList(
                    description=
                    '\n                    Loot applied if sim manages to skip audition\n                    ',
                    tunable=LootActions.TunableReference(pack_safe=True)))),
        'advance_notice_time':
        TunableTimeSpan(
            description=
            '\n            The amount of time between the alert and the start of the event.\n            ',
            default_hours=1,
            locked_args={
                'days': 0,
                'minutes': 0
            }),
        'loot_on_schedule':
        TunableList(
            description=
            '\n            Loot applied if the audition drama node is scheduled successfully.\n            ',
            tunable=LootActions.TunableReference(pack_safe=True)),
        'advance_notice_notification':
        TunableUiDialogNotificationSnippet(
            description=
            '\n            The notification that is displayed at the advance notice time.\n            '
        )
    }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._calculated_audition_time = None
        self._calculated_gig_time = None
        self._rabbit_hole_id = None

    @classproperty
    def drama_node_type(cls):
        return DramaNodeType.AUDITION

    @property
    def _require_instanced_sim(self):
        return False

    @classproperty
    def persist_when_active(cls):
        return True

    def get_picker_schedule_time(self):
        return self._calculated_audition_time

    def create_picker_row(self, owner=None, **kwargs):
        now_time = services.game_clock_service().now()
        min_audition_time = now_time + self.audition_prep_time()
        possible_audition_times = self.get_final_times_based_on_schedule(
            self.min_and_max_times,
            anchor_time=min_audition_time,
            scheduled_time_only=True)
        audition_time = min_audition_time
        if possible_audition_times is not None:
            now = services.time_service().sim_now
            for possible_audition_time in possible_audition_times:
                if possible_audition_time[0] >= now:
                    audition_time = possible_audition_time[0]
                    break
        gig = self.gig
        time_till_gig = gig.get_time_until_next_possible_gig(audition_time)
        if time_till_gig is None:
            return
        gig_time = audition_time + time_till_gig
        if self.skip_audition and self.skip_audition.skip_audition_tests.run_tests(
                SingleSimResolver(owner)):
            formatted_string = Career.GIG_PICKER_SKIPPED_AUDITION_LOCALIZATION_FORMAT(
                gig.gig_pay.lower_bound, gig.gig_pay.upper_bound, gig_time,
                self.audition_prep_recommendation())
        else:
            formatted_string = Career.GIG_PICKER_LOCALIZATION_FORMAT(
                gig.gig_pay.lower_bound, gig.gig_pay.upper_bound,
                audition_time, gig_time, self.audition_prep_recommendation())
        self._calculated_audition_time = audition_time
        self._calculated_gig_time = gig_time
        return gig.create_picker_row(formatted_string, owner)

    def schedule(self,
                 resolver,
                 specific_time=None,
                 time_modifier=TimeSpan.ZERO):
        if self.skip_audition and self.skip_audition.skip_audition_tests.run_tests(
                resolver):
            for loot in self.skip_audition.skipped_audition_loot:
                loot.apply_to_resolver(resolver)
            resolver.sim_info_to_test.career_tracker.set_gig(
                self.gig, self._calculated_gig_time)
            return False
        success = super().schedule(resolver,
                                   specific_time=specific_time,
                                   time_modifier=time_modifier)
        if success:
            services.calendar_service().mark_on_calendar(
                self, advance_notice_time=self.advance_notice_time())
            self._send_career_ui_update(is_add=True)
            for loot in self.loot_on_schedule:
                loot.apply_to_resolver(resolver)
        return success

    def cleanup(self, from_service_stop=False):
        services.calendar_service().remove_on_calendar(self.uid)
        self._send_career_ui_update(is_add=False)
        rabbit_hole_service = services.get_rabbit_hole_service()
        if self._rabbit_hole_id and rabbit_hole_service.is_in_rabbit_hole(
                self._receiver_sim_info.id,
                rabbit_hole_id=self._rabbit_hole_id):
            rabbit_hole_service.remove_rabbit_hole_expiration_callback(
                self._receiver_sim_info.id, self._rabbit_hole_id,
                self._on_sim_return)
        super().cleanup(from_service_stop=from_service_stop)

    def resume(self):
        if self._rabbit_hole_id and not services.get_rabbit_hole_service(
        ).is_in_rabbit_hole(self._receiver_sim_info.id,
                            rabbit_hole_id=self._rabbit_hole_id):
            services.drama_scheduler_service().complete_node(self.uid)

    def _run(self):
        rabbit_hole_service = services.get_rabbit_hole_service()
        self._rabbit_hole_id = rabbit_hole_service.put_sim_in_managed_rabbithole(
            self._receiver_sim_info, self.audition_rabbit_hole)
        if self._rabbit_hole_id is None:
            self._on_sim_return(canceled=True)
        rabbit_hole_service.set_rabbit_hole_expiration_callback(
            self._receiver_sim_info.id, self._rabbit_hole_id,
            self._on_sim_return)
        return DramaNodeRunOutcome.SUCCESS_NODE_INCOMPLETE

    def _on_sim_return(self, canceled=False):
        receiver_sim_info = self._receiver_sim_info
        resolver = SingleSimResolver(receiver_sim_info)
        weights = []
        failure_outcomes = []
        for outcome in self.audition_outcomes:
            if canceled:
                if not outcome.is_success:
                    failure_outcomes.append(outcome)
                    weight = outcome.weight.get_multiplier(resolver)
                    if weight > 0:
                        weights.append((weight, outcome))
            else:
                weight = outcome.weight.get_multiplier(resolver)
                if weight > 0:
                    weights.append((weight, outcome))
        if failure_outcomes:
            selected_outcome = random.choice(failure_outcomes)
        else:
            selected_outcome = sims4.random.weighted_random_item(weights)
        if not selected_outcome:
            logger.error(
                'No valid outcome is tuned on this audition. Verify weights in audition_outcome for {}.',
                self.guid64)
            services.drama_scheduler_service().complete_node(self.uid)
            return
        if selected_outcome.is_success:
            receiver_sim_info.career_tracker.set_gig(self.gig,
                                                     self._calculated_gig_time)
        for loot in selected_outcome.loot_list:
            loot.apply_to_resolver(resolver)
        services.drama_scheduler_service().complete_node(self.uid)

    def _save_custom_data(self, writer):
        if self._calculated_audition_time is not None:
            writer.write_uint64(AUDITION_TIME_TOKEN,
                                self._calculated_audition_time)
        if self._calculated_gig_time is not None:
            writer.write_uint64(GIG_TIME_TOKEN, self._calculated_gig_time)
        if self._rabbit_hole_id is not None:
            writer.write_uint64(RABBIT_HOLE_ID_TOKEN, self._rabbit_hole_id)

    def _load_custom_data(self, reader):
        self._calculated_audition_time = DateAndTime(
            reader.read_uint64(AUDITION_TIME_TOKEN, None))
        self._calculated_gig_time = DateAndTime(
            reader.read_uint64(GIG_TIME_TOKEN, None))
        self._rabbit_hole_id = reader.read_uint64(RABBIT_HOLE_ID_TOKEN, None)
        rabbit_hole_service = services.get_rabbit_hole_service()
        if not self._rabbit_hole_id:
            rabbit_hole_service = services.get_rabbit_hole_service()
            self._rabbit_hole_id = services.get_rabbit_hole_service(
            ).get_rabbit_hole_id_by_type(self._receiver_sim_info.id,
                                         self.audition_rabbit_hole)
        if self._rabbit_hole_id and rabbit_hole_service.is_in_rabbit_hole(
                self._receiver_sim_info.id,
                rabbit_hole_id=self._rabbit_hole_id):
            rabbit_hole_service.set_rabbit_hole_expiration_callback(
                self._receiver_sim_info.id, self._rabbit_hole_id,
                self._on_sim_return)
        self._send_career_ui_update()
        return True

    def _send_career_ui_update(self, is_add=True):
        audition_update_msg = DistributorOps_pb2.AuditionUpdate()
        if is_add:
            self.gig.build_gig_msg(
                audition_update_msg.audition_info,
                self._receiver_sim_info,
                gig_time=self._calculated_gig_time,
                audition_time=self._calculated_audition_time)
        op = GenericProtocolBufferOp(Operation.AUDITION_UPDATE,
                                     audition_update_msg)
        build_icon_info_msg(
            IconInfoData(icon_resource=self.audition_prep_icon),
            self.audition_prep_recommendation(),
            audition_update_msg.recommended_task)
        Distributor.instance().add_op(self._receiver_sim_info, op)

    def load(self, drama_node_proto, schedule_alarm=True):
        super_success = super().load(drama_node_proto,
                                     schedule_alarm=schedule_alarm)
        if not super_success:
            return False
        services.calendar_service().mark_on_calendar(
            self, advance_notice_time=self.advance_notice_time())
        return True

    def on_calendar_alert_alarm(self):
        receiver_sim_info = self._receiver_sim_info
        resolver = SingleSimResolver(receiver_sim_info)
        dialog = self.advance_notice_notification(receiver_sim_info,
                                                  resolver=resolver)
        dialog.show_dialog()
Ejemplo n.º 16
0
class DeliverBabyOnSurgeryTableInteraction(DeliverBabySuperInteraction):
    INSTANCE_TUNABLES = {
        'bassinet_to_use':
        TunableReference(
            description=
            '\n            Bassinet with Baby object definition id.\n            ',
            manager=services.definition_manager()),
        'bassinet_slot_type':
        SlotType.TunableReference(
            description=
            '\n            SlotType used to place the bassinet when it is created.\n            '
        ),
        'surgery_table_participant_type':
        TunableEnumEntry(
            description=
            '\n            A reference to the ParticipantType that the surgery table will be\n            in this interaction.\n            ',
            tunable_type=ParticipantType,
            default=ParticipantType.Object),
        '_loot_per_baby':
        TunableList(
            description=
            '\n            Loot that will be applied when a baby is born to a non NPC Sim. \n            Actor will be the mom and TargetSim will be the sim_info of the baby.\n            This will work for multiple babies as each loot will be applied to \n            each baby sim info.\n            \n            None of these loots will be applied to a baby born to an NPC Sim.\n            ',
            tunable=LootActions.TunableReference(pack_safe=True)),
        'destroy_baby_in_bassinet_vfx':
        PlayEffect.TunableFactory(
            description=
            '\n            The VFX to play when the baby is being destroyed during an NPC \n            birth sequence.\n            '
        ),
        'after_delivery_interaction':
        TunablePackSafeReference(
            description=
            '\n            The interaction to push on the Sim after delivering a baby at the\n            hospital.\n            ',
            manager=services.affordance_manager(),
            allow_none=True)
    }
    FADE_BASSINET_EVENT_TAG = 201
    DESTROY_BASSINET_EVENT_TAG = 202

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.add_exit_function(self._destroy_sim_info_if_neccesary)
        self._bassinet = None
        self._baby_object = None

    def _pre_perform(self, *args, **kwargs):
        bassinet_def = BabyTuning.get_corresponding_definition(
            self.bassinet_to_use)
        self._bassinet = create_object(bassinet_def)
        surgery_table = self.get_participant(
            self.surgery_table_participant_type)
        if surgery_table is None:
            logger.error('No surgery table found for {}', self, owner='tastle')
            return False
        surgery_table.part_owner.slot_object(self.bassinet_slot_type,
                                             self._bassinet)
        return True

    def _build_outcome_sequence(self):
        sequence = super()._build_outcome_sequence()
        return build_element(
            (self._handle_npc_bassinet_fade_and_effect, sequence))

    def _handle_npc_bassinet_fade_and_effect(self, timeline):
        if self.sim.is_npc:

            def _fade_bassinet(_):
                if self._baby_object is not None:
                    self._baby_object.fade_out()
                    effect = self.destroy_baby_in_bassinet_vfx(
                        self._baby_object)
                    effect.start_one_shot()

            self.store_event_handler(_fade_bassinet,
                                     handler_id=self.FADE_BASSINET_EVENT_TAG)

    def _destroy_sim_info_if_neccesary(self):
        if self.sim.is_npc and self._baby_object is not None:
            baby_sim_info = self._baby_object.sim_info
            if self._parent_is_playable(baby_sim_info):
                baby_sim_info.inject_into_inactive_zone(
                    baby_sim_info.household.home_zone_id)
            else:
                baby_sim_info.remove_permanently(
                    household=baby_sim_info.household)

    def _parent_is_playable(self, sim_info):
        for parent_info in sim_info.genealogy.get_parent_sim_infos_gen():
            if parent_info is not None:
                if parent_info.is_player_sim:
                    return True
        return False

    def _complete_pregnancy_gen(self, timeline, pregnancy_tracker):
        offspring_data_list = list(pregnancy_tracker.get_offspring_data_gen())
        new_baby = self._create_new_bassinet_with_baby(pregnancy_tracker,
                                                       offspring_data_list[0],
                                                       self._bassinet)
        self._bassinet.destroy()
        self._bassinet = None
        self._reset_actor_in_asms(new_baby, 'bassinet')
        self.interaction_parameters['created_target_id'] = new_baby.id
        self._baby_object = new_baby
        sim_infos = []
        if not self.sim.is_npc or self._parent_is_playable(new_baby.sim_info):
            sim_infos = self._create_additional_babies(
                pregnancy_tracker,
                offspring_data_list[1:],
                position=new_baby.position,
                routing_surface=new_baby.routing_surface,
                create_bassinet=not self.sim.is_npc)
        sim_infos.append(new_baby.sim_info)
        if not self.sim.is_npc:
            self._apply_per_baby_loot(sim_infos)
            self._apply_inherited_loots(sim_infos, pregnancy_tracker)
            self._push_post_delivery_interaction(sim_infos)
        pregnancy_tracker.complete_pregnancy()
        return True
        yield

    def _push_post_delivery_interaction(self, sim_infos):
        if self._baby_object is not None and self.after_delivery_interaction is not None:

            def _destroy_all_bassinets():
                for sim_info in sim_infos:
                    bassinet_object = services.object_manager().get(
                        sim_info.sim_id)
                    if bassinet_object is not None:
                        bassinet_object.make_transient()

            liabilities = ((
                CLEANUP_INTERACTION_CALLBACK_LIABILITY,
                CleanupInteractionCallbackLiability(
                    cleanup_interaction_callback=_destroy_all_bassinets)), )
            context = self.context.clone_for_continuation(self)
            self.sim.push_super_affordance(self.after_delivery_interaction,
                                           self._baby_object,
                                           context,
                                           liabilities=liabilities)

    def _apply_per_baby_loot(self, sim_infos):
        for sim_info in sim_infos:
            resolver = DoubleSimResolver(self.sim.sim_info, sim_info)
            for loot in self._loot_per_baby:
                loot.apply_to_resolver(resolver)
Ejemplo n.º 17
0
class DeliverBabySuperInteraction(SuperInteraction,
                                  NameOffspringSuperInteractionMixin):
    INSTANCE_TUNABLES = {
        'inherited_loots':
        OptionalTunable(
            description=
            '\n             If enabled, these loots will be applied to the baby if the parents\n             passed the test.\n             ',
            tunable=TunableList(
                description=
                '\n                 List of loot given to the child based on the tests on the parents.\n                 ',
                tunable=TunableTuple(
                    description=
                    '\n                     Tuple of tests on parents to loots given to children.\n                     ',
                    birther_test=TunableTestSet(
                        description=
                        '\n                        Test to run on the sim giving birth. \n                        '
                    ),
                    non_birther_test=TunableTestSet(
                        description=
                        '\n                        Test to run on the sim not giving birth. \n                        '
                    ),
                    child_loot=TunableList(
                        description=
                        '\n                        A list of loots to apply when both parents pass their tests.\n                        Actor = birther sim\n                        Target = baby\n                        ',
                        tunable=LootActions.TunableReference(
                            pack_safe=True)))))
    }

    def _get_name_dialog(self):
        data = PregnancyTuning.get_pregnancy_data(self.sim)
        return data.dialog(self.sim, resolver=SingleSimResolver(self.sim))

    def _build_outcome_sequence(self, *args, **kwargs):
        sequence = super()._build_outcome_sequence(*args, **kwargs)
        pregnancy_tracker = self.sim.sim_info.pregnancy_tracker
        return element_utils.must_run(
            element_utils.build_critical_section_with_finally(
                self._name_and_create_babies_gen, sequence,
                lambda _: pregnancy_tracker.clear_pregnancy()))

    def _name_and_create_babies_gen(self, timeline):
        pregnancy_tracker = self.sim.sim_info.pregnancy_tracker
        if not pregnancy_tracker.is_pregnant:
            return False
            yield
        pregnancy_tracker.create_offspring_data()
        if not self.sim.is_npc:
            result = yield from self._do_renames_gen(
                timeline, list(pregnancy_tracker.get_offspring_data_gen()))
            if not result:
                return result
                yield
        else:
            pregnancy_tracker.assign_random_first_names_to_offspring_data()
        result = yield from self._complete_pregnancy_gen(
            timeline, pregnancy_tracker)
        return result
        yield

    def _reset_actor_in_asms(self, target, actor_name):
        animation_context = self.animation_context
        if animation_context is not None:
            for asm in animation_context.get_asms_gen():
                asm.set_actor(actor_name, None)
                asm.set_actor(actor_name, target)

    def _create_new_bassinet_with_baby(self, pregnancy_tracker, offspring_data,
                                       empty_bassinet):
        sim_info = pregnancy_tracker.create_sim_info(offspring_data)
        new_bassinet = set_baby_sim_info_with_switch_id(
            empty_bassinet, sim_info)
        return new_bassinet

    def _create_additional_babies(self,
                                  pregnancy_tracker,
                                  extra_baby_list,
                                  position=None,
                                  routing_surface=None,
                                  create_bassinet=True):
        created_sim_infos = []
        for offspring_data in extra_baby_list:
            sim_info = pregnancy_tracker.create_sim_info(offspring_data)
            created_sim_infos.append(sim_info)
            if not assign_bassinet_for_baby(sim_info):
                if create_bassinet:
                    create_and_place_baby(sim_info,
                                          position=position,
                                          routing_surface=routing_surface)
        return created_sim_infos

    def _create_non_baby_offspring(self, pregnancy_tracker,
                                   offspring_data_list, target):
        created_sim_infos = []
        for offspring_data in offspring_data_list:
            sim_info = pregnancy_tracker.create_sim_info(offspring_data)
            created_sim_infos.append(sim_info)
            SimSpawner.spawn_sim(sim_info,
                                 target.position,
                                 sim_location=target.location)
        return tuple(created_sim_infos)

    def _handle_show_baby_in_bassinet(self, animation_context, bassinet):
        if animation_context is not None:

            def on_show_baby(_):
                bassinet.enable_baby_state()

            bassinet.empty_baby_state()
            self.store_event_handler(on_show_baby, handler_id=100)

    def _handle_hide_pregnant_sims_belly(self, animation_context,
                                         pregnancy_tracker):
        if animation_context is not None:

            def on_hide_belly(_):
                pregnancy_tracker.clear_pregnancy_visuals()

            self.store_event_handler(on_hide_belly, handler_id=101)

    def _complete_pregnancy_gen(self, timeline, pregnancy_tracker):
        offspring_data_list = list(pregnancy_tracker.get_offspring_data_gen())
        pregnant_sim = pregnancy_tracker._sim_info
        is_bassinet_birth = pregnant_sim.get_birth_age() == Age.BABY
        if is_bassinet_birth:
            new_target = self._create_new_bassinet_with_baby(
                pregnancy_tracker, offspring_data_list[0], self.target)
            self._reset_actor_in_asms(new_target, 'bassinet')
            self.target.transient = True
            self.set_target(new_target)
            self._handle_show_baby_in_bassinet(self.animation_context,
                                               new_target)
            sim_infos = self._create_additional_babies(pregnancy_tracker,
                                                       offspring_data_list[1:])
            sim_infos.append(new_target.sim_info)
        else:
            sim_infos = self._create_non_baby_offspring(
                pregnancy_tracker, offspring_data_list, self.sim)
        self._handle_hide_pregnant_sims_belly(self.animation_context,
                                              pregnancy_tracker)
        if not pregnant_sim.is_npc and self.inherited_loots:
            self._apply_inherited_loots(sim_infos, pregnancy_tracker)
        pregnancy_tracker.complete_pregnancy()
        return True
        yield

    def _apply_inherited_loots(self, sim_infos, pregnancy_tracker):
        inherited_loots = self.inherited_loots
        if not inherited_loots:
            return
        (birther, non_birther) = pregnancy_tracker.get_parents()
        for inherited_loot in inherited_loots:
            if birther and not inherited_loot.birther_test.run_tests(
                    SingleSimResolver(birther)):
                continue
            if non_birther and not inherited_loot.non_birther_test.run_tests(
                    SingleSimResolver(non_birther)):
                continue
            for sim_info in sim_infos:
                resolver = DoubleSimResolver(self.sim.sim_info, sim_info)
                for individual_loot in inherited_loot.child_loot:
                    individual_loot.apply_to_resolver(resolver)
Ejemplo n.º 18
0
class SecurityBouncerSituation(TendObjectSituation):
    INSTANCE_TUNABLES = {
        '_loots_on_create':
        TunableList(
            description=
            '\n            Loot Actions that will be applied to staffed object in this \n            situation on situation create.\n            ',
            tunable=LootActions.TunableReference(),
            tuning_group=GroupNames.SPECIAL_CASES),
        '_loots_on_set_sim_job':
        TunableList(
            description=
            '\n            Loot Actions that will be applied to a sim and staffed object in \n            this situation when the sim has been assigned a job.\n            ',
            tunable=LootActions.TunableReference(),
            tuning_group=GroupNames.SPECIAL_CASES),
        '_loots_on_remove_sim_from_situation':
        TunableList(
            description=
            '\n            Loot Actions that will be applied to a sim and staffed object in \n            this situation when the sim is removed from situation.\n            ',
            tunable=LootActions.TunableReference(),
            tuning_group=GroupNames.SPECIAL_CASES),
        '_loots_on_destroy':
        TunableList(
            description=
            '\n            Loot Actions that will be applied to staff member and staffed \n            object in this situation on situation destroy.\n            ',
            tunable=LootActions.TunableReference(),
            tuning_group=GroupNames.SPECIAL_CASES)
    }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._except_sim_ids = None
        self._persisted_sim_id = None
        self._load_sim_id_exception_lock_data()
        self._apply_loots(self._loots_on_create)

    def _on_set_sim_job(self, sim, job_type):
        super()._on_set_sim_job(sim, job_type)
        self._apply_loots(self._loots_on_set_sim_job)
        if self._persisted_sim_id is not None:
            if self._persisted_sim_id == sim.sim_id:
                self._apply_sim_id_exception_lock_data()
            self._persisted_sim_id = None

    def _on_remove_sim_from_situation(self, sim):
        if sim is self._staff_member:
            self._apply_loots(self._loots_on_remove_sim_from_situation)
        super()._on_remove_sim_from_situation(sim)

    def _destroy(self):
        self._apply_loots(self._loots_on_destroy)
        super()._destroy()

    def _apply_loots(self, loots):
        sim_info = None if self._staff_member is None else self._staff_member.sim_info
        obj = self._staffed_object
        if sim_info is not None:
            if obj is not None:
                resolver = SingleActorAndObjectResolver(sim_info, obj, self)
            else:
                resolver = SingleSimResolver(sim_info)
        elif obj is not None:
            resolver = SingleObjectResolver(obj)
        else:
            return
        for loot_action in loots:
            loot_action.apply_to_resolver(resolver)

    def load_situation(self):
        persisted_guest_info = next(
            iter(self._guest_list.get_persisted_sim_guest_infos()), None)
        if persisted_guest_info is not None:
            self._persisted_sim_id = persisted_guest_info.sim_id
        return super().load_situation()

    def _load_sim_id_exception_lock_data(self):
        reader = self._seed.custom_init_params_reader
        if reader is None:
            return
        self._except_sim_ids = reader.read_uint64s(EXCEPTION_SIM_ID_LIST_TOKEN,
                                                   None)

    def _apply_sim_id_exception_lock_data(self):
        if self._staffed_object is None:
            return
        if not self._except_sim_ids:
            return
        lock_data = LockAllWithSimIdExceptionData(
            lock_priority=LockPriority.PLAYER_LOCK,
            lock_sides=LockSide.LOCK_FRONT,
            should_persist=False,
            except_actor=False,
            except_household=False)
        lock_data.except_sim_ids = self._except_sim_ids
        self._staffed_object.add_lock_data(
            lock_data, clear_existing_locks=ClearLock.CLEAR_NONE)

    def _save_sim_id_exception_lock_data(self, writer):
        if self._staffed_object is None:
            return
        portal_locking_component = self._staffed_object.portal_locking_component
        if portal_locking_component is None:
            return
        lock_datas = portal_locking_component.lock_datas
        if LockType.LOCK_ALL_WITH_SIMID_EXCEPTION not in lock_datas:
            return
        except_sim_ids = lock_datas[
            LockType.LOCK_ALL_WITH_SIMID_EXCEPTION].except_sim_ids
        writer.write_uint64s(EXCEPTION_SIM_ID_LIST_TOKEN, except_sim_ids)

    def _save_custom_situation(self, writer):
        super()._save_custom_situation(writer)
        self._save_sim_id_exception_lock_data(writer)
class ObjectRoutingComponent(FootprintToggleMixin, Component, HasTunableFactory, AutoFactoryInit, component_name=types.OBJECT_ROUTING_COMPONENT):
    FACTORY_TUNABLES = {'routing_behavior_map': TunableMapping(description='\n            A mapping of states to behavior. When the object enters a state, its\n            corresponding routing behavior is started.\n            ', key_type=TunableReference(manager=services.get_instance_manager(sims4.resources.Types.OBJECT_STATE), class_restrictions='ObjectStateValue'), value_type=OptionalTunable(tunable=ObjectRoutingBehavior.TunableReference(), enabled_by_default=True, enabled_name='Start_Behavior', disabled_name='Stop_All_Behavior', disabled_value=UNSET)), 'privacy_rules': OptionalTunable(description='\n            If enabled, this object will care about privacy regions.\n            ', tunable=TunableTuple(description='\n                Privacy rules for this object.\n                ', on_enter=TunableTuple(description='\n                    Tuning for when this object is considered a violator of\n                    privacy.\n                    ', loot_list=TunableList(description='\n                        A list of loot operations to apply when the object\n                        enters a privacy region.\n                        ', tunable=LootActions.TunableReference(pack_safe=True))))), 'tracking_category': TunableEnumEntry(description='\n            Used to classify routing objects for the purpose of putting them\n            into buckets for the object routing service to restrict the number\n            of simultaneously-active objects.\n            ', tunable_type=ObjectRoutingBehaviorTrackingCategory, default=ObjectRoutingBehaviorTrackingCategory.NONE)}

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._running_behavior = None
        self._idle_element = None
        self._previous_parent_ref = None
        self._pending_running_behavior = None
        self._privacy_violations = WeakSet()

    @property
    def previous_parent(self):
        if self._previous_parent_ref is not None:
            return self._previous_parent_ref()

    def _setup(self):
        master_controller = services.get_master_controller()
        master_controller.add_sim(self.owner)
        if self.privacy_rules:
            privacy_service = services.privacy_service()
            privacy_service.add_vehicle_to_monitor(self.owner)
        self.owner.routing_component.on_sim_added()
        self.add_callbacks()

    def on_add(self, *_, **__):
        zone = services.current_zone()
        if not zone.is_zone_loading:
            self._setup()

    def on_finalize_load(self):
        self._setup()

    def on_remove(self):
        self.remove_callbacks()
        self.owner.routing_component.on_sim_removed()
        master_controller = services.get_master_controller()
        master_controller.remove_sim(self.owner)
        if self.privacy_rules:
            privacy_service = services.privacy_service()
            privacy_service.remove_vehicle_to_monitor(self.owner)

    def add_callbacks(self):
        if self.privacy_rules:
            self.owner.register_on_location_changed(self._check_privacy)
        self.register_routing_event_callbacks()

    def remove_callbacks(self):
        if self.owner.is_on_location_changed_callback_registered(self._check_privacy):
            self.owner.unregister_on_location_changed(self._check_privacy)
        self.unregister_routing_event_callbacks()

    def handle_privacy_violation(self, privacy):
        if not self.privacy_rules:
            return
        resolver = SingleObjectResolver(self.owner)
        loots = LootOperationList(resolver, self.privacy_rules.on_enter.loot_list)
        loots.apply_operations()
        if privacy not in self._privacy_violations:
            self._privacy_violations.add(privacy)

    def violates_privacy(self, privacy):
        if not self.privacy_rules:
            return False
        elif not privacy.vehicle_violates_privacy(self.owner):
            return False
        return True

    def _check_privacy(self, _, old_location, new_location):
        if not self.privacy_rules:
            return
        for privacy in services.privacy_service().privacy_instances:
            if not privacy.privacy_violators & PrivacyViolators.VEHICLES:
                continue
            new_violation = privacy not in self._privacy_violations
            violates_privacy = self.violates_privacy(privacy)
            if new_violation:
                if violates_privacy:
                    self.handle_privacy_violation(privacy)
                    if not violates_privacy:
                        self._privacy_violations.discard(privacy)
            elif not violates_privacy:
                self._privacy_violations.discard(privacy)

    def on_state_changed(self, state, old_value, new_value, from_init):
        if new_value is old_value:
            return
        if new_value not in self.routing_behavior_map:
            return
        self._stop_runnning_behavior()
        routing_behavior_type = self.routing_behavior_map[new_value]
        if routing_behavior_type is UNSET:
            return
        routing_behavior = routing_behavior_type(self.owner)
        self._set_running_behavior(routing_behavior)
        self._cancel_idle_behavior()

    def on_location_changed(self, old_location):
        parent = self.owner.parent
        if parent is not None:
            self._previous_parent_ref = parent.ref()

    def component_reset(self, reset_reason):
        if self._running_behavior is not None:
            self._pending_running_behavior = type(self._running_behavior)
            self._running_behavior.trigger_hard_stop()
            self._set_running_behavior(None)
        services.get_master_controller().on_reset_sim(self.owner, reset_reason)

    def post_component_reset(self):
        if self._pending_running_behavior is not None:
            routing_behavior = self._pending_running_behavior(self.owner)
            self._set_running_behavior(routing_behavior)
            self._pending_running_behavior = None
            self._cancel_idle_behavior()

    def _cancel_idle_behavior(self):
        if self._idle_element is not None:
            self._idle_element.trigger_soft_stop()
            self._idle_element = None

    def _set_running_behavior(self, new_behavior):
        if new_behavior == self._running_behavior:
            return
        self._running_behavior = new_behavior
        if self.tracking_category and self.tracking_category is not ObjectRoutingBehaviorTrackingCategory.NONE:
            routing_service = services.get_object_routing_service()
            if routing_service:
                if new_behavior:
                    routing_service.on_routing_start(self.owner, self.tracking_category, new_behavior)
                else:
                    routing_service.on_routing_stop(self.owner, self.tracking_category)

    @componentmethod
    def get_idle_element(self):
        self._idle_element = soft_sleep_forever()
        return (self._idle_element, self._cancel_idle_behavior)

    @componentmethod
    def get_next_work(self):
        if self._running_behavior is None or self.owner.has_work_locks:
            return WorkRequest()
        work_request = WorkRequest(work_element=self._running_behavior, required_sims=(self.owner,))
        return work_request

    @componentmethod
    def get_next_work_priority(self):
        return PriorityExtended.SubLow

    @componentmethod
    def on_requested_as_resource(self, other_work):
        if not any(resource.is_sim for resource in other_work.resources):
            return
        self.restart_running_behavior()

    def restart_running_behavior(self):
        routing_behavior_type = type(self._running_behavior) if self._running_behavior is not None else None
        self._stop_runnning_behavior()
        if routing_behavior_type is not None:
            routing_behavior = routing_behavior_type(self.owner)
            self._set_running_behavior(routing_behavior)

    def _stop_runnning_behavior(self):
        if self._running_behavior is not None:
            self._running_behavior.trigger_soft_stop()
            self._set_running_behavior(None)

    @componentmethod
    def get_participant(self, participant_type=ParticipantType.Actor, **kwargs):
        participants = self.get_participants(participant_type=participant_type, **kwargs)
        if not participants:
            return
        if len(participants) > 1:
            raise ValueError('Too many participants returned for {}!'.format(participant_type))
        return next(iter(participants))

    @componentmethod
    def get_participants(self, participant_type, **kwargs):
        if participant_type is ParticipantType.Actor:
            obj = self._running_behavior._obj if self._running_behavior else None
            return (obj,)
        elif participant_type is ParticipantType.Object:
            target = self._running_behavior.get_target() if self._running_behavior else None
            return (target,)
Ejemplo n.º 20
0
class TempleTuning:
    TEMPLES = TunableMapping(
        description=
        '\n        A Mapping of Temple Templates (house descriptions) and the data\n        associated with each temple.\n        ',
        key_name='Template House Description',
        key_type=TunableHouseDescription(pack_safe=True),
        value_name='Temple Data',
        value_type=TunableTuple(
            description=
            '\n            The data associated with the mapped temple template.\n            ',
            rooms=TunableMapping(
                description=
                '\n                A mapping of room number to the room data. Room number 0 will be\n                the entrance room to the temple, room 1 will be the first room\n                that needs to be unlocked, and so on.\n                ',
                key_name='Room Number',
                key_type=int,
                value_name='Room Data',
                value_type=TunableTempleRoomData(pack_safe=True)),
            enter_lot_loot=TunableSet(
                description=
                '\n                Loot applied to Sims when they enter or spawn in to this Temple.\n                \n                NOTE: Exit Lot Loot is not guaranteed to be given. For example,\n                if the Sim walks onto the lot, player switches to a different\n                zone, then summons that Sim, that Sim will bypass getting the\n                exit loot.\n                ',
                tunable=LootActions.TunableReference(pack_safe=True)),
            exit_lot_loot=TunableSet(
                description=
                '\n                Loot applied to Sims when they exit or spawn out of this Temple.\n                \n                NOTE: This loot is not guaranteed to be given after Enter Lot\n                Loot. For example, if the Sim walks onto the lot, player\n                switches to a different zone, then summons that Sim, that Sim\n                will bypass getting the exit loot.\n                ',
                tunable=LootActions.TunableReference(pack_safe=True))))
    GATE_TAG = TunableTag(
        description=
        '\n        The tag used to find the gate objects inside Temples.\n        ',
        filter_prefixes=('func_temple', ))
    TRAP_TAG = TunableTag(
        description=
        '\n        The tag used to identify traps inside temples. This will be used to find\n        placeholder traps as well.\n        ',
        filter_prefixes=('func_temple', ))
    CHEST_TAG = TunableTag(
        description=
        "\n        The tag used to identify the final chest of a temple. If it's in the\n        open state, the temple will be considered solved.\n        ",
        filter_prefixes=('func_temple', ))
    CHEST_OPEN_STATE = TunablePackSafeReference(
        description=
        '\n        The state that indicates the chest is open.\n        ',
        manager=services.get_instance_manager(
            sims4.resources.Types.OBJECT_STATE),
        class_restrictions='ObjectStateValue')
    GATE_STATE = TunablePackSafeReference(
        description=
        '\n        The state for temple gates. Used for easy look up.\n        ',
        manager=services.get_instance_manager(
            sims4.resources.Types.OBJECT_STATE),
        class_restrictions='ObjectState')
    GATE_UNLOCK_STATE = TunablePackSafeReference(
        description='\n        The unlock state for temple gates.\n        ',
        manager=services.get_instance_manager(
            sims4.resources.Types.OBJECT_STATE),
        class_restrictions='ObjectStateValue')
    TEMPLE_LOT_DESCRIPTION = TunablePackSafeLotDescription(
        description=
        '\n        A reference to the lot description file for the temple lot. This is used\n        for easier zone ID lookups.\n        '
    )
    GATE_LOCK_LOOT = LockDoor.TunableFactory(
        description=
        '\n        The LockDoor loot to run on the gates in the temple on load when they\n        should be locked.\n        '
    )
    GATE_UNLOCK_LOOT = UnlockDoor.TunableFactory(
        description=
        '\n        The UnlockDoor loot to run on the gates when they should be unlocked.\n        '
    )
    CHEST_OPEN_INTEARCTION = TunableInteractionOfInterest(
        description=
        '\n        A reference to the open interaction for chests. This interaction will be\n        listened for to determine temple completion.\n        '
    )
Ejemplo n.º 21
0
class RankedStatistic(
        HasTunableReference,
        ProgressiveStatisticCallbackMixin,
        statistics.continuous_statistic_tuning.TunedContinuousStatistic,
        metaclass=HashedTunedInstanceMetaclass,
        manager=services.get_instance_manager(sims4.resources.Types.STATISTIC)
):
    @classmethod
    def _verify_tuning_callback(cls):
        super()._verify_tuning_callback()
        ranks_tuned = [
            level_data for level_data in cls.event_data.values()
            if level_data.rank_up
        ]
        ranks_needed = len(ranks_tuned) + 1
        actual_ranks = len(cls.rank_tuning)
        tuned_rank_up_notifications = len(cls.rank_up_notification_tuning)
        tuned_rank_down_notifications = len(cls.rank_down_notification_tuning)
        if actual_ranks != ranks_needed:
            logger.error(
                '{} ranks have been enabled, but there is tuning for {} ranks in the rank_tuning. Please double check the tuning for {}',
                ranks_needed, actual_ranks, cls)
        if actual_ranks != tuned_rank_up_notifications:
            logger.error(
                'There are {} ranks tuned but {} rank up notifications tuned. These need to be the same. Please double check the tuning for {}',
                actual_ranks, tuned_rank_up_notifications, cls)
        if tuned_rank_down_notifications > 0 and actual_ranks != tuned_rank_down_notifications:
            logger.error(
                'There are {} ranks tuned but {} rank down notifications tuned. These need to be the same. Please double check the tuning for {}',
                actual_ranks, tuned_rank_down_notifications, cls)

    INSTANCE_TUNABLES = {
        'stat_name':
        TunableLocalizedString(
            description=
            '\n            Localized name of this statistic.\n            ',
            allow_none=True),
        'event_intervals':
        TunableList(
            description=
            '\n            The level boundaries for an event, specified as a delta from the\n            previous value.\n            ',
            tunable=Tunable(
                description=
                '\n                Points required to reach this level.\n                ',
                tunable_type=int,
                default=0),
            export_modes=ExportModes.All),
        'event_data':
        TunableMapping(
            description=
            '\n            The data associated with a specific tuned event. \n            \n            The Key is the event number as tuned in the event intervals.\n            \n            The value is a list of loots to apply when the event occurs and an\n            bool for whether or not to rank up the stat. \n            ',
            key_type=int,
            value_type=TunableTuple(
                description=
                '\n                The data associated with a tuned event from event_intervals.\n                ',
                rank_up=Tunable(
                    description=
                    "\n                    If checked then this event will cause the statistic to rank\n                    up and all that entails. Currently that will increment\n                    the rank count.\n                    \n                    There should be a rank up entry for each of the levels \n                    tuned, except the initial rank. We assume that you don't \n                    need to rank into the initial rank. This means you will \n                    need one more level tuned than number of rank up events\n                    found in this list.\n                    ",
                    tunable_type=bool,
                    default=False),
                loot=TunableList(
                    description=
                    '\n                    A list of loots to apply when this event happens. This loot\n                    is only applied the first time you reach a specific level.\n                    If you want the loot applied every time you reach a level\n                    (for instance after you decay to a previous level and then\n                    regain a level) please use the loot_always tuning.\n                    ',
                    tunable=TunableReference(
                        description=
                        '\n                        The loot to apply.\n                        ',
                        manager=services.get_instance_manager(
                            sims4.resources.Types.ACTION),
                        class_restrictions=('LootActions',
                                            'RandomWeightedLoot'),
                        pack_safe=True)),
                tooltip=TunableLocalizedStringFactory(
                    description=
                    '\n                    The tooltip to display in the UI for each of the event\n                    lines. This is to be used for telling the user what loot \n                    they are going to get at an individual event.\n                    '
                ),
                level_down_loot=TunableList(
                    description=
                    '\n                    A list of loots to apply when the Sim loses enough points \n                    to level down.\n                    ',
                    tunable=LootActions.TunableReference(pack_safe=True)),
                tests=event_testing.tests.TunableTestSet(
                    description=
                    "\n                    Tests to run when reaching this level. If the tests don't \n                    pass then the value will be set back to min points for \n                    the rank before it. This means that the Sim won't be able\n                    to make any progress towards the rank with the failed\n                    tests.\n                    ",
                    export_modes=ExportModes.ServerXML),
                loot_always=TunableList(
                    description=
                    '\n                    This loot is always awarded on level up, regardless of \n                    whether or not this level has already been achieved or not.\n                    \n                    If you want the loot to only be applied the first time you\n                    reach a certain level then please use the loot tuning.\n                    ',
                    tunable=TunableReference(
                        description=
                        '\n                        The loot to award on level up.\n                        ',
                        manager=services.get_instance_manager(
                            sims4.resources.Types.ACTION),
                        class_restrictions=('LootActions',
                                            'RandomWeightedLoot'),
                        pack_safe=True)),
                loot_always_on_load=TunableList(
                    description=
                    '\n                    This loot is always awarded when a sim loads with this\n                    level.\n                    ',
                    tunable=LootActions.TunableReference(pack_safe=True)),
                export_class_name='EventDataTuple'),
            tuple_name='TunableEventData',
            export_modes=ExportModes.All),
        'initial_rank':
        Tunable(description=
                '\n            The initial rank for this stat.\n            ',
                tunable_type=int,
                default=1,
                export_modes=ExportModes.All),
        'rank_tuning':
        TunableMapping(
            description=
            '\n            This is the tuning that is associated with a specific rank level \n            instead of each individual event level. When the rank has increased \n            the matching information will be retrieved from here and used.\n            \n            There needs to be an equal number of ranks tuned to match all of \n            the rank up events in event data plus an extra one for the \n            rank you start out on initially.\n            ',
            key_type=int,
            value_type=TunableTuple(
                description=
                '\n                A tuple of all the data for each Rank associated wit this\n                ranked statistic.\n                ',
                rank_name=TunableLocalizedString(
                    description=
                    "\n                    The rank's normal name.\n                    "
                ),
                icon=OptionalTunable(
                    description=
                    '\n                    If enabled then the Rank Statistic will have an icon \n                    associated with this Rank.\n                    ',
                    tunable=TunableResourceKey(
                        description=
                        '\n                        Icon to be displayed for the rank.\n                        ',
                        resource_types=sims4.resources.CompoundTypes.IMAGE),
                    enabled_by_default=True),
                rank_description=OptionalTunable(
                    description=
                    '\n                    When enabled this string will be used as the description\n                    for the rank.\n                    ',
                    tunable=TunableLocalizedString(
                        description=
                        "\n                        The rank's description.\n                        "
                    )),
                rank_short_name=OptionalTunable(
                    description=
                    '\n                    When enabled this string will be used as an alternate \n                    short name for the rank.\n                    ',
                    tunable=TunableLocalizedString(
                        description=
                        "\n                        The rank's short name.\n                        "
                    )),
                hide_in_ui=Tunable(
                    description=
                    '\n                    If checked, this rank will not be shown in some places in the UI (XP bars, Relationship tooltip, Gallery)\n                    ',
                    tunable_type=bool,
                    default=False),
                export_class_name='RankDataTuple'),
            tuple_name='TunableRankData',
            export_modes=ExportModes.All),
        'rank_down_notification_tuning':
        TunableMapping(
            description=
            '\n            A mapping of Rank to tuning needed to display all the notifications\n            when a Sim ranks down. \n            \n            The number of notifications tuned must match the number of items\n            in rank_tuning.\n            ',
            key_type=int,
            value_type=TunableTuple(
                description=
                '\n                A Tuple containing both the rank down screen slam and the rank\n                down notification to display.\n                ',
                show_notification_tests=event_testing.tests.TunableTestSet(
                    description=
                    '\n                    Tests that must be true when the we want to show notification.\n                    '
                ),
                rank_down_screen_slam=OptionalTunable(
                    description=
                    '\n                    Screen slam to show when Sim goes down to this rank level.\n                    Localization Tokens: Sim - {0.SimFirstName}, Rank Name - \n                    {1.String}, Rank Number - {2.Number}\n                    ',
                    tunable=ui.screen_slam.TunableScreenSlamSnippet()),
                rank_down_notification=OptionalTunable(
                    description=
                    '\n                    The notification to display when the Sim obtains this\n                    rank. The text will be provided two tokens: the Sim owning\n                    the stat and a number representing the 1-based rank\n                    level.\n                    ',
                    tunable=UiDialogNotification.TunableFactory(
                        locked_args={
                            'text_tokens': DEFAULT,
                            'icon': None,
                            'secondary_icon': None
                        })))),
        'rank_up_notification_tuning':
        TunableMapping(
            description=
            '\n            A mapping of Rank to tuning needed to display all the notifications\n            when a Sim ranks up. \n            \n            The number of notifications tuned must match the number of items\n            in rank_tuning.\n            ',
            key_type=int,
            value_type=TunableTuple(
                description=
                '\n                A Tuple containing both the rank up screen slam and the rank\n                up notification to display.\n                ',
                show_notification_tests=event_testing.tests.TunableTestSet(
                    description=
                    '\n                    Tests that must be true when the we want to show notification.\n                    '
                ),
                rank_up_screen_slam=OptionalTunable(
                    description=
                    '\n                    Screen slam to show when reaches this rank level.\n                    Localization Tokens: Sim - {0.SimFirstName}, Rank Name - \n                    {1.String}, Rank Number - {2.Number}\n                    \n                    This will only happen the first time a rank is reached.\n                    ',
                    tunable=ui.screen_slam.TunableScreenSlamSnippet()),
                rank_up_notification=OptionalTunable(
                    description=
                    '\n                    The notification to display when the Sim obtains this\n                    rank. The text will be provided two tokens: the Sim owning\n                    the stat and a number representing the 1-based rank\n                    level.\n                    \n                    This will only happen the first time a rank is reached. If\n                    you want to show a display on subsequent rank ups you can \n                    tune the re_rank_up_notifcation.\n                    ',
                    tunable=UiDialogNotification.TunableFactory(
                        locked_args={
                            'text_tokens': DEFAULT,
                            'icon': None,
                            'secondary_icon': None
                        })),
                re_rank_up_notification=OptionalTunable(
                    description=
                    '\n                    The notification to display when the Sim obtains this rank\n                    every time other than the first time. For instance if the\n                    Sim achieves rank 3, drops down to rank 2 because of decay,\n                    and then re-achieves rank 3, that is when this dialog will\n                    be displayed.\n                    \n                    If you want this dialog to be displayed the first time the\n                    Sim reaches a rank please tune rank_up_notification instead.\n                    ',
                    tunable=UiDialogNotification.TunableFactory(
                        locked_args={
                            'text_tokens': DEFAULT,
                            'icon': None,
                            'secondary_icon': None
                        })))),
        'tags':
        TunableList(
            description=
            '\n            The associated categories of the ranked statistic.\n            ',
            tunable=TunableEnumEntry(tunable_type=tag.Tag,
                                     default=tag.Tag.INVALID,
                                     pack_safe=True)),
        'icon':
        TunableIcon(
            description="\n            The ranked stat's icon.\n            ",
            allow_none=True),
        'initial_loot':
        TunableList(
            description=
            '\n            A list of loots to apply when the Sim first receives this ranked\n            statistic.\n            ',
            tunable=LootActions.TunableReference(pack_safe=True)),
        'min_decay_per_highest_level_achieved':
        TunableMapping(
            description=
            '\n            A mapping of highest level reached to the absolute minimum \n            that this Ranked Stat is allowed to decay to in ranks.\n            ',
            key_type=int,
            value_type=TunableRange(
                description=
                '\n                The lowest level this stat can decay to based on the associated\n                highest level reached.\n                ',
                tunable_type=int,
                minimum=1,
                default=1)),
        'associated_bucks_types':
        TunableList(
            description=
            '\n            A list of bucks types that are associated with this ranked stat.\n            These bucks types may have tuned data that is affected by ranking\n            up/down.\n            ',
            tunable=TunableEnumEntry(
                description=
                '\n                A buck type that is associated with this ranked stat.\n                ',
                tunable_type=BucksType,
                default=BucksType.INVALID),
            unique_entries=True,
            export_modes=ExportModes.All),
        'zero_out_on_lock':
        Tunable(
            description=
            '\n            If checked, when this ranked stat is locked it will zero out\n            the value, highest_level, and bucks.\n            ',
            tunable_type=bool,
            default=True),
        'headline':
        OptionalTunable(
            description=
            '\n            If enabled when this relationship track updates we will display\n            a headline update to the UI.\n            ',
            tunable=TunableReference(
                description=
                '\n                The headline that we want to send down.\n                ',
                manager=services.get_instance_manager(
                    sims4.resources.Types.HEADLINE)),
            tuning_group=GroupNames.UI),
        'send_stat_update_for_npcs':
        Tunable(
            description=
            "\n            If checked then whenever we attempt to send the ranked stat update\n            message it will be sent, even if the Sim is an NPC.\n            \n            NOTE: We don't want to mark very many of the stats like this. This \n            is being done to make sure that Fame gets sent so we don't have\n            to request Fame when building the tooltip for sims which could be\n            really slow.\n            ",
            tunable_type=bool,
            default=False),
        'center_bar_tooltip':
        Tunable(
            description=
            '\n            If true, always put motive panel ranked stat bar tooltip at the center.\n            If false, put tooltip on each increment mark instead.\n            ',
            tunable_type=bool,
            default=False,
            export_modes=ExportModes.All),
        'visible':
        Tunable(
            description=
            '\n            Whether or not statistic should be sent to client.\n            \n            NOTE: Please work with your UI engineering partner to determine if this \n            should be True. If False, for performance reasons, \n            the stat will be removed from the sim if their\n            current value matches the default convergence value. \n            ',
            tunable_type=bool,
            default=False,
            export_modes=ExportModes.All)
    }
    REMOVE_INSTANCE_TUNABLES = ('min_value_tuning', 'max_value_tuning')

    def __init__(self, tracker):
        self._rank_level = self.initial_rank
        self.highest_level = 0
        super().__init__(tracker, self.initial_value)
        self._current_event_level = 0
        self.previous_event_level = 0
        self._notifications_disabled = False
        self._initial_loots_awarded = False
        self._suppress_level_up_telemetry = False

    @constproperty
    def is_ranked():
        return True

    @property
    def rank_level(self):
        return self._rank_level

    @property
    def process_non_selectable_sim(self):
        return True

    @rank_level.setter
    def rank_level(self, value):
        self._rank_level = value
        services.get_event_manager().process_event(
            TestEvent.RankedStatisticChange,
            sim_info=self.tracker.owner.sim_info)

    @property
    def highest_rank_achieved(self):
        rank_level = self.initial_rank
        for i in range(1, self.highest_level + 1):
            if self.event_data.get(i).rank_up:
                rank_level += 1
        return rank_level

    @property
    def is_visible(self):
        if self.tracker is None or not self.tracker.owner.is_sim:
            return False
        return self.visible

    @property
    def instance_required(self):
        if self.is_visible:
            return True
        return super().instance_required

    def increase_rank_level(self, new_rank=True, from_add=False):
        self.rank_level += 1
        self._on_rank_up(new_rank=new_rank, from_add=from_add)

    def increase_rank_levels(self, levels):
        start_level = self.rank_level
        self.rank_level = start_level + levels
        self.send_rank_change_update_message(start_level, start_level + levels)

    def decrease_rank_level(self):
        self.rank_level = max(self.rank_level - 1, 0)
        self._on_rank_down()

    def _on_rank_up(self, new_rank=True, from_add=False):
        current_rank = self.rank_level
        self.send_rank_change_update_message(current_rank - 1, current_rank)
        sim_info = self.tracker.owner.sim_info
        rank_data = self.rank_tuning.get(current_rank)
        rank_up_data = self.rank_up_notification_tuning.get(current_rank)
        if rank_data is None:
            logger.error(
                'Sim {}: {} is trying to rank up to level {} but there is no rank tuning.',
                sim_info, self, current_rank)
            return
        if not from_add and (sim_info.is_selectable
                             and rank_up_data is not None
                             ) and self.can_show_notification(rank_up_data):
            icon_override = None if rank_data.icon is None else IconInfoData(
                icon_resource=rank_data.icon)
            if new_rank:
                self._show_initial_rank_up_notifications(
                    sim_info, current_rank, rank_data, rank_up_data,
                    icon_override)
            else:
                self._show_re_rank_up_notifications(sim_info, current_rank,
                                                    rank_data, rank_up_data,
                                                    icon_override)

    def _show_initial_rank_up_notifications(self, sim_info, current_rank,
                                            rank_data, rank_up_data,
                                            icon_override):
        if rank_up_data.rank_up_notification is not None:
            notification = rank_up_data.rank_up_notification(
                sim_info, resolver=SingleSimResolver(sim_info))
            notification.show_dialog(
                icon_override=icon_override,
                secondary_icon_override=IconInfoData(obj_instance=sim_info),
                additional_tokens=(current_rank, ))
        if rank_up_data.rank_up_screen_slam is not None:
            rank_up_data.rank_up_screen_slam.send_screen_slam_message(
                sim_info, sim_info, rank_data.rank_name, current_rank)

    def _show_re_rank_up_notifications(self, sim_info, current_rank, rank_data,
                                       rank_up_data, icon_override):
        if rank_up_data.re_rank_up_notification is not None:
            notification = rank_up_data.re_rank_up_notification(
                sim_info, resolver=SingleSimResolver(sim_info))
            notification.show_dialog(
                icon_override=icon_override,
                secondary_icon_override=IconInfoData(obj_instance=sim_info),
                additional_tokens=(current_rank, ))

    def _on_rank_down(self):
        current_rank = self.rank_level
        self.send_rank_change_update_message(current_rank + 1, current_rank)
        sim_info = self.tracker.owner.sim_info
        rank_data = self.rank_tuning.get(current_rank)
        rank_down_data = self.rank_down_notification_tuning.get(current_rank)
        if rank_data is None:
            logger.error(
                'Sim {}: {} is trying to rank down to level {} but there is no rank tuning.',
                sim_info, self, current_rank)
            return
        if self.can_show_notification(rank_down_data):
            if rank_down_data.rank_down_notification is not None:
                notification = rank_down_data.rank_down_notification(
                    sim_info, resolver=SingleSimResolver(sim_info))
                icon_override = None if rank_data.icon is None else IconInfoData(
                    icon_resource=rank_data.icon)
                notification.show_dialog(icon_override=icon_override,
                                         secondary_icon_override=IconInfoData(
                                             obj_instance=sim_info),
                                         additional_tokens=(current_rank, ))
            if rank_down_data.rank_down_screen_slam is not None:
                rank_down_data.rank_down_screen_slam.send_screen_slam_message(
                    sim_info, sim_info, rank_data.rank_name, current_rank)
        for bucks_type in self.associated_bucks_types:
            bucks_tracker = BucksUtils.get_tracker_for_bucks_type(
                bucks_type, owner_id=self.tracker.owner.id)
            bucks_tracker.validate_perks(bucks_type, self.rank_level)

    def on_add(self):
        super().on_add()
        self.tracker.owner.sim_info.on_add_ranked_statistic()
        self.on_stat_event(self.highest_level,
                           self.get_user_value(),
                           from_add=True)
        self.previous_event_level = self.get_user_value()
        if self.tracker.owner.is_simulating:
            self.apply_initial_loot()

    @classmethod
    def get_level_list(cls):
        return list(cls.event_intervals)

    def on_initial_startup(self):
        super().on_initial_startup()
        self.decay_enabled = self.tracker.owner.is_selectable and not self.tracker.owner.is_locked(
            self)

    @staticmethod
    def _callback_handler(stat_inst):
        new_level = stat_inst.get_user_value()
        stat_inst.on_stat_event(stat_inst.previous_event_level, new_level)
        stat_inst.previous_event_level = new_level
        stat_inst.refresh_threshold_callback()

    def on_stat_event(self, old_level, new_level, from_add=False):
        batch_rank_levels = 0
        while old_level < new_level:
            old_level += 1
            event_data = self.event_data.get(old_level)
            if event_data is not None:
                if self.tracker.owner.is_simulating:
                    resolver = SingleSimResolver(self.tracker.owner)
                    is_new_level = old_level > self.highest_level
                    if is_new_level:
                        for loot in event_data.loot:
                            loot.apply_to_resolver(resolver)
                        self.highest_level = old_level
                    if event_data.rank_up:
                        self.increase_rank_level(new_rank=is_new_level,
                                                 from_add=from_add)
                    for loot in event_data.loot_always:
                        loot.apply_to_resolver(resolver)
                elif event_data.rank_up:
                    batch_rank_levels += 1
            if self.tracker.owner.is_npc:
                if not from_add:
                    self._handle_level_up_telemetry(old_level)
            self._handle_level_up_telemetry(old_level)
        if batch_rank_levels > 0:
            self.increase_rank_levels(batch_rank_levels)
        else:
            self.create_and_send_commodity_update_msg(is_rate_change=False)

    @contextlib.contextmanager
    def suppress_level_up_telemetry(self):
        if self._suppress_level_up_telemetry:
            yield None
        else:
            self._suppress_level_up_telemetry = True
            try:
                yield None
            finally:
                self._suppress_level_up_telemetry = False

    def _handle_level_up_telemetry(self, level):
        if not self._suppress_level_up_telemetry:
            with telemetry_helper.begin_hook(
                    ranked_stat_telemetry_writer,
                    TELEMETRY_HOOK_RANKED_STAT_LEVEL_UP) as hook:
                hook.write_guid(TELEMETRY_FIELD_RANKED_STAT_TYPE, self.guid64)
                hook.write_int(TELEMETRY_FIELD_RANKED_STAT_LEVEL, level)

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

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

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

    @sims4.utils.classproperty
    def max_rank(cls):
        (_, rank) = cls.calculate_level_and_rank(cls.max_value)
        return rank

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

    def can_show_notification(self, rank_data):
        if self._notifications_disabled:
            return False
        elif rank_data is not None and rank_data.show_notification_tests is not None:
            resolver = event_testing.resolver.SingleSimResolver(
                self.tracker.owner)
            result = rank_data.show_notification_tests.run_tests(resolver)
            if not result:
                return False
        return True

    def set_value(self,
                  value,
                  *args,
                  from_load=False,
                  interaction=None,
                  **kwargs):
        old_points = self.get_value()
        old_user_value = self.get_user_value()
        value_to_set = value
        if not from_load:
            value_to_set = self._get_valid_value(value, old_user_value)
        minimum_level = self._get_minimum_decay_level()
        value_to_set = max(value_to_set, minimum_level)
        super().set_value(value_to_set,
                          *args,
                          from_load=from_load,
                          interaction=interaction,
                          **kwargs)
        new_user_value = self.get_user_value()
        if not from_load:
            self._handle_level_down(old_user_value, new_user_value)
            sim_info = self._tracker._owner
            new_points = self.get_value()
            stat_type = self.stat_type
            if old_points == self.initial_value or old_points != new_points:
                services.get_event_manager().process_event(
                    TestEvent.StatValueUpdate,
                    sim_info=sim_info,
                    statistic=stat_type,
                    custom_keys=(stat_type, ))
        self.send_commodity_progress_msg(is_rate_change=False)
        self.send_change_update_message(value - old_points,
                                        from_load=from_load)
        self.previous_event_level = new_user_value
        self.refresh_threshold_callback()

    def _update_value(self):
        minimum_decay = self._get_minimum_decay_level()
        old_value = self._value
        old_user_value = self.convert_to_user_value(self._value)
        super()._update_value(minimum_decay_value=minimum_decay)
        new_value = self._value
        new_user_value = self.convert_to_user_value(self._value)
        self._handle_level_down(old_user_value, new_user_value)
        if old_user_value > new_user_value:
            self.previous_event_level = new_user_value
            self.refresh_threshold_callback()
        stat_type = self.stat_type
        if new_value > old_value:
            sim_info = self._tracker._owner if self._tracker is not None else None
            services.get_event_manager().process_event(
                TestEvent.StatValueUpdate,
                sim_info=sim_info,
                statistic=stat_type,
                custom_keys=(stat_type, ))

    def _get_minimum_decay_level(self):
        min_rank = self.min_decay_per_highest_level_achieved.get(
            self.highest_level, None)
        if min_rank is None:
            return 0
        points = self.points_to_rank(min_rank)
        return points

    def _handle_level_down(self, old_value, new_value):
        while new_value < old_value:
            event_data = self.event_data.get(old_value)
            if event_data is not None:
                resolver = SingleSimResolver(self.tracker.owner)
                for loot in event_data.level_down_loot:
                    loot.apply_to_resolver(resolver)
                if event_data.rank_up:
                    self.decrease_rank_level()
            old_value -= 1

    def get_next_rank_level(self):
        current_value = self.get_user_value()
        index = current_value + 1
        if index > len(self.event_data):
            return current_value
        while not self.event_data[index].rank_up:
            if index == len(self.event_data):
                break
            index += 1
        return index

    @constproperty
    def remove_on_convergence():
        return False

    def send_commodity_progress_msg(self, is_rate_change=True):
        self.create_and_send_commodity_update_msg(
            is_rate_change=is_rate_change)

    @classmethod
    def points_to_level(cls, event_level):
        level = 0
        running_sum = 0
        level_list = cls.get_level_list()
        while level < len(level_list):
            while level < event_level:
                running_sum += level_list[level]
                level += 1
        return running_sum

    @classmethod
    def points_to_rank(cls, rank_level):
        rank = cls.initial_rank
        level = 0
        running_sum = 0
        level_list = cls.get_level_list()
        while rank < rank_level:
            while level < len(level_list):
                event_data = cls.event_data.get(level)
                if event_data is not None:
                    if cls.event_data[level].rank_up:
                        rank += 1
                if rank < rank_level:
                    running_sum += level_list[level]
                level += 1
        return running_sum

    def points_to_current_rank(self):
        return self.points_to_rank(self.rank_level)

    def create_and_send_commodity_update_msg(self,
                                             is_rate_change=True,
                                             allow_npc=False,
                                             from_add=False):
        ranked_stat_msg = Commodities_pb2.RankedStatisticProgressUpdate()
        ranked_stat_msg.stat_id = self.guid64
        ranked_stat_msg.change_rate = self.get_change_rate()
        ranked_stat_msg.rank = self.rank_level
        difference = self.get_value() - self.points_to_current_rank()
        ranked_stat_msg.curr_rank_points = int(
            difference) if difference > 0 else 0
        send_sim_ranked_stat_update_message(self.tracker.owner,
                                            ranked_stat_msg,
                                            allow_npc=allow_npc
                                            or self.send_stat_update_for_npcs)

    @classmethod
    def send_commodity_update_message(cls, sim_info, old_value, new_value):
        commodity_tracker = sim_info.commodity_tracker
        if commodity_tracker is None:
            return
        stat_instance = commodity_tracker.get_statistic(cls)
        if stat_instance is None:
            return
        stat_instance.create_and_send_commodity_update_msg(is_rate_change=True)

    def send_change_update_message(self, amount, from_load=False):
        if from_load:
            return
        if self.headline is None:
            return
        sim = self.tracker.owner
        if sim.is_selectable:
            self.headline.send_headline_message(sim, amount)

    def send_rank_change_update_message(self, previous_rank, current_rank):
        msg = Commodities_pb2.RankedStatisticRankChangedUpdate()
        msg.stat_id = self.guid64
        msg.previous_rank = previous_rank
        msg.current_rank = current_rank
        send_sim_ranked_stat_change_rank_change_update_message(
            self.tracker.owner, msg)
        self.send_commodity_progress_msg()

    def on_sim_ready_to_simulate(self):
        level = self.get_user_value()
        event_data = self.event_data.get(level)
        if event_data is not None:
            resolver = SingleSimResolver(self.tracker.owner)
            for loot in event_data.loot_always_on_load:
                loot.apply_to_resolver(resolver)
        self.apply_initial_loot()

    def apply_initial_loot(self):
        if not self.initial_loot:
            return
        if self._initial_loots_awarded:
            return
        resolver = SingleSimResolver(self.tracker.owner)
        for loot in self.initial_loot:
            loot.apply_to_resolver(resolver)
        self._initial_loots_awarded = True

    def _get_valid_value(self, value, old_score):
        new_score = self.convert_to_user_value(value)
        if old_score <= new_score:
            resolver = SingleSimResolver(self.tracker.owner)
            while old_score <= new_score:
                old_score += 1
                event_data = self.event_data.get(old_score)
                if event_data is not None:
                    if not event_data.tests.run_tests(resolver=resolver):
                        points = self.points_to_level(old_score - 1)
                        return points
        return value

    def on_lock(self, action_on_lock):
        self._notifications_disabled = True
        should_zero_out = self.zero_out_on_lock or action_on_lock == StatisticLockAction.USE_MIN_VALUE_TUNING
        if should_zero_out:
            self.highest_level = 0
        super().on_lock(action_on_lock)
        if should_zero_out:
            self.reset_bucks()
        self._notifications_disabled = False

    def reset_bucks(self):
        for bucks_type in self.associated_bucks_types:
            bucks_tracker = BucksUtils.get_tracker_for_bucks_type(
                bucks_type, self.tracker.owner.id)
            if bucks_tracker is not None:
                bucks_tracker.try_modify_bucks(
                    bucks_type,
                    -bucks_tracker.get_bucks_amount_for_type(bucks_type))

    @classmethod
    def calculate_level_and_rank(cls, value):
        level = 0
        rank = cls.initial_rank
        for points_to_next_level in cls.get_level_list():
            value -= points_to_next_level
            if value < 0:
                break
            level += 1
            level_data = cls.event_data.get(level)
            if level_data.rank_up:
                rank += 1
        return (level, rank)

    def set_level_and_rank(self):
        (level, rank) = self.calculate_level_and_rank(self.get_value())
        self.previous_event_level = level
        self.rank_level = rank

    def should_display_delayed_decay_warning(self):
        if self.highest_level == 0:
            return False
        return super().should_display_delayed_decay_warning()

    @classproperty
    def valid_for_stat_testing(cls):
        return True

    @classmethod
    def load_statistic_data(cls, tracker, data):
        super().load_statistic_data(tracker, data)
        stat = tracker.get_statistic(cls)
        if stat is not None:
            stat._initial_loots_awarded = data.initial_loots_awarded
            stat.set_level_and_rank()
            stat.highest_level = data.highest_level
            stat.load_time_of_last_value_change(data)
            stat.fixup_callbacks_during_load()

    def save_statistic(self, commodities, skills, ranked_statistics, tracker):
        message = protocols.RankedStatistic()
        message.name_hash = self.guid64
        message.value = self.get_saved_value()
        message.highest_level = self.highest_level
        message.initial_loots_awarded = self._initial_loots_awarded
        if self._time_of_last_value_change:
            message.time_of_last_value_change = self._time_of_last_value_change.absolute_ticks(
            )
        ranked_statistics.append(message)
class CareerTone(AwayAction):
    __qualname__ = 'CareerTone'
    INSTANCE_TUNABLES = {
        'dominant_tone_loot_actions':
        TunableList(
            description=
            '\n            Loot to apply at the end of a work period if this tone ran for the\n            most amount of time out of all tones.\n            ',
            tunable=LootActions.TunableReference()),
        'performance_multiplier':
        Tunable(
            description=
            '\n            Performance multiplier applied to work performance gain.\n            ',
            tunable_type=float,
            default=1)
    }
    runtime_commodity = None

    @classmethod
    def _tuning_loaded_callback(cls):
        if cls.runtime_commodity is not None:
            return
        commodity = RuntimeCommodity.generate(cls.__name__)
        commodity.decay_rate = 0
        commodity.convergence_value = 0
        commodity.remove_on_convergence = True
        commodity.visible = False
        commodity.max_value_tuning = date_and_time.SECONDS_PER_WEEK
        commodity.min_value_tuning = 0
        commodity.initial_value = 0
        commodity._time_passage_fixup_type = CommodityTimePassageFixupType.DO_NOT_FIXUP
        cls.runtime_commodity = commodity

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._performance_change_alarm_handle = None
        self._last_performance_change_time = None

    def run(self, callback):
        super().run(callback)
        self._last_performance_change_time = services.time_service().sim_now
        time_span = clock.interval_in_sim_minutes(
            Career.CAREER_PERFORMANCE_UPDATE_INTERVAL)
        self._performance_change_alarm_handle = alarms.add_alarm(
            self, time_span, self._do_performance_change, repeating=True)

    def stop(self):
        if self._performance_change_alarm_handle is not None:
            alarms.cancel_alarm(self._performance_change_alarm_handle)
            self._performance_change_alarm_handle = None
        self._apply_performance_change()
        super().stop()

    def _do_performance_change(self, alarm_handle):
        self._apply_performance_change()

    def _apply_performance_change(self):
        career = self.sim_info.career_tracker.get_at_work_career()
        if career is None:
            logger.error(
                'CareerTone {} trying to update performance when Sim {} not at work',
                self,
                self.sim_info,
                owner='tingyul')
            return
        now = services.time_service().sim_now
        elapsed = now - self._last_performance_change_time
        self._last_performance_change_time = now
        career.apply_performance_change(elapsed, self.performance_multiplier)
        career.resend_career_data()

    def apply_dominant_tone_loot(self):
        resolver = self.get_resolver()
        for loot in self.dominant_tone_loot_actions:
            loot.apply_to_resolver(resolver)
Ejemplo n.º 23
0
class CareerTone(AwayAction):
    INSTANCE_TUNABLES = {
        'dominant_tone_loot_actions':
        TunableList(
            description=
            '\n            Loot to apply at the end of a work period if this tone ran for the\n            most amount of time out of all tones.\n            ',
            tunable=TunableReference(
                manager=services.get_instance_manager(
                    sims4.resources.Types.ACTION),
                class_restrictions=('LootActions', 'RandomWeightedLoot'))),
        'performance_multiplier':
        Tunable(
            description=
            '\n            Performance multiplier applied to work performance gain.\n            ',
            tunable_type=float,
            default=1),
        'periodic_sim_filter_loot':
        TunableList(
            description=
            '\n            Loot to apply periodically to between the working Sim and other\n            Sims, specified via a Sim filter.\n            \n            Example Usages:\n            -Gain relationship with 2 coworkers every hour.\n            -Every hour, there is a 5% chance of meeting a new coworker.\n            ',
            tunable=TunableTuple(
                chance=SuccessChance.TunableFactory(
                    description=
                    '\n                    Chance per hour of loot being applied.\n                    '
                ),
                sim_filter=TunableSimFilter.TunableReference(
                    description=
                    '\n                    Filter for specifying who to set at target Sims for loot\n                    application.\n                    '
                ),
                max_sims=OptionalTunable(
                    description=
                    '\n                    If enabled and the Sim filter finds more than the specified\n                    number of Sims, the loot will only be applied a random\n                    selection of this many Sims.\n                    ',
                    tunable=TunableRange(tunable_type=int,
                                         default=1,
                                         minimum=1)),
                loot=LootActions.TunableReference(
                    description=
                    '\n                    Loot actions to apply to the two Sims. The Sim in the \n                    career is Actor, and if Targeted is enabled those Sims\n                    will be TargetSim.\n                    '
                )))
    }
    runtime_commodity = None

    @classmethod
    def _tuning_loaded_callback(cls):
        if cls.runtime_commodity is not None:
            return
        commodity = RuntimeCommodity.generate(cls.__name__)
        commodity.decay_rate = 0
        commodity.convergence_value = 0
        commodity.remove_on_convergence = True
        commodity.visible = False
        commodity.max_value_tuning = date_and_time.SECONDS_PER_WEEK
        commodity.min_value_tuning = 0
        commodity.initial_value = 0
        commodity._time_passage_fixup_type = CommodityTimePassageFixupType.DO_NOT_FIXUP
        cls.runtime_commodity = commodity

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._update_alarm_handle = None
        self._last_update_time = None

    def run(self, callback):
        super().run(callback)
        self._last_update_time = services.time_service().sim_now
        time_span = clock.interval_in_sim_minutes(
            Career.CAREER_PERFORMANCE_UPDATE_INTERVAL)
        self._update_alarm_handle = alarms.add_alarm(
            self,
            time_span,
            lambda alarm_handle: self._update(),
            repeating=True)

    def stop(self):
        if self._update_alarm_handle is not None:
            alarms.cancel_alarm(self._update_alarm_handle)
            self._update_alarm_handle = None
        self._update()
        super().stop()

    def _update(self):
        career = self.sim_info.career_tracker.get_at_work_career()
        if career is None:
            logger.error(
                'CareerTone {} trying to update performance when Sim {} not at work',
                self,
                self.sim_info,
                owner='tingyul')
            return
        if career._upcoming_gig is not None and career._upcoming_gig.odd_job_tuning is not None:
            return
        now = services.time_service().sim_now
        elapsed = now - self._last_update_time
        self._last_update_time = now
        career.apply_performance_change(elapsed, self.performance_multiplier)
        career.resend_career_data()
        resolver = SingleSimResolver(self.sim_info)
        for entry in self.periodic_sim_filter_loot:
            chance = entry.chance.get_chance(resolver) * elapsed.in_hours()
            if random.random() > chance:
                continue
            services.sim_filter_service().submit_filter(
                entry.sim_filter,
                self._sim_filter_loot_response,
                callback_event_data=entry,
                requesting_sim_info=self.sim_info,
                gsi_source_fn=self.get_sim_filter_gsi_name)

    def get_sim_filter_gsi_name(self):
        return str(self)

    def _sim_filter_loot_response(self, filter_results, callback_event_data):
        entry = callback_event_data
        if entry.max_sims is None:
            targets = tuple(result.sim_info for result in filter_results)
        else:
            sample_size = min(len(filter_results), entry.max_sims)
            targets = tuple(
                result.sim_info
                for result in random.sample(filter_results, sample_size))
        for target in targets:
            resolver = DoubleSimResolver(self.sim_info, target)
            entry.loot.apply_to_resolver(resolver)

    def apply_dominant_tone_loot(self):
        resolver = self.get_resolver()
        for loot in self.dominant_tone_loot_actions:
            loot.apply_to_resolver(resolver)
Ejemplo n.º 24
0
class TutorialDramaNode(SituationDramaNode):
    INSTANCE_TUNABLES = {'weather_to_force': WeatherEvent.TunablePackSafeReference(description='\n            The weather that will exist for the duration of this drama node.\n            '), 'friendship_value': TunableRange(description='\n            The value to set the friendship between player sim and housemate.\n            ', tunable_type=int, maximum=100, minimum=-100, default=20), 'romance_value': TunableRange(description='\n            The value to set the romance between player sim and housemate.\n            ', tunable_type=int, maximum=100, minimum=-100, default=0), 'end_loots': TunableList(description='\n            Loots to apply when the tutorial drama node ends.\n            \n            Player sim is Actor.\n            Housemate sim is Targetsim.\n            ', tunable=LootActions.TunableReference(pack_safe=True))}

    @classproperty
    def drama_node_type(cls):
        return DramaNodeType.TUTORIAL

    @classproperty
    def spawn_sims_during_zone_spin_up(cls):
        return True

    @classproperty
    def persist_when_active(cls):
        return True

    @property
    def _require_instanced_sim(self):
        return False

    def _run(self):
        super()._run()
        self._disable_non_tutorial_functionality()
        return DramaNodeRunOutcome.SUCCESS_NODE_INCOMPLETE

    def get_player_sim_info(self):
        return self._receiver_sim_info

    def get_housemate_sim_info(self):
        return self._sender_sim_info

    def resume(self):
        super().resume()
        self._disable_non_tutorial_functionality()

    def end(self):
        drama_service = services.drama_scheduler_service()
        drama_service.set_enabled_state(True)
        if self._receiver_sim_info is not None:
            household = self._receiver_sim_info.household
            if household is not None:
                household.bills_manager.autopay_bills = False
            if self._sender_sim_info is not None:
                sim_info = self._sender_sim_info
                household = self._receiver_sim_info.household
                if sim_info in household:
                    client = services.client_manager().get_first_client()
                    if not client.set_active_sim_info(self._receiver_sim_info):
                        logger.error('Tutorial Drama node ended without being able to set player sim to active.')
                    client.remove_selectable_sim_info(sim_info)
                    household.remove_sim_info(sim_info)
                    sim_info.transfer_to_hidden_household()
                relationship = services.relationship_service().create_relationship(self._receiver_sim_info.sim_id, sim_info.sim_id)
                if relationship is not None:
                    relationship.relationship_track_tracker.set_longterm_tracks_locked(False)
        npc_hosted_situation_service = services.npc_hosted_situation_service()
        if npc_hosted_situation_service is not None:
            npc_hosted_situation_service.resume_welcome_wagon()
        situation_manager = services.get_zone_situation_manager()
        if situation_manager is not None:
            situation = situation_manager.get_situation_by_type(self.situation_to_run)
            if situation is not None:
                situation_manager.destroy_situation_by_id(situation.id)
        weather_service = services.weather_service()
        if weather_service is not None:
            weather_service.reset_forecasts()
        fire_service = services.fire_service
        if fire_service is not None:
            fire_service.fire_enabled = True
        resolver = DoubleSimResolver(self._receiver_sim_info, self._sender_sim_info)
        for loot_action in self.end_loots:
            loot_action.apply_to_resolver(resolver)
        drama_service.complete_node(self.uid)

    def _disable_non_tutorial_functionality(self):
        services.drama_scheduler_service().set_enabled_state(False)
        npc_hosted_situation_service = services.npc_hosted_situation_service()
        if npc_hosted_situation_service is not None:
            npc_hosted_situation_service.suspend_welcome_wagon()
        if self._receiver_sim_info is not None:
            household = self._receiver_sim_info.household
            if household is not None:
                household.bills_manager.autopay_bills = True
            if self._sender_sim_info is not None:
                if self._receiver_sim_info.age == self._sender_sim_info.age:
                    receiver_age_progress = self._receiver_sim_info.age_progress
                    if receiver_age_progress <= self._sender_sim_info.age_progress:
                        if receiver_age_progress == 0:
                            self._receiver_sim_info.age_progress = 0.1
                        else:
                            self._sender_sim_info.age_progress = 0
                relationship_tracker = self._receiver_sim_info.relationship_tracker
                sender_id = self._sender_sim_info.sim_id
                relationship = services.relationship_service().create_relationship(self._receiver_sim_info.sim_id, sender_id)
                relationship_tracker.add_relationship_score(sender_id, self.friendship_value, RelationshipTrack.FRIENDSHIP_TRACK)
                relationship_tracker.add_relationship_score(sender_id, self.romance_value, RelationshipTrack.ROMANCE_TRACK)
                relationship.relationship_track_tracker.set_longterm_tracks_locked(True)
        weather_service = services.weather_service()
        if weather_service is not None:
            weather_service.force_start_weather_event(self.weather_to_force, None)
            weather_service.update_weather_type(during_load=True)
        fire_service = services.fire_service
        if fire_service is not None:
            fire_service.fire_enabled = False
Ejemplo n.º 25
0
class AdventureMoment(HasTunableFactory, AutoFactoryInit):
    LOOT_NOTIFICATION_TEXT = TunableLocalizedStringFactory(description='\n        A string used to recursively build loot notification text. It will be\n        given two tokens: a loot display text string, if any, and the previously\n        built LOOT_NOTIFICATION_TEXT string.\n        ')
    NOTIFICATION_TEXT = TunableLocalizedStringFactory(description='\n        A string used to format notifications. It will be given two arguments:\n        the notification text and the built version of LOOT_NOTIFICATION_TEXT,\n        if not empty.\n        ')
    CHEAT_TEXT = TunableTuple(description='\n        Strings to be used for display text on cheat buttons to trigger all\n        adventure moments. \n        ', previous_display_text=TunableLocalizedStringFactory(description='\n            Text that will be displayed on previous cheat button.\n            '), next_display_text=TunableLocalizedStringFactory(description='\n            Text that will be displayed on next cheat button.\n            '), text_pattern=TunableLocalizedStringFactory(description='\n            Format for displaying next and previous buttons text including the\n            progress.\n            '), tooltip=TunableLocalizedStringFactory(description='\n            Tooltip to show when disabling previous or next button.\n            '))
    COST_TYPE_SIMOLEONS = 0
    COST_TYPE_ITEMS = 1
    CHEAT_PREVIOUS_INDEX = 1
    CHEAT_NEXT_INDEX = 2
    FACTORY_TUNABLES = {'description': '\n            A phase of an adventure. Adventure moments may present\n            some information in a dialog form and for a choice to be\n            made regarding how the overall adventure will branch.\n            ', '_visibility': OptionalTunable(description='\n            Control whether or not this moment provides visual feedback to\n            the player (i.e., a modal dialog).\n            ', tunable=UiDialog.TunableFactory(), disabled_name='not_visible', enabled_name='show_dialog'), '_finish_actions': TunableList(description='\n            A list of choices that can be made by the player to determine\n            branching for the adventure. They will be displayed as buttons\n            in the UI. If no dialog is displayed, then the first available\n            finish action will be selected. If this list is empty, the\n            adventure ends.\n            ', tunable=TunableTuple(availability_tests=TunableTestSet(description='\n                    A set of tests that must pass in order for this Finish\n                    Action to be available on the dialog. A Finish Action failing\n                    all tests is handled as if it were never tuned.\n                    '), display_text=TunableLocalizedStringFactoryVariant(description="\n                   This finish action's title. This will be the button text in\n                   the UI.\n                   ", allow_none=True), display_subtext=TunableLocalizedStringFactoryVariant(description='\n                    If tuned, this text will display below the button for this Finish Action.\n                    \n                    Span tags can be used to change the color of the text to green/positive and red/negative.\n                    <span class="positive">TEXT</span> will make the word TEXT green\n                    <span class="negative">TEXT</span> will make the word TEXT red\n                    ', allow_none=True), disabled_text=OptionalTunable(description="\n                    If enabled, this is the string that will be displayed if \n                    this finishing action is not available because the tests \n                    don't pass.\n                    ", tunable=TunableLocalizedStringFactory()), cost=TunableVariant(description='\n                    The cost associated with this finish action. Only one type\n                    of cost may be tuned. The player is informed of the cost\n                    before making the selection by modifying the display_text\n                    string to include this information.\n                    ', simoleon_cost=TunableTuple(description="The specified\n                        amount will be deducted from the Sim's funds.\n                        ", locked_args={'cost_type': COST_TYPE_SIMOLEONS}, amount=TunableRange(description='How many Simoleons to\n                            deduct.\n                            ', tunable_type=int, default=0, minimum=0)), item_cost=TunableTuple(description="The specified items will \n                        be removed from the Sim's inventory.\n                        ", locked_args={'cost_type': COST_TYPE_ITEMS}, item_cost=ItemCost.TunableFactory()), default=None), action_results=TunableList(description='\n                    A list of possible results that can occur if this finish\n                    action is selected. Action results can award loot, display\n                    notifications, and control the branching of the adventure by\n                    selecting the next adventure moment to run.\n                    ', tunable=TunableTuple(weight_modifiers=TunableList(description='\n                            A list of modifiers that affect the probability that\n                            this action result will be chosen. These are exposed\n                            in the form (test, multiplier). If the test passes,\n                            then the multiplier is applied to the running total.\n                            The default multiplier is 1. To increase the\n                            likelihood of this action result being chosen, tune\n                            multiplier greater than 1. To decrease the\n                            likelihood of this action result being chose, tune\n                            multipliers lower than 1. If you want to exclude\n                            this action result from consideration, tune a\n                            multiplier of 0.\n                            ', tunable=TunableTuple(description='\n                                A pair of test and weight multiplier. If the\n                                test passes, the associated weight multiplier is\n                                applied. If no test is specified, the multiplier\n                                is always applied.\n                                ', test=TunableTestVariant(description='\n                                    The test that has to pass for this weight\n                                    multiplier to be applied. The information\n                                    available to this test is the same\n                                    information available to the interaction\n                                    owning this adventure.\n                                    ', test_locked_args={'tooltip': None}), weight_multiplier=Tunable(description='\n                                    The weight multiplier to apply if the\n                                    associated test passes.\n                                    ', tunable_type=float, default=1))), notification=OptionalTunable(description='\n                            If set, this notification will be displayed.\n                            ', tunable=TunableUiDialogNotificationSnippet()), next_moments=TunableList(description='\n                            A list of adventure moment keys. One of these keys will\n                            be selected to determine which adventure moment is\n                            selected next. If the list is empty, the adventure ends\n                            here. Any of the keys tuned here will have to be tuned\n                            in the _adventure_moments tunable for the owning adventure.\n                            ', tunable=AdventureMomentKey), loot_actions=TunableList(description='\n                            List of Loot actions that are awarded if this action result is selected.\n                            ', tunable=LootActions.TunableReference()), continuation=TunableContinuation(description='\n                            A continuation to push when running finish actions.\n                            '), results_dialog=OptionalTunable(description='\n                            A results dialog to show. This dialog allows a list\n                            of icons with labels.\n                            ', tunable=UiDialogLabeledIcons.TunableFactory()), events_to_send=TunableList(description='\n                            A list of events to send.\n                            ', tunable=TunableEnumEntry(description='\n                                events types to send\n                                ', tunable_type=TestEvent, default=TestEvent.Invalid))))))}

    def __init__(self, parent_adventure, **kwargs):
        super().__init__(**kwargs)
        self._parent_adventure = parent_adventure
        self.resolver = self._interaction.get_resolver()

    @property
    def _interaction(self):
        return self._parent_adventure.interaction

    @property
    def _sim(self):
        return self._interaction.sim

    def run_adventure(self):
        if self._visibility is None:
            if self._finish_actions:
                self._run_first_valid_finish_action()
        else:
            dialog = self._get_dialog()
            if dialog is not None:
                self._parent_adventure.force_action_result = True
                dialog.show_dialog(auto_response=0)

    def _run_first_valid_finish_action(self):
        resolver = self.resolver
        for (action_id, finish_action) in enumerate(self._finish_actions):
            if finish_action.availability_tests.run_tests(resolver):
                return self._run_action_from_index(action_id)

    def _is_action_result_available(self, action_result):
        if not action_result.next_moments:
            return True
        for moment_key in action_result.next_moments:
            if self._parent_adventure.is_adventure_moment_available(moment_key):
                return True
        return False

    def _run_action_from_cheat(self, action_index):
        cheat_index = action_index - len(self._finish_actions) + 1
        if cheat_index == self.CHEAT_PREVIOUS_INDEX:
            self._parent_adventure.run_cheat_previous_moment()
        elif cheat_index == self.CHEAT_NEXT_INDEX:
            self._parent_adventure.run_cheat_next_moment()

    def _get_action_result_weight(self, action_result):
        interaction_resolver = self.resolver
        weight = 1
        for modifier in action_result.weight_modifiers:
            if not modifier.test is None:
                if interaction_resolver(modifier.test):
                    weight *= modifier.weight_multiplier
            weight *= modifier.weight_multiplier
        return weight

    def _apply_action_cost(self, action):
        if action.cost.cost_type == self.COST_TYPE_SIMOLEONS:
            if not self._sim.family_funds.try_remove(action.cost.amount, Consts_pb2.TELEMETRY_INTERACTION_COST, sim=self._sim):
                return False
        elif action.cost.cost_type == self.COST_TYPE_ITEMS:
            item_cost = action.cost.item_cost
            return item_cost.consume_interaction_cost(self._interaction)()
        return True

    def _run_action_from_index(self, action_index):
        try:
            finish_action = self._finish_actions[action_index]
        except IndexError as err:
            logger.exception('Exception {} while attempting to get finish action.\nFinishActions length: {}, ActionIndex: {},\nCurrent Moment: {},\nResolver: {}.\n', err, len(self._finish_actions), action_index, self._parent_adventure._current_moment_key, self.resolver)
            return
        forced_action_result = False
        weight_pairs = [(self._get_action_result_weight(action_result), action_result) for action_result in finish_action.action_results if self._is_action_result_available(action_result)]
        if not weight_pairs:
            if self._parent_adventure.force_action_result:
                forced_action_result = True
                weight_pairs = [(self._get_action_result_weight(action_result), action_result) for action_result in finish_action.action_results]
        action_result = weighted_random_item(weight_pairs)
        if not (action_result is not None or not finish_action.action_results) and not self._apply_action_cost(finish_action):
            return
        if action_result is not None:
            loot_display_text = None
            resolver = self.resolver
            for actions in action_result.loot_actions:
                for (loot_op, test_ran) in actions.get_loot_ops_gen(resolver):
                    (success, _) = loot_op.apply_to_resolver(resolver, skip_test=test_ran)
                    if success and action_result.notification is not None:
                        current_loot_display_text = loot_op.get_display_text()
                        if current_loot_display_text is not None:
                            if loot_display_text is None:
                                loot_display_text = current_loot_display_text
                            else:
                                loot_display_text = self.LOOT_NOTIFICATION_TEXT(loot_display_text, current_loot_display_text)
            if action_result.notification is not None:
                if loot_display_text is not None:
                    notification_text = lambda *tokens: self.NOTIFICATION_TEXT(action_result.notification.text(*tokens), loot_display_text)
                else:
                    notification_text = action_result.notification.text
                dialog = action_result.notification(self._sim, self.resolver)
                dialog.text = notification_text
                dialog.show_dialog()
            if action_result.next_moments:
                if forced_action_result:
                    next_moment_key = random.choice(action_result.next_moments)
                else:
                    next_moment_key = random.choice(tuple(moment_key for moment_key in action_result.next_moments if self._parent_adventure.is_adventure_moment_available(moment_key)))
                self._parent_adventure.queue_adventure_moment(next_moment_key)
            if action_result.results_dialog:
                dialog = action_result.results_dialog(self._sim, resolver=self.resolver)
                dialog.show_dialog()
            event_manager = services.get_event_manager()
            for event_type in action_result.events_to_send:
                event_manager.process_event(event_type, sim_info=self._sim.sim_info)
            if action_result.continuation:
                self._interaction.push_tunable_continuation(action_result.continuation)

    def _is_cheat_response(self, response):
        cheat_response = response - len(self._finish_actions)
        if cheat_response < 0:
            return False
        return True

    def _on_dialog_response(self, dialog):
        response_index = dialog.response
        if response_index is None:
            return
        if False and self._is_cheat_response(response_index):
            self._run_action_from_cheat(response_index)
            return
        if response_index >= len(self._finish_actions):
            return
        self._run_action_from_index(response_index)

    def _get_action_display_text(self, action):
        display_name = self._interaction.create_localized_string(action.display_text)
        if action.cost is not None:
            if action.cost.cost_type == self.COST_TYPE_SIMOLEONS:
                amount = action.cost.amount
                display_name = self._interaction.SIMOLEON_COST_NAME_FACTORY(display_name, amount)
            elif action.cost.cost_type == self.COST_TYPE_ITEMS:
                item_cost = action.cost.item_cost
                display_name = item_cost.get_interaction_name(self._interaction, display_name)
        return lambda *_, **__: display_name

    def _get_dialog(self):
        resolver = self.resolver
        dialog = self._visibility(self._sim, resolver)
        responses = []
        for (action_id, finish_action) in enumerate(self._finish_actions):
            result = finish_action.availability_tests.run_tests(resolver)
            if not result:
                if finish_action.disabled_text is not None:
                    disabled_text = finish_action.disabled_text if not result else None
                    responses.append(UiDialogResponse(dialog_response_id=action_id, text=self._get_action_display_text(finish_action), subtext=self._interaction.create_localized_string(finish_action.display_subtext), disabled_text=disabled_text() if disabled_text is not None else None))
            disabled_text = finish_action.disabled_text if not result else None
            responses.append(UiDialogResponse(dialog_response_id=action_id, text=self._get_action_display_text(finish_action), subtext=self._interaction.create_localized_string(finish_action.display_subtext), disabled_text=disabled_text() if disabled_text is not None else None))
        if not responses:
            return
        if False and _show_all_adventure_moments:
            responses.extend(self._parent_adventure.get_cheat_responses(action_id))
        dialog.set_responses(responses)
        dialog.add_listener(self._on_dialog_response)
        return dialog
Ejemplo n.º 26
0
class BaseCivicPolicyProvider(ComponentContainer, HasStatisticComponent, HasTunableFactory, AutoFactoryInit):
    CIVIC_POLICY_SCHEDULE = TunableTuple(description='\n        Global schedule to control when voting on civic policies is active.\n        ', voting_open=TunableTimeOfWeek(description='\n            The time of the week that voting for civic policies starts.\n            ', default_day=Days.MONDAY, default_hour=8, default_minute=0), voting_close=TunableTimeOfWeek(description='\n            The time of the week that voting for civic policies ends.  Votes are\n            tallied and policies are modified at this time.\n            ', default_day=Days.SUNDAY, default_hour=16, default_minute=0), voting_close_warning_duration=TunableTimeSpan(description='\n            Duration before the Voting Close to warn players that voting is about to close.\n            ', default_hours=8), schedule_text=TunableLocalizedStringFactory(description='\n            Text for the schedule string.\n            '))
    INFLUENCE_BUCK_TYPE = TunableEnumEntry(description='\n        The type of Bucks used to hold Influence.\n        ', tunable_type=BucksType, default=BucksType.INVALID, pack_safe=True)
    INFLUENCE_TO_VOTE_COST = Tunable(description='\n        The amount of influence used with 1 vote.\n        ', tunable_type=int, default=10, export_modes=ExportModes.All)
    REPEAL_PETITION_THRESHOLD = Tunable(description='\n        The number of petition signatures required to have a policy repealed.\n        ', tunable_type=int, default=10, export_modes=ExportModes.All)
    COMMUNITY_BOARD_TAG = TunableTag(description='\n        The tag of the community boards so we can find them in the world.\n        ')
    VOTING_OPEN_NOTIFICATION = UiDialogNotification.TunableFactory(description='\n        A TNS that will fire when the voting period opens.\n        ')
    VOTING_OPEN_MAX_ENABLED_NOTIFICATION = UiDialogNotification.TunableFactory(description='\n        A TNS that will fire when the voting period opens with maximum enabled policies.\n        ')
    VOTING_CLOSE_WARNING_NOTIFICATION = UiDialogNotification.TunableFactory(description='\n        A TNS that will fire when the voting close approaches.\n        ')
    VOTING_CLOSE_WARNING_MAX_ENABLED_NOTIFICATION = UiDialogNotification.TunableFactory(description='\n        A TNS that will fire when the voting close approaches with maximum enabled policies and\n        a policy being repealed.\n        ')
    VOTING_CLOSE_NOTIFICATION = UiDialogNotification.TunableFactory(description='\n        A TNS that will fire when the voting period closes.\n        ')
    VOTING_CLOSE_MAX_ENABLED_NOTIFICATION_SUCCESS = UiDialogNotification.TunableFactory(description='\n        A TNS that will fire when the voting period closes with maximum enabled\n        policies and a policy being successfully repealed.\n        ')
    VOTING_CLOSE_MAX_ENABLED_NOTIFICATION_FAIL = UiDialogNotification.TunableFactory(description='\n        A TNS that will fire when the voting period closes with maximum enabled\n        policies and a policy being unsuccessfully repealed.\n        ')
    VOTING_CONTINUATION_AUTONOMY_COMMODITIES = TunableList(description='\n        A list of static commodities that will be solved for by autonomy to\n        find and push the vote interaction after viewing the community board.\n        ', tunable=StaticCommodity.TunableReference(description='\n            A static commodity that is solved for by autonomy to find the vote\n            interaction to push. \n            ', pack_safe=True))
    COMMUNITY_BOARD_TEXT = TunableTuple(voting_closed_policy_tooltip_text=TunableLocalizedStringFactory(description="\n            String to insert into the policy tooltips when voting isn't possible\n            because voting is closed.\n            "), voting_open_add_policy_tooltip_text=TunableLocalizedStringFactory(description="\n            Text for the tooltip on the add policy button when it's disabled because\n            voting is open. \n            "), ineligible_voter_policy_tooltip_text=TunableLocalizedStringFactory(description="\n            String to insert into the policy tooltips when voting isn't possible because\n            the sim (first token) lives on a different street.\n            "), ineligible_voter_confirm_tooltip_text=TunableLocalizedStringFactory(description='\n            Text for the tooltip on the confirm button when the button is disabled because\n            the sim (first token) lives on a different street.\n            '), no_room_confirm_tooltip_text=TunableLocalizedStringFactory(description='\n            Text for the tooltip on the confirm button when the button is disabled because\n            already full up on enacted policies.\n            '), no_room_policy_tooltip_text=TunableLocalizedStringFactory(description="\n            String to insert into the policy tooltips when voting isn't possible  because\n            already full up on enacted policies.\n            "), add_policy_picker=TunablePickerDialogVariant(description='\n            The item picker dialog.\n            ', available_picker_flags=ObjectPickerTuningFlags.ITEM))
    CALL_TO_ACTIONS = TunableList(description='\n        List of Call to Action that should be started to introduce the Civic Policy features.\n        ', tunable=TunableReference(manager=services.get_instance_manager(sims4.resources.Types.CALL_TO_ACTION), pack_safe=True))
    FACTORY_TUNABLES = {'civic_policies': TunableSet(description='\n            The civic policies that may be enacted.\n            ', tunable=TunableReference(description='\n                A civic policy.\n                ', manager=services.get_instance_manager(sims4.resources.Types.SNIPPET), class_restrictions=('BaseCivicPolicy',), pack_safe=True, export_modes=ExportModes.All)), 'voting_open_loot': TunableList(description='\n            Loot applied to Resident Sims when voting opens.\n            ', tunable=LootActions.TunableReference(description='\n                Loot to apply on voting open.\n                ', pack_safe=True), export_modes=ExportModes.ServerXML), 'voting_close_loot': TunableList(description='\n            Loot applied to Resident Sims when voting opens.\n            ', tunable=LootActions.TunableReference(description='\n                Loot to apply on voting open.\n                ', pack_safe=True), export_modes=ExportModes.ServerXML), 'community_board_dialog_title': TunableLocalizedStringFactoryVariant(description="\n            The Community Board Dialog's title text.\n            ", export_modes=ExportModes.ServerXML), 'initial_vote_test': TunableTestSet(description='\n            If at least one test passes, and the user option is enabled, initial voting will\n            be performed when voting opens.\n            ', export_modes=ExportModes.ServerXML), 'daily_random_vote_test': TunableTestSet(description='\n            If at least one test passes, and the user option is enabled, daily random voting\n            will be performed at midnight.\n            ', export_modes=ExportModes.ServerXML)}
    CIVIC_POLICY_TEST_EVENTS = (TestEvent.CivicPolicyOpenVoting, TestEvent.CivicPolicyDailyRandomVoting, TestEvent.CivicPolicyCloseVoting)

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.add_statistic_component()
        self._enacted_policies = set()
        self._balloted_policies = set()
        self._up_for_repeal_policies = set()
        self._civic_policies = set()
        for policy in self.civic_policies:
            self._civic_policies.add(policy(self))

    @constproperty
    def is_sim():
        return False

    @property
    def is_downloaded(self):
        return False

    def get_enacted_policies(self, tuning=False):
        if tuning:
            return set([type(p) for p in self._enacted_policies])
        return self._enacted_policies

    def get_balloted_policies(self, tuning=False):
        if tuning:
            return set([type(p) for p in self._balloted_policies])
        return self._balloted_policies

    def get_up_for_repeal_policies(self, tuning=False):
        if tuning:
            return set([type(p) for p in self._up_for_repeal_policies])
        return self._up_for_repeal_policies

    def get_dormant_policies(self, tuning=False):
        policies = self._civic_policies - self._enacted_policies - self._balloted_policies - self._up_for_repeal_policies
        if tuning:
            return set([type(p) for p in policies])
        return policies

    def get_civic_policies(self, tuning=False):
        if tuning:
            return self.civic_policies
        return self._civic_policies

    def new_enact_max_count(self):
        return min(len(self.civic_policies), self.max_enacted_policy_count) - len(self._enacted_policies)

    def _reset_voting_statistics(self):
        for policy in self._civic_policies:
            if policy.vote_count_statistic is None:
                logger.error('{} tuned without voting statistic', policy)
            else:
                self.set_stat_value(policy.vote_count_statistic, 0)
        self.commodity_tracker.check_for_unneeded_initial_statistics()
        self.statistic_tracker.check_for_unneeded_initial_statistics()

    def run_sim_voting_loot(self, loot_actions):
        for resolver in self.open_close_voting_loot_resolver_list:
            for loot in loot_actions:
                loot.apply_to_resolver(resolver)

    def run_auto_voting_tests(self, test_set):
        if not services.street_service().enable_automatic_voting:
            return False
        if not test_set:
            return True
        return test_set.run_tests(GlobalResolver())

    def open_voting(self):
        self._reset_voting_statistics()
        self.finalize_startup()
        self.run_sim_voting_loot(self.voting_open_loot)
        if self.run_auto_voting_tests(self.initial_vote_test):
            for policy in self._balloted_policies:
                self.vote(policy, policy.get_initial_vote_count())

    def close_voting(self):

        def get_most_voted_for_policy(policies):
            if not policies:
                return set()
            policy = max(policies, key=lambda policy: self.get_stat_value(policy.vote_count_statistic))
            if self.get_stat_value(policy.vote_count_statistic) <= 0:
                return set()
            return set((policy,))

        balloted_policies = self.get_balloted_policies()
        to_enact = get_most_voted_for_policy(balloted_policies)
        repealable_policies = self.get_up_for_repeal_policies()
        to_repeal = set()
        for policy in repealable_policies:
            if BaseCivicPolicyProvider.REPEAL_PETITION_THRESHOLD <= self.get_stat_value(policy.vote_count_statistic):
                to_repeal.add(policy)
        to_repeal -= to_enact
        to_enact -= self._enacted_policies
        self._enacted_policies -= to_repeal
        for policy in to_repeal:
            policy.repeal()
        to_enact_max_count = self.new_enact_max_count()
        while len(to_enact) > to_enact_max_count:
            to_enact.pop()
        self._enacted_policies.update(to_enact)
        for policy in to_enact:
            policy.enact()
        self._balloted_policies = set()
        self._up_for_repeal_policies = set()
        self._reset_voting_statistics()
        self.finalize_startup()
        self.run_sim_voting_loot(self.voting_close_loot)

    def get_schedule_text(self):
        return self.CIVIC_POLICY_SCHEDULE.schedule_text(self.CIVIC_POLICY_SCHEDULE.voting_open(), self.CIVIC_POLICY_SCHEDULE.voting_close())

    def do_daily_vote(self):
        if self.run_auto_voting_tests(self.daily_random_vote_test):
            for policy in self._balloted_policies:
                self.vote(policy, policy.get_daily_vote_count())

    @property
    def max_enacted_policy_count(self):
        raise NotImplementedError

    @property
    def max_balloted_policy_count(self):
        raise NotImplementedError

    @property
    def initial_balloted_policy_count(self):
        raise NotImplementedError

    @property
    def max_repealable_policy_count(self):
        raise NotImplementedError

    @property
    def open_close_voting_loot_resolver_list(self):
        raise NotImplementedError

    @classproperty
    def provider_type_id(cls):
        raise NotImplementedError

    def get_world_description_id(self):
        return 0

    def is_eligible_voter(self, sim_info):
        raise NotImplementedError

    def is_new_policy_allowed(self, sim_info):
        return False

    def _select_balloted_policies(self):
        self._balloted_policies.clear()
        count_needed = self.initial_balloted_policy_count
        r = random.Random()
        dormant_policies = list(self.get_dormant_policies())
        while dormant_policies:
            while len(self._balloted_policies) < count_needed:
                policy = r.choice(dormant_policies)
                dormant_policies.remove(policy)
                self._balloted_policies.add(policy)

    def finalize_startup(self):
        statistic_component = self.get_component(objects.components.types.STATISTIC_COMPONENT)
        statistic_component.on_finalize_load()
        if not self._civic_policies:
            return
        services.get_event_manager().unregister(self, BaseCivicPolicyProvider.CIVIC_POLICY_TEST_EVENTS)
        services.get_event_manager().register(self, BaseCivicPolicyProvider.CIVIC_POLICY_TEST_EVENTS)
        if not self._balloted_policies:
            self._select_balloted_policies()
        for policy in self._civic_policies:
            policy.finalize_startup()

    def stop_civic_policy_provider(self):
        services.get_event_manager().unregister(self, BaseCivicPolicyProvider.CIVIC_POLICY_TEST_EVENTS)

    def get_policy_instance_for_tuning(self, policy_guid64):
        for inst in self._civic_policies:
            if policy_guid64 == inst.guid64:
                return inst

    def enact(self, policy):
        policy = self.get_policy_instance_for_tuning(policy.guid64)
        if policy is None or policy in self._enacted_policies:
            return False
        if self.new_enact_max_count() == 0:
            return False
        self._enacted_policies.add(policy)
        self._balloted_policies.discard(policy)
        self._up_for_repeal_policies.discard(policy)
        policy.enact()
        return True

    def repeal(self, policy):
        policy = self.get_policy_instance_for_tuning(policy.guid64)
        if policy is None or policy not in self._enacted_policies:
            return False
        self._enacted_policies.discard(policy)
        self._up_for_repeal_policies.discard(policy)
        policy.repeal()
        return True

    def vote(self, policy, count=1, user_directed=False, lobby_interaction=False):
        policy_instance = self.get_policy_instance_for_tuning(policy.guid64)
        if policy_instance is None:
            return False
        return self.vote_by_instance(policy_instance, count, user_directed, lobby_interaction)

    def vote_by_instance(self, policy_instance, count=1, user_directed=False, lobby_interaction=False):
        if policy_instance.vote_count_statistic is not None:
            policy_list = None

            def get_current_rank():
                policy_list.sort(key=lambda policy: (self.get_stat_value(policy.vote_count_statistic), policy.guid64), reverse=True)
                return policy_list.index(policy_instance)

            if user_directed:
                factor = 0
                if policy_instance in self._balloted_policies:
                    policy_list = list(self._balloted_policies)
                    factor = 1
                elif policy_instance in self._up_for_repeal_policies:
                    policy_list = list(self._up_for_repeal_policies)
                    factor = -1
                orig_rank = get_current_rank()
            elif lobby_interaction:
                factor = 0
                if policy_instance in self._balloted_policies:
                    factor = 1
                elif policy_instance in self._up_for_repeal_policies:
                    factor = -1
            value = self.get_stat_value(policy_instance.vote_count_statistic) + count
            self.set_stat_value(policy_instance.vote_count_statistic, value)
            services.street_service().update_community_board_tooltip(self)
            if user_directed:
                if policy_list is not None:
                    with telemetry_helper.begin_hook(civic_policy_telemetry_writer, TELEMETRY_HOOK_CIVIC_POLICY_VOTE) as hook:
                        hook.write_guid(TELEMETRY_FIELD_NEIGHBORHOOD, self.get_world_description_id())
                        hook.write_guid(TELEMETRY_FIELD_POLICY, policy_instance.guid64)
                        hook.write_guid(TELEMETRY_FIELD_VOTES, factor*value)
                        hook.write_guid(TELEMETRY_FIELD_PLAYER_VOTES, factor*count)
                        hook.write_guid(TELEMETRY_FIELD_OLD_RANK, orig_rank)
                        hook.write_guid(TELEMETRY_FIELD_NEW_RANK, get_current_rank())
            if lobby_interaction:
                with telemetry_helper.begin_hook(civic_policy_telemetry_writer, TELEMETRY_HOOK_CIVIC_POLICY_LOBBY) as hook:
                    hook.write_guid(TELEMETRY_FIELD_NEIGHBORHOOD, self.get_world_description_id())
                    hook.write_guid(TELEMETRY_FIELD_POLICY, policy_instance.guid64)
                    hook.write_guid(TELEMETRY_FIELD_VOTES, factor*value)
            return True
        return False

    def _log_propose_telemetry(self, policy_instance, action):
        with telemetry_helper.begin_hook(civic_policy_telemetry_writer, TELEMETRY_HOOK_CIVIC_POLICY_PROPOSE) as hook:
            hook.write_guid(TELEMETRY_FIELD_NEIGHBORHOOD, self.get_world_description_id())
            hook.write_guid(TELEMETRY_FIELD_POLICY, policy_instance.guid64)
            hook.write_guid(TELEMETRY_FIELD_PROPOSE_ACTION, action)

    def add_to_ballot(self, policy_instance):
        if policy_instance.vote_count_statistic is not None and policy_instance not in self._balloted_policies:
            self._balloted_policies.add(policy_instance)
            self._log_propose_telemetry(policy_instance, TELEMETRY_FIELD_ACTION_VALUE_BALLOT)
            return True
        return False

    def add_for_repeal(self, policy):
        policy = self.get_policy_instance_for_tuning(policy.guid64)
        if policy is None:
            return False
        if policy not in self._enacted_policies:
            return False
        if policy in self._up_for_repeal_policies:
            return False
        self._up_for_repeal_policies.add(policy)
        self._log_propose_telemetry(policy, TELEMETRY_FIELD_ACTION_VALUE_REPEAL)
        return True

    def remove_from_repeal(self, policy):
        policy = self.get_policy_instance_for_tuning(policy.guid64)
        if policy is None:
            return False
        if policy not in self._up_for_repeal_policies:
            return False
        self._up_for_repeal_policies.discard(policy)
        self._log_propose_telemetry(policy, TELEMETRY_FIELD_ACTION_VALUE_CANCEL_REPEAL)
        return True

    def save(self, parent_data_msg):
        parent_data_msg.ClearField('policy_data')
        for policy in self._civic_policies:
            policy.save(parent_data_msg)
        parent_data_msg.ClearField('balloted_policy_ids')
        for policy in self._balloted_policies:
            parent_data_msg.balloted_policy_ids.append(policy.guid64)
        parent_data_msg.ClearField('up_for_repeal_policy_ids')
        for policy in self._up_for_repeal_policies:
            parent_data_msg.up_for_repeal_policy_ids.append(policy.guid64)
        parent_data_msg.ClearField('commodity_tracker')
        parent_data_msg.ClearField('statistics_tracker')
        parent_data_msg.ClearField('ranked_statistic_tracker')
        self.update_all_commodities()
        (commodites, _, ranked_statistics) = self.commodity_tracker.save()
        parent_data_msg.commodity_tracker.commodities.extend(commodites)
        regular_statistics = self.statistic_tracker.save()
        parent_data_msg.statistics_tracker.statistics.extend(regular_statistics)
        parent_data_msg.ranked_statistic_tracker.ranked_statistics.extend(ranked_statistics)

    def load(self, parent_data_msg):
        self.commodity_tracker.load(parent_data_msg.commodity_tracker.commodities)
        self.statistic_tracker.load(parent_data_msg.statistics_tracker.statistics)
        self.commodity_tracker.load(parent_data_msg.ranked_statistic_tracker.ranked_statistics)
        self._enacted_policies.clear()
        for policy_data in parent_data_msg.policy_data:
            policy = self.get_policy_instance_for_tuning(policy_data.policy_id)
            if policy:
                policy.load(policy_data)
                if policy.enacted:
                    self._enacted_policies.add(policy)
        for policy_id in parent_data_msg.balloted_policy_ids:
            policy = self.get_policy_instance_for_tuning(policy_id)
            if policy:
                self._balloted_policies.add(policy)
        for policy_id in parent_data_msg.up_for_repeal_policy_ids:
            policy = self.get_policy_instance_for_tuning(policy_id)
            if policy:
                self._up_for_repeal_policies.add(policy)

    def handle_event(self, sim_info, event, resolver):
        if event == TestEvent.CivicPolicyDailyRandomVoting:
            self.do_daily_vote()
        elif event == TestEvent.CivicPolicyOpenVoting:
            self.open_voting()
        elif event == TestEvent.CivicPolicyCloseVoting:
            self.close_voting()

    def get_influence(self, sim_info):
        tracker = BucksUtils.get_tracker_for_bucks_type(self.INFLUENCE_BUCK_TYPE, owner_id=sim_info.id, add_if_none=False)
        if tracker is None:
            return 0
        return tracker.get_bucks_amount_for_type(self.INFLUENCE_BUCK_TYPE)

    def modify_influence(self, sim_info, delta):
        if delta == 0:
            return
        tracker = BucksUtils.get_tracker_for_bucks_type(self.INFLUENCE_BUCK_TYPE, owner_id=sim_info.id, add_if_none=True)
        if tracker is None:
            return
        tracker.try_modify_bucks(self.INFLUENCE_BUCK_TYPE, delta)

    def populate_community_board_op(self, sim_info, op, target_id):
        op.sim_id = sim_info.id
        op.target_id = target_id
        op.influence_points = self.get_influence(sim_info)
        op.title = self.community_board_dialog_title()
        if hasattr(op, 'schedule_text'):
            op.schedule_text = self.get_schedule_text()
        for policy in self._enacted_policies:
            with ProtocolBufferRollback(op.enacted_policies) as enacted_policy:
                enacted_policy.policy_id = policy.guid64
                if policy in self._up_for_repeal_policies:
                    if policy.vote_count_statistic is None:
                        enacted_policy.count = 0
                    else:
                        enacted_policy.count = int(self.get_stat_value(policy.vote_count_statistic))
        for policy in self._balloted_policies:
            with ProtocolBufferRollback(op.balloted_policies) as balloted_policy:
                balloted_policy.policy_id = policy.guid64
                stat = policy.vote_count_statistic
                if stat is None:
                    balloted_policy.count = 0
                else:
                    balloted_policy.count = int(self.get_stat_value(stat))
                    balloted_policy.max_count = stat.max_value
        op.provider_type = self.provider_type_id
        op.new_policy_allowed = self.is_new_policy_allowed(sim_info)
        if not services.street_service().voting_open:
            op.policy_disabled_tooltip = self.COMMUNITY_BOARD_TEXT.voting_closed_policy_tooltip_text()
        if not self.is_eligible_voter(sim_info):
            op.disabled_tooltip = self.COMMUNITY_BOARD_TEXT.ineligible_voter_confirm_tooltip_text(sim_info)
            op.policy_disabled_tooltip = self.COMMUNITY_BOARD_TEXT.ineligible_voter_policy_tooltip_text(sim_info)

    def _on_add_picker_selected(self, dialog):
        tag_objs = dialog.get_result_tags()
        if not tag_objs:
            return
        num_tags = len(tag_objs)
        can_add_more = dialog.max_selectable.number_selectable - num_tags > 0
        if can_add_more:
            can_add_more = len(dialog.picker_rows) > num_tags
        op = CommunityBoardAddPolicy(tag_objs, dialog.target_sim.sim_id, can_add_more)
        Distributor.instance().add_op_with_no_owner(op)

    def create_add_policy_picker(self, sim_info, used_policy_ids):
        resolver = SingleSimResolver(sim_info)
        dialog = self.COMMUNITY_BOARD_TEXT.add_policy_picker(sim_info, resolver=resolver)
        for policy in self.get_dormant_policies():
            if policy.guid64 not in used_policy_ids:
                tooltip = lambda *_, tooltip=policy.display_description: tooltip(sim_info)
                dialog.add_row(BasePickerRow(name=policy.display_name(sim_info), icon=policy.display_icon, tag=policy.guid64, row_tooltip=tooltip))
        dialog.max_selectable.number_selectable = min(len(dialog.picker_rows), self.max_balloted_policy_count - len(self._balloted_policies) - len(used_policy_ids))
        dialog.set_target_sim(sim_info)
        dialog.add_listener(self._on_add_picker_selected)
        dialog.show_dialog()

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

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

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

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

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

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

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

    @classproperty
    def buff_type(cls):
        return cls

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    def get_timeout_time(self):
        commodity_instance = self._get_commodity_instance()
        if commodity_instance is None:
            return NO_TIMEOUT
        buff_type = self.buff_type
        if self._replacing_buff_type is not None:
            buff_type = self._replacing_buff_type
        else:
            buff_type = self.buff_type
        state_index = commodity_instance.get_state_index_matches_buff_type(buff_type)
        if state_index is None:
            return NO_TIMEOUT
        state_lower_bound_value = commodity_instance.commodity_states[state_index].value
        if commodity_instance.convergence_value <= state_lower_bound_value:
            threshold_value = state_lower_bound_value
            comparison = operator.le
        else:
            comparison = operator.ge
            next_state_index = state_index + 1
            if next_state_index >= len(commodity_instance.commodity_states):
                threshold_value = commodity_instance.convergence_value
            else:
                threshold_value = commodity_instance.commodity_states[next_state_index].value
        threshold = sims4.math.Threshold(threshold_value, comparison)
        return self._get_absolute_timeout_time(commodity_instance, threshold)
Ejemplo n.º 28
0
class ObjectDefinitionPickerInteraction(ObjectPickerInteraction):
    INSTANCE_TUNABLES = {
        'object_tags_or_definition':
        TunableVariant(
            description=
            '\n            The method that will be used to generate the list of objects that\n            will populate the picker.\n            ',
            all_items=DefinitionsFromTags.TunableFactory(
                description=
                '\n                Look through all the items and populate any with these tags.\n                \n                This should be accompanied with specific filtering tags in\n                Object Populate Filter to get a good result.\n                '
            ),
            specific_items=DefinitionsExplicit.TunableFactory(
                description=
                '\n                A list of specific items that is populated in this\n                dialog.\n                '
            ),
            tuning_group=GroupNames.PICKERTUNING),
        'definition_to_loot':
        TunableList(
            description=
            '\n            Does a definition test to see loot should be applied to subjects.\n            ',
            tunable=TunableTuple(
                description=
                '\n                Loot to apply if definition id passes.\n                ',
                definition_id_test=DefinitionIdFilter.TunableFactory(
                    description=
                    '\n                    Definition to test for.\n                    '
                ),
                loot_to_apply=LootActions.TunableReference(
                    description=
                    '\n                    Loot to apply.\n                    '
                )),
            tuning_group=GroupNames.PICKERTUNING)
    }

    @classmethod
    def has_valid_choice(cls, target, context, **kwargs):
        return cls.object_tags_or_definition.has_choices(cls,
                                                         target=target,
                                                         context=context,
                                                         sim=context.sim,
                                                         **kwargs)

    @flexmethod
    def _get_objects_gen(cls, inst, *args, **kwargs):
        inst_or_cls = inst if inst is not None else cls
        yield from inst_or_cls.object_tags_or_definition.get_items_gen()

    @flexmethod
    def create_row(cls, inst, row_obj, context=DEFAULT, target=DEFAULT):
        inst_or_cls = inst if inst is not None else cls
        icon_info = IconInfoData(obj_def_id=row_obj.id,
                                 obj_geo_hash=row_obj.thumbnail_geo_state_hash,
                                 obj_material_hash=row_obj.material_variant)
        row = ObjectPickerRow(
            object_id=row_obj.id,
            def_id=row_obj.id,
            icon_info=icon_info,
            tag=row_obj,
            name=LocalizationHelperTuning.get_object_name(row_obj))
        inst_or_cls._test_continuation(row, context=context, target=target)
        return row

    def on_choice_selected(self, choice_tag, **kwargs):
        if choice_tag is None:
            return
        resolver = self.get_resolver()
        for id_loot_test in self.definition_to_loot:
            if id_loot_test.definition_id_test(choice_tag):
                id_loot_test.loot_to_apply.apply_to_resolver(resolver)
        super().on_choice_selected(choice_tag, **kwargs)
Ejemplo n.º 29
0
class _LoudNeighborState(CommonInteractionCompletedSituationState):
    LOOT_ACTION_DELAY = 'loot_action_delay'
    FACTORY_TUNABLES = {'loot_actions_on_situation_start': TunableTuple(description='\n            A list of loot actions and a delay before they are applied to all\n            instanced sims on the active lot.\n            ', loot_actions=TunableList(description='\n                Loot Actions that will be applied to instanced Sims on lot when\n                this situation starts.\n                ', tunable=LootActions.TunableReference(description='\n                    A loot action applied to instanced Sims on the active lot when\n                    the situation starts.\n                    ')), delay=TunableSimMinute(description="\n                The delay in sim minutes before we give the loot to Sims on\n                lot. This delay starts from when the loud state is set on the\n                neighbor's door.\n                ", default=0, minimum=0))}

    def __init__(self, *args, loot_actions_on_situation_start=None, **kwargs):
        super().__init__(*args, **kwargs)
        self._loot_actions_on_situation_start = loot_actions_on_situation_start

    def on_activate(self, reader=None):
        super().on_activate(reader)
        self._add_neighbor_to_auto_fill_blacklist()
        self._set_loud_door_state()
        self._create_or_load_alarm(_LoudNeighborState.LOOT_ACTION_DELAY, self._loot_actions_on_situation_start.delay, lambda _: self._apply_loud_loot_actions(), should_persist=True, reader=reader)

    def _add_neighbor_to_auto_fill_blacklist(self):
        timeout = self._get_remaining_alarm_time(self._time_out_string)
        next_state_timeout = self.owner.complain_state()._time_out
        if next_state_timeout is not None:
            timeout += clock.interval_in_sim_minutes(next_state_timeout)
        services.get_zone_situation_manager().add_sim_to_auto_fill_blacklist(self.owner._neighbor_sim_id, blacklist_all_jobs_time=timeout.in_hours())

    def _set_loud_door_state(self):
        if self.owner._neighbor_door_id is None:
            self.owner._self_destruct()
        apartment_door = services.object_manager().get(self.owner._neighbor_door_id)
        if apartment_door is None:
            self.owner._self_destruct()
            return
        apartment_door.set_state(self.owner.loud_door_state_on.state, self.owner.loud_door_state_on)

    def _apply_loud_loot_actions(self):
        sim_info_manager = services.sim_info_manager()
        for sim in sim_info_manager.instanced_sims_gen():
            resolver = SingleSimResolver(sim.sim_info)
            if sim.is_on_active_lot():
                for loot_action in self._loot_actions_on_situation_start.loot_actions:
                    loot_action.apply_to_resolver(resolver)

    def _on_interaction_of_interest_complete(self, **kwargs):
        self.owner._change_state(self.owner.complain_state())

    def timer_expired(self):
        services.get_zone_situation_manager().remove_sim_from_auto_fill_blacklist(self.owner._neighbor_sim_id)
        self.owner._self_destruct()
Ejemplo n.º 30
0
class WeatherAwareComponent(RouteEventProviderMixin,
                            Component,
                            HasTunableFactory,
                            AutoFactoryInit,
                            component_name=WEATHER_AWARE_COMPONENT):
    class TunableWeatherAwareMapping(TunableMapping):
        def __init__(self,
                     start_description=None,
                     end_description=None,
                     **kwargs):
            super().__init__(
                key_type=TunableEnumEntry(
                    description=
                    '\n                    The weather type we are interested in.\n                    ',
                    tunable_type=WeatherType,
                    default=WeatherType.UNDEFINED),
                value_type=TunableTuple(
                    start_loot=TunableList(
                        description=start_description,
                        tunable=LootActions.TunableReference(
                            description=
                            '\n                            The loot action applied.\n                            ',
                            pack_safe=True)),
                    end_loot=TunableList(
                        description=end_description,
                        tunable=LootActions.
                        TunableReference(
                            description=
                            '\n                            The loot action applied.\n                            ',
                            pack_safe=True))),
                **kwargs)
            self.cache_key = 'TunableWeatherAwareMapping'

        def load_etree_node(self, node, source, expect_error):
            value = super().load_etree_node(node, source, expect_error)
            modified_dict = {}
            for (weather_type, loots) in value.items():
                if not loots.start_loot:
                    if loots.end_loot:
                        modified_dict[weather_type] = loots
                modified_dict[weather_type] = loots
            return frozendict(modified_dict)

    FACTORY_TUNABLES = {
        'inside_loot':
        TunableWeatherAwareMapping(
            description=
            "\n            A tunable mapping linking a weather type to the loot actions the \n            component owner should get when inside.\n            \n            WeatherType will be UNDEFINED if weather isn't installed.\n            ",
            start_description=
            '\n                Loot actions the owner should get when the weather \n                starts if inside or when the object moves inside \n                during the specified weather.\n                ',
            end_description=
            '\n                Loot actions the owner should get when the weather \n                ends if inside or when the object moves outside \n                during the specified weather.\n                '
        ),
        'outside_loot':
        TunableWeatherAwareMapping(
            description=
            "\n            A tunable mapping linking a weather type to the loot actions the \n            component owner should get when outside.\n            \n            WeatherType will be UNDEFINED if weather isn't installed.\n            ",
            start_description=
            '\n                Loot actions the owner should get when the weather \n                starts if outside or when the object moves outside \n                during the specified weather.\n                ',
            end_description=
            '\n                Loot actions the owner should get when the weather \n                ends if outside or when the object moves inside \n                during the specified weather.\n                '
        ),
        'anywhere_loot':
        TunableWeatherAwareMapping(
            description=
            "\n            A tunable mapping linking a weather type to the loot actions the \n            component owner should get regardless of inside/outside location.\n            \n            WeatherType will be UNDEFINED if weather isn't installed.\n            Anywhere actions happen after inside/outside actions when weather starts.\n            Anywhere actions happen before inside/outside actions when weather ends.\n            ",
            start_description=
            '\n                Loot actions the owner should get when the weather \n                starts regardless of location.\n                ',
            end_description=
            '\n                Loot actions the owner should get when the weather \n                ends regardless of location.\n                '
        ),
        'disable_loot':
        TunableList(
            description=
            '\n            A list of loot actions to apply to the owner of this component when\n            the component is disabled.\n            ',
            tunable=LootActions.TunableReference(
                description=
                '\n                The loot action applied.\n                ',
                pack_safe=True)),
        'enable_loot':
        TunableList(
            description=
            '\n            A list of loot actions to apply to the owner of this component when\n            the component is enabled.\n            ',
            tunable=LootActions.TunableReference(
                description=
                '\n                The loot action applied.\n                ',
                pack_safe=True)),
        'lightning_strike_loot':
        TunableList(
            description=
            '\n            A list of loot actions to apply to the owner of this component when\n            they are struck by lightning.\n            ',
            tunable=LootActions.TunableReference(
                description=
                '\n                The loot action applied.\n                ',
                pack_safe=True)),
        'umbrella_route_events':
        OptionalTunable(
            description=
            '\n            If tuned, we will consider points around inside/outside threshold\n            to handle umbrella route events.\n            ',
            tunable=TunableTuple(
                description=
                '\n                Data used to populate fields on the path plan context.\n                ',
                enter_carry_event=RouteEvent.TunablePackSafeReference(
                    description=
                    '\n                    To be moved into weather aware component.\n                    Route event to trigger umbrella carry.\n                    '
                ),
                exit_carry_event=RouteEvent.TunablePackSafeReference(
                    description=
                    '\n                    To be moved into weather aware component.\n                    Route event to trigger umbrella carry.\n                    '
                ))),
        'lightning_strike_multiplier':
        OptionalTunable(
            description=
            '\n            If enabled, we will modify the chance that this object is struck by\n            lightning. Note that the object must be tagged as an object that\n            can be struck. See Lightning module tuning.\n            ',
            tunable=TunableMultiplier.TunableFactory(
                description=
                '\n                A multiplier to the weight for this object to be struck by\n                lightning instead of other objects. \n                \n                Note that this affects Sims as well, but will affect the chance\n                this Sim is struck vs. other Sims, not other objects.\n                '
            )),
        'lightning_effect_override':
        OptionalTunable(
            description=
            '\n            If enabled, when this object is struck by lightning we will play \n            this tuned effect instead of the one specified in module lightning \n            tuning.\n            ',
            tunable=PlayEffect.TunableFactory(
                description=
                '\n                The effect we want to play when this object is struck by \n                lightning.\n                '
            ))
    }

    class LocationUpdateStatus(enum.Int, export=False):
        NOT_IN_PROGRESS = 0
        IN_PROGRESS = 1
        PENDING = 2

    def __init__(self, *args, parent=True, **kwargs):
        super().__init__(*args, **kwargs)
        self._is_outside = None
        self._location_update_status = WeatherAwareComponent.LocationUpdateStatus.NOT_IN_PROGRESS
        self._inside_sensitive = True if self.outside_loot or self.inside_loot else False
        self._disabled_count = 0
        self._safety_umbrella_putaway_event = None

    def is_valid_to_add(self):
        if not super().is_valid_to_add():
            return False
        if not self.lightning_strike_loot:
            if self.umbrella_route_events is None:
                return False
            elif not self.umbrella_route_events.enter_carry_event and not self.umbrella_route_events.exit_carry_event:
                return False
        return True

    def enable(self, value):
        if not value:
            if self.enabled:
                if self.owner.manager is not None and services.current_zone(
                ).is_zone_running:
                    self._update_location(disabling=True)
                    if self.owner.is_sim:
                        resolver = SingleSimResolver(self.owner.sim_info)
                    else:
                        resolver = SingleObjectResolver(self.owner)
                    loot_ops_list = LootOperationList(resolver,
                                                      self.disable_loot)
                    loot_ops_list.apply_operations()
                self.on_remove()
            self._disabled_count += 1
        else:
            self._disabled_count -= 1
            if self.enabled:
                if self.owner.is_sim:
                    resolver = SingleSimResolver(self.owner.sim_info)
                else:
                    resolver = SingleObjectResolver(self.owner)
                loot_ops_list = LootOperationList(resolver, self.enable_loot)
                loot_ops_list.apply_operations()
                self.on_add()
                if self._inside_sensitive:
                    self.on_location_changed_callback()
            elif self._disabled_count < 0:
                logger.error(
                    'Unbalanced enabled/disable in weathercomponent.  Called disable once more than enable.'
                )
                self._disabled_count = 0

    @property
    def enabled(self):
        return self._disabled_count == 0

    def on_add(self):
        if self.enabled:
            if self._inside_sensitive:
                self.owner.register_on_location_changed(
                    self.on_location_changed_callback)
            else:
                self.on_location_changed_callback()
            if self._has_routing_events():
                self.owner.routing_component.register_routing_stage_event(
                    RoutingStageEvent.ROUTE_END, self._on_route_finished)

    def on_remove(self):
        if self.enabled:
            self._stop()
            if self.owner.is_on_location_changed_callback_registered(
                    self.on_location_changed_callback):
                self.owner.unregister_on_location_changed(
                    self.on_location_changed_callback)
            if self._has_routing_events():
                self.owner.routing_component.unregister_routing_stage_event(
                    RoutingStageEvent.ROUTE_END, self._on_route_finished)

    def _stop(self):
        weather_service = services.weather_service()
        if weather_service is not None:
            weather_service.flush_weather_aware_message(self.owner)
            if self._is_outside is not None:
                weather_types = set(self.anywhere_loot)
                if self._is_outside:
                    weather_types.update(self.outside_loot)
                else:
                    weather_types.update(self.inside_loot)
                weather_service.deregister_object(self.owner, weather_types)
        self._is_outside = None

    def on_added_to_inventory(self):
        if self.enabled:
            if not self._inside_sensitive:
                self.on_location_changed_callback()
            self._stop()

    def on_removed_from_inventory(self):
        if self.enabled:
            self.on_location_changed_callback()

    def on_finalize_load(self):
        if self.enabled and not self.owner.is_sim:
            self._update_location()

    def on_preroll_autonomy(self):
        if self.enabled:
            is_inside_override = False
            next_interaction = self.owner.queue.peek_head()
            if next_interaction is not None:
                if next_interaction.counts_as_inside:
                    is_inside_override = True
            self._update_location(is_inside_override=is_inside_override)

    def on_buildbuy_exit(self):
        if self.enabled:
            self._update_location()

    def on_location_changed_callback(self, *_, **__):
        if self.enabled and self.owner.manager is not None and services.current_zone(
        ).is_zone_running:
            self._update_location()

    def _update_location(self, is_inside_override=False, disabling=False):
        if disabling:
            is_outside = None
        elif is_inside_override:
            is_outside = False
        elif self.owner.is_in_inventory():
            is_outside = None
        elif not self._inside_sensitive:
            is_outside = True
        else:
            is_outside = self.owner.is_outside
        if is_outside == self._is_outside:
            return
        if self._location_update_status != WeatherAwareComponent.LocationUpdateStatus.NOT_IN_PROGRESS:
            self._location_update_status = WeatherAwareComponent.LocationUpdateStatus.PENDING
            return
        self._location_update_status = WeatherAwareComponent.LocationUpdateStatus.IN_PROGRESS
        was_outside = self._is_outside
        self._is_outside = is_outside
        recurse = False
        try:
            weather_service = services.weather_service()
            if weather_service is not None:
                weather_types = weather_service.get_current_weather_types()
                weather_service.update_weather_aware_message(self.owner)
            else:
                weather_types = {WeatherType.UNDEFINED}
            if self.owner.is_sim:
                resolver = SingleSimResolver(self.owner.sim_info)
            else:
                resolver = SingleObjectResolver(self.owner)
            if was_outside is not None:
                if was_outside:
                    self._give_loot(weather_types, self.outside_loot, resolver,
                                    False)
                    if weather_service is not None:
                        weather_service.deregister_object(
                            self.owner, self.outside_loot.keys())
                else:
                    self._give_loot(weather_types, self.inside_loot, resolver,
                                    False)
                    if weather_service is not None:
                        weather_service.deregister_object(
                            self.owner, self.inside_loot.keys())
            if is_outside is not None:
                if is_outside:
                    self._give_loot(weather_types, self.outside_loot, resolver,
                                    True)
                    if weather_service is not None:
                        weather_service.register_object(
                            self.owner, self.outside_loot.keys())
                        weather_service.register_object(
                            self.owner, self.anywhere_loot.keys())
                else:
                    self._give_loot(weather_types, self.inside_loot, resolver,
                                    True)
                    if weather_service is not None:
                        weather_service.register_object(
                            self.owner, self.inside_loot.keys())
                        weather_service.register_object(
                            self.owner, self.anywhere_loot.keys())
                if was_outside is None:
                    self._give_loot(weather_types, self.anywhere_loot,
                                    resolver, True)
            else:
                self._give_loot(weather_types, self.anywhere_loot, resolver,
                                False)
                if weather_service is not None:
                    weather_service.deregister_object(
                        self.owner, self.anywhere_loot.keys())
            recurse = self._location_update_status == WeatherAwareComponent.LocationUpdateStatus.PENDING
        finally:
            self._location_update_status = WeatherAwareComponent.LocationUpdateStatus.NOT_IN_PROGRESS
        if recurse:
            self._update_location(is_inside_override=is_inside_override,
                                  disabling=disabling)

    def _give_loot(self, weather_types, loot_dict, resolver, is_start):
        for weather_type in weather_types & loot_dict.keys():
            loot = loot_dict[
                weather_type].start_loot if is_start else loot_dict[
                    weather_type].end_loot
            for loot_action in loot:
                loot_action.apply_to_resolver(resolver)

    @componentmethod_with_fallback(lambda *_, **__: None)
    def give_weather_loot(self, weather_types, is_start):
        if self._is_outside is not None:
            if self.owner.is_sim:
                resolver = SingleSimResolver(self.owner.sim_info)
            else:
                resolver = SingleObjectResolver(self.owner)
            if not is_start:
                self._give_loot(weather_types, self.anywhere_loot, resolver,
                                is_start)
            if self._is_outside:
                self._give_loot(weather_types, self.outside_loot, resolver,
                                is_start)
            else:
                self._give_loot(weather_types, self.inside_loot, resolver,
                                is_start)
            if is_start:
                self._give_loot(weather_types, self.anywhere_loot, resolver,
                                is_start)

    @componentmethod_with_fallback(lambda *_, **__: 1.0)
    def get_lightning_strike_multiplier(self):
        if not self.enabled:
            return 0
        elif self.lightning_strike_multiplier is not None:
            return self.lightning_strike_multiplier.get_multiplier(
                SingleObjectResolver(self.owner))
        return 1.0

    def on_struck_by_lightning(self):
        loot_ops_list = LootOperationList(SingleObjectResolver(self.owner),
                                          self.lightning_strike_loot)
        loot_ops_list.apply_operations()

    def _on_route_finished(self, *_, **__):
        self._safety_umbrella_putaway_event = None

    def is_route_event_valid(self, route_event, time, sim, path):
        if not self.enabled:
            return False
        if self._safety_umbrella_putaway_event is route_event:
            location = path.final_location
            if is_location_outside(location.transform.translation,
                                   location.routing_surface.secondary_id):
                self._safety_umbrella_putaway_event = None
                return False
            elif not sims4.math.almost_equal(time, path.duration() - 0.5):
                self._safety_umbrella_putaway_event = None
                return False
        return True

    def _no_regular_put_away_scheduled(self, route_event_context):
        if self._safety_umbrella_putaway_event is None:
            return not route_event_context.route_event_already_scheduled(
                self.umbrella_route_events.exit_carry_event, provider=self)
        for route_event in route_event_context.route_event_of_data_type_gen(
                type(self._safety_umbrella_putaway_event.event_data)):
            if route_event is not self._safety_umbrella_putaway_event:
                return False
        return True

    def _has_routing_events(self):
        if self.umbrella_route_events is None or self.umbrella_route_events.enter_carry_event is None or self.umbrella_route_events.exit_carry_event is None:
            return False
        return True

    def provide_route_events(self,
                             route_event_context,
                             sim,
                             path,
                             start_time=0,
                             end_time=0,
                             **kwargs):
        if not self.enabled:
            return
        if not self._has_routing_events():
            return
        resolver = SingleSimResolver(sim.sim_info)
        can_carry_umbrella = self.umbrella_route_events.enter_carry_event.test(
            resolver)
        added_enter_carry = False
        added_exit_carry = False
        is_prev_point_outside = None
        prev_time = None
        node = None
        prev_node = None
        prev_force_no_carry = False
        for (transform, routing_surface,
             time) in path.get_location_data_along_path_gen(
                 time_step=0.5, start_time=start_time, end_time=end_time):
            force_no_carry = routing_surface.type == SurfaceType.SURFACETYPE_POOL
            if not force_no_carry:
                node = path.node_at_time(time)
                if node is prev_node:
                    force_no_carry = prev_force_no_carry
                elif not force_no_carry:
                    if node.portal_object_id != 0:
                        portal_object = services.object_manager(
                            routing_surface.primary_id).get(
                                node.portal_object_id)
                        if portal_object is not None:
                            portal_instance = portal_object.get_portal_by_id(
                                node.portal_id)
                            force_no_carry = portal_instance is not None and (
                                portal_instance.portal_template is not None and
                                (portal_instance.portal_template.required_flags
                                 is not None and
                                 portal_instance.portal_template.required_flags
                                 & PortalFlags.REQUIRE_NO_CARRY
                                 == PortalFlags.REQUIRE_NO_CARRY))
            level = 0 if routing_surface is None else routing_surface.secondary_id
            is_curr_point_outside = is_location_outside(
                transform.translation, level)
            if is_prev_point_outside is None:
                is_prev_point_outside = is_curr_point_outside
                prev_time = time
            else:
                if can_carry_umbrella:
                    if not added_enter_carry:
                        if is_curr_point_outside:
                            if not route_event_context.route_event_already_scheduled(
                                    self.umbrella_route_events.
                                    enter_carry_event,
                                    provider=self):
                                route_event_context.add_route_event(
                                    RouteEventType.FIRST_OUTDOOR,
                                    self.umbrella_route_events.
                                    enter_carry_event(provider=self,
                                                      time=time))
                                added_enter_carry = True
                if not added_exit_carry:
                    if is_curr_point_outside or not is_prev_point_outside or force_no_carry:
                        if self._no_regular_put_away_scheduled(
                                route_event_context):
                            route_event_context.add_route_event(
                                RouteEventType.LAST_OUTDOOR,
                                self.umbrella_route_events.exit_carry_event(
                                    provider=self, time=prev_time))
                            added_exit_carry = True
                if not can_carry_umbrella or added_enter_carry:
                    if added_exit_carry:
                        break
                is_prev_point_outside = is_curr_point_outside
                prev_time = time
                prev_node = node
                prev_force_no_carry = force_no_carry
        if self._safety_umbrella_putaway_event is None:
            location = path.final_location
            if not is_location_outside(location.transform.translation,
                                       location.routing_surface.secondary_id):
                self._safety_umbrella_putaway_event = self.umbrella_route_events.exit_carry_event(
                    provider=self, time=path.duration() - 0.5)
                route_event_context.add_route_event(
                    RouteEventType.LAST_OUTDOOR,
                    self._safety_umbrella_putaway_event)