Example #1
0
class LandlordTuning:
    LANDLORD_FILTER = TunableSimFilter.TunablePackSafeReference(
        description=
        '\n        The Sim Filter used to find/create a Landlord for the game.\n        '
    )
    LANDLORD_REL_BIT = RelationshipBit.TunablePackSafeReference(
        description=
        '\n        The rel bit to add between a landlord and apartment tenants. This will\n        be removed if a tenant moves out of an apartment.\n        '
    )
    TENANT_REL_BIT = RelationshipBit.TunablePackSafeReference(
        description=
        '\n        The rel bit to add between an apartment Tenant and their Landlord. This\n        will be removed if a tenant moves out of an apartment.\n        '
    )
    LANDLORD_TRAIT = Trait.TunablePackSafeReference(
        description=
        '\n        The Landlord Trait used in testing and Sim Filters.\n        '
    )
    LANDLORD_FIRST_PLAY_RENT_REMINDER_NOTIFICATION = TunableUiDialogNotificationSnippet(
        description=
        '\n        The notification to show a household if they are played on a new\n        apartment home.\n        '
    )
    HOUSEHOLD_LANDLORD_EXCEPTION_TESTS = TunableTestSet(
        description=
        '\n        Tests to run when determining if a household requires a landlord.\n        '
    )
Example #2
0
    class _SimFilterSimInfoSource(_SlotSpawningSimInfoSource):
        FACTORY_TUNABLES = {
            'filter':
            TunableSimFilter.TunableReference(
                description=
                '\n                Sim filter that is used to create or find a Sim that matches\n                this filter request.\n                '
            )
        }

        def get_sim_filter_gsi_name(self):
            return str(self)

        def get_sim_infos_and_positions(self, resolver, household):
            use_fgl = True
            sim_info = resolver.get_participant(self.participant)
            filter_result = services.sim_filter_service(
            ).submit_matching_filter(
                sim_filter=self.filter,
                requesting_sim_info=sim_info,
                allow_yielding=False,
                gsi_source_fn=self.get_sim_filter_gsi_name)
            if not filter_result:
                return ()
            (position, location) = (None, None)
            spawning_object = self._get_spawning_object(resolver)
            if spawning_object is not None:
                (position, location) = self._get_position_and_location(
                    spawning_object, resolver)
                use_fgl = position is None
            return ((filter_result[0].sim_info, position, location, use_fgl), )
Example #3
0
class TravelTuning:
    ENTER_LOT_AFFORDANCE = TunableReference(services.affordance_manager(), description='SI to push when sim enters the lot.')
    EXIT_LOT_AFFORDANCE = TunableReference(services.affordance_manager(), description='SI to push when sim is exiting the lot.')
    NPC_WAIT_TIME = TunableSimMinute(15, description='Delay in sim minutes before pushing the ENTER_LOT_AFFORDANCE on a NPC at the spawn point if they have not moved.')
    TRAVEL_AVAILABILITY_SIM_FILTER = TunableSimFilter.TunableReference(description='Sim Filter to show what Sims the player can travel with to send to Game Entry.')
    TRAVEL_SUCCESS_AUDIO_STING = TunablePlayAudio(description='\n        The sound to play when we finish loading in after the player has traveled.\n        ')
    NEW_GAME_AUDIO_STING = TunablePlayAudio(description='\n        The sound to play when we finish loading in from a new game, resume, or\n        household move in.\n        ')
    GO_HOME_INTERACTION = TunableReference(description='\n        The interaction to push a Sim to go home.\n        ', manager=services.get_instance_manager(sims4.resources.Types.INTERACTION))
class Organization(DisplaySnippet):
    INSTANCE_TUNABLES = {
        'progress_statistic':
        TunableReference(
            description=
            '\n            The Ranked Statistic represents Organization Progress.\n            ',
            manager=services.get_instance_manager(
                sims4.resources.Types.STATISTIC),
            class_restrictions='RankedStatistic',
            export_modes=ExportModes.All),
        'hidden':
        Tunable(
            description=
            '\n            If True, then the organization is hidden from the organization panel.\n            ',
            tunable_type=bool,
            default=False,
            export_modes=ExportModes.All),
        'organization_task_data':
        TunableList(
            description=
            '\n            List of possible tested organization tasks that can be offered to \n            active organization members.\n            ',
            tunable=TunableTuple(
                description=
                '\n                Tuple of test and aspirations that is run on activating\n                organization tasks.\n                ',
                tests=event_testing.tests.TunableTestSet(
                    description=
                    '\n                   Tests run when the task is activated. If tests do not pass,\n                   the tasks are not considered for assignment.\n                   '
                ),
                organization_task=TunableReference(
                    description=
                    '\n                    An aspiration to use for task completion.\n                    ',
                    manager=services.get_instance_manager(
                        sims4.resources.Types.ASPIRATION),
                    class_restrictions='AspirationOrganizationTask'))),
        'organization_filter':
        TunableSimFilter.TunableReference(
            description=
            "\n            Terms to add a member to the Organization's membership list.\n            "
        ),
        'no_events_are_scheduled_string':
        OptionalTunable(
            description=
            '\n            If enabled and the organization has no scheduled events, this text\n            will be displayed in the org panel background.\n            ',
            tunable=TunableLocalizedString(
                description=
                '\n                The string to show in the organization panel if there are no scheduled\n                events.\n                '
            ))
    }
Example #5
0
class _StoryProgressionDemographicWithFilter(_StoryProgressionDemographic):
    FACTORY_TUNABLES = {
        'population_filter':
        TunableSimFilter.TunableReference(
            description=
            '\n            The subset of Sims this action can operate on.\n            '
        )
    }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for sim_info_agent in self._get_sims_from_filter():
            self._add_sim_info_agent_internal(sim_info_agent)

    def add_sim_info_agent(self, sim_info_agent):
        if self._is_valid_agent(sim_info_agent):
            self._add_sim_info_agent_internal(sim_info_agent)

    def _add_sim_info_agent_internal(self, sim_info_agent):
        raise NotImplementedError

    def remove_sim_info_agent(self, sim_info_agent):
        if self._is_valid_agent(sim_info_agent):
            self._remove_sim_info_agent_internal(sim_info_agent)

    def _remove_sim_info_agent_internal(self, sim_info_agent):
        raise NotImplementedError

    def _is_valid_agent(self, sim_info_agent):
        def get_sim_filter_gsi_name():
            return 'Request to check if {} matches filter from {}'.format(
                sim_info_agent, self)

        return services.sim_filter_service().does_sim_match_filter(
            sim_info_agent.sim_id,
            sim_filter=self.population_filter,
            gsi_source_fn=get_sim_filter_gsi_name)

    def get_sim_filter_gsi_name(self):
        return str(self)

    def _get_sims_from_filter(self):
        results = services.sim_filter_service().submit_filter(
            self.population_filter,
            None,
            allow_yielding=False,
            gsi_source_fn=self.get_sim_filter_gsi_name)
        return tuple(StoryProgressionAgentSimInfo(r.sim_info) for r in results)
Example #6
0
class _StoryProgressionFilterAction(_StoryProgressionAction):
    FACTORY_TUNABLES = {'sim_filter': TunableSimFilter.TunableReference(description='\n            The subset of Sims this action can operate on.\n            ')}

    def _get_filter(self):
        return self.sim_filter()

    def _apply_action(self, sim_info):
        raise NotImplementedError

    def _pre_apply_action(self):
        pass

    def _post_apply_action(self):
        pass

    def _allow_instanced_sims(self):
        return False

    def get_sim_filter_gsi_name(self):
        return str(self)

    def process_action(self, story_progression_flags):

        def _on_filter_request_complete(results, *_, **__):
            if results is None:
                return
            self._pre_apply_action()
            for result in results:
                sim_info = result.sim_info
                if sim_info is None:
                    continue
                if not self._allow_instanced_sims():
                    if not sim_info.is_instanced(allow_hidden_flags=ALL_HIDDEN_REASONS):
                        self._apply_action(sim_info)
                self._apply_action(sim_info)
            self._post_apply_action()

        services.sim_filter_service().submit_filter(self._get_filter(), _on_filter_request_complete, household_id=services.active_household_id(), gsi_source_fn=self.get_sim_filter_gsi_name)
Example #7
0
    class _PregnancyParentFilter(HasTunableSingletonFactory, AutoFactoryInit):
        FACTORY_TUNABLES = {
            'filter':
            TunableSimFilter.TunableReference(
                description=
                '\n                The filter to use to find a parent.\n                '
            )
        }

        def get_sim_filter_gsi_name(self):
            return str(self)

        def get_parent(self, interaction, pregnancy_subject_sim_info):
            filter_results = services.sim_filter_service(
            ).submit_matching_filter(
                sim_filter=self.filter,
                allow_yielding=False,
                requesting_sim_info=pregnancy_subject_sim_info,
                gsi_source_fn=self.get_sim_filter_gsi_name)
            if filter_results:
                parent = random.choice([
                    filter_result.sim_info for filter_result in filter_results
                ])
                return parent
Example #8
0
class RelationshipCommandTuning:
    INTRODUCE_BIT = TunableReference(
        description=
        '\n        Relationship bit to add to all Sims when running the introduce command.\n        ',
        manager=services.get_instance_manager(
            sims4.resources.Types.RELATIONSHIP_BIT))
    INTRODUCE_TRACK = TunableReference(
        description=
        '\n        Relationship track for friendship used by cheats to introduce sims. \n        ',
        manager=services.get_instance_manager(sims4.resources.Types.STATISTIC),
        class_restrictions=relationships.relationship_track.RelationshipTrack)
    INTRODUCE_VALUE = Tunable(
        description=
        '\n        The value to add to the relationship to introduce the sims.\n        ',
        tunable_type=int,
        default=0)
    CREATE_FRIENDS_COMMAND_QUANTITY = Tunable(
        description=
        '\n        The number of friendly sims to generate \n        using command |relationships.create_friends_for_sim.\n        ',
        tunable_type=int,
        default=1)
    CREATE_FRIENDS_COMMAND_FILTER = TunableSimFilter.TunableReference(
        description=
        '\n        The sim-filter for generating friendly sims.\n        ')
class LoudNeighborSituation(SituationComplexCommon):
    INSTANCE_TUNABLES = {'loud_neighbor_state': _LoudNeighborState.TunableFactory(description='\n            The situation state used for when a neighbor starts being loud.\n            This will listen for a Sim to bang on the door and complain about\n            the noise before transitioning to the complain state.\n            ', tuning_group=SituationComplexCommon.SITUATION_STATE_GROUP, display_name='01_loud_neighbor_situation_state'), 'complain_state': _ComplainState.TunableFactory(description="\n            The situation state used for when a player Sim has banged on the\n            neighbor's door and we are waiting for them to complain to the\n            neighbor.\n            ", tuning_group=SituationComplexCommon.SITUATION_STATE_GROUP, display_name='02_complain_situation_state'), 'loud_neighbor_filters': TunableList(description='\n            Sim filters that fit the description of loud neighbor(s). We run\n            through them until we find someone matching the filter.\n            ', tunable=TunableVariant(description='\n                The filter we want to use to find loud neighbors.\n                ', single_filter=TunableSimFilter.TunablePackSafeReference(description='\n                    A Sim Filter to find a loud neighbor.\n                    '), aggregate_filter=TunableSimFilter.TunablePackSafeReference(description='\n                    An aggregate Sim Filter to find a loud neighbor that has\n                    other Sims.\n                    ', class_restrictions=filters.tunable.TunableAggregateFilter), default='single_filter')), 'loud_door_state_on': TunableStateValueReference(description='\n            State to set on the apartment door of the loud neighbor when the\n            situation starts.\n            '), 'loud_door_state_off': TunableStateValueReference(description='\n            State to set on the apartment door of the loud neighbor when they\n            are no longer being loud.\n            ')}

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        reader = self._seed.custom_init_params_reader
        if reader is not None:
            self._neighbor_sim_id = reader.read_uint64(NEIGHBOR_TOKEN, None)
            self._neighbor_door_id = reader.read_uint64(DOOR_TOKEN, None)
        else:
            self._neighbor_sim_id = None
            self._neighbor_door_id = None

    @classproperty
    def allow_user_facing_goals(cls):
        return False

    @classmethod
    def _get_tuned_job_and_default_role_state_tuples(cls):
        return []

    @classmethod
    def _states(cls):
        return (SituationStateData(1, _LoudNeighborState, factory=cls.loud_neighbor_state), SituationStateData(2, _ComplainState, factory=cls.complain_state))

    @classmethod
    def default_job(cls):
        pass

    @classmethod
    def situation_meets_starting_requirements(cls, **kwargs):
        neighbor_sim_id = cls._get_loud_neighbor()
        if neighbor_sim_id is None:
            return False
        return True

    def start_situation(self):
        super().start_situation()
        neighbor_sim_id = self._get_loud_neighbor()
        self._set_loud_neighbor_and_door(neighbor_sim_id)
        if self._neighbor_sim_id is not None:
            self._change_state(self.loud_neighbor_state())

    def _save_custom_situation(self, writer):
        super()._save_custom_situation(writer)
        if self._neighbor_sim_id is not None:
            writer.write_uint64(NEIGHBOR_TOKEN, self._neighbor_sim_id)
        if self._neighbor_door_id is not None:
            writer.write_uint64(DOOR_TOKEN, self._neighbor_door_id)

    def _destroy(self):
        if self._neighbor_door_id is not None:
            apartment_door = services.object_manager().get(self._neighbor_door_id)
            if apartment_door is not None:
                apartment_door.set_state(self.loud_door_state_off.state, self.loud_door_state_off)
        services.get_zone_situation_manager().remove_sim_from_auto_fill_blacklist(self._neighbor_sim_id)
        super()._destroy()

    @classmethod
    def _get_loud_neighbor(cls):
        neighbor_sim_id = None
        blacklist_sim_ids = {sim_info.sim_id for sim_info in services.active_household()}
        blacklist_sim_ids.update(set(sim_info.sim_id for sim_info in services.sim_info_manager().instanced_sims_gen()))
        loud_neighbor_filters = sorted(cls.loud_neighbor_filters, key=lambda *args: random.random())
        for neighbor_filter in loud_neighbor_filters:
            neighbors = services.sim_filter_service().submit_matching_filter(sim_filter=neighbor_filter, allow_yielding=False, blacklist_sim_ids=blacklist_sim_ids, gsi_source_fn=cls.get_sim_filter_gsi_name)
            neighbor_sim_infos_at_home = [result.sim_info for result in neighbors if result.sim_info.is_at_home]
            if len(neighbor_sim_infos_at_home) > 1 or len(neighbor_sim_infos_at_home):
                if neighbor_filter.is_aggregate_filter():
                    continue
                neighbor_sim_id = neighbor_sim_infos_at_home[0].sim_id if neighbor_sim_infos_at_home else None
                if neighbor_sim_id is not None:
                    break
        return neighbor_sim_id

    def _set_loud_neighbor_and_door(self, neighbor_sim_id):
        neighbor_sim_info = services.sim_info_manager().get(neighbor_sim_id)
        if neighbor_sim_info is None:
            self._self_destruct()
            return
        self._neighbor_sim_id = neighbor_sim_id
        door_service = services.get_door_service()
        plex_door_infos = door_service.get_plex_door_infos()
        object_manager = services.object_manager()
        for door_info in plex_door_infos:
            door = object_manager.get(door_info.door_id)
            if door is not None:
                if door.household_owner_id == neighbor_sim_info.household_id:
                    self._neighbor_door_id = door_info.door_id
                    break
        else:
            logger.error('Could not find door object that belongs to {}', neighbor_sim_info.household.name)
            self._self_destruct()
Example #10
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)
Example #11
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)
    class _OutfitActionApplyCareerOutfit(HasTunableSingletonFactory,
                                         AutoFactoryInit):
        FACTORY_TUNABLES = {
            'picker_dialog':
            UiSimPicker.TunableFactory(
                description=
                '\n                The picker dialog to show when selecting Sims to apply this\n                outfit on.\n                '
            ),
            'sim_filter':
            TunableSimFilter.TunableReference(
                description=
                '\n                The set of available Sims to show in the Sim picker.\n                '
            ),
            'pie_menu_test_tooltip':
            OptionalTunable(
                description=
                '\n                If enabled, then a greyed-out tooltip will be displayed if there\n                are no valid choices.\n                ',
                tunable=TunableLocalizedStringFactory(
                    description=
                    '\n                    The tooltip text to show in the greyed-out tooltip when no\n                    valid choices exist.\n                    '
                ))
        }

        def get_disabled_tooltip(self):
            if self.pie_menu_test_tooltip is None:
                return
            else:
                filter_results = self._get_filter_results()
                if not filter_results:
                    return self.pie_menu_test_tooltip

        def get_sim_filter_gsi_name(self):
            return str(self)

        def _get_filter_results(self):
            return services.sim_filter_service().submit_filter(
                self.sim_filter,
                None,
                allow_yielding=False,
                gsi_source_fn=self.get_sim_filter_gsi_name)

        def _get_on_sim_choice_selected(self, interaction, picked_items):
            def _on_sim_choice_selected(dialog):
                if dialog.accepted:
                    outfit_source = interaction.outfit_sim_info.get_outfit_sim_info(
                        interaction)
                    for sim_info in dialog.get_result_tags():
                        sim_info.generate_merged_outfit(
                            outfit_source, (OutfitCategory.CAREER, 0),
                            sim_info.get_current_outfit(), picked_items[0])
                        sim_info.resend_current_outfit()

            return _on_sim_choice_selected

        def on_choice_selected(self, interaction, picked_items, **kwargs):
            dialog = self.picker_dialog(interaction.sim,
                                        title=lambda *_, **__: interaction.
                                        get_name(apply_name_modifiers=False),
                                        resolver=interaction.get_resolver())
            for filter_result in self._get_filter_results():
                dialog.add_row(
                    SimPickerRow(filter_result.sim_info.sim_id,
                                 tag=filter_result.sim_info))
            dialog.add_listener(
                self._get_on_sim_choice_selected(interaction, picked_items))
            dialog.show_dialog()
class AdoptionPickerInteraction(SuperInteraction, PickerSuperInteractionMixin):
    __qualname__ = 'AdoptionPickerInteraction'
    INSTANCE_TUNABLES = {
        'picker_dialog':
        TunablePickerDialogVariant(
            description='\n                Sim Picker Dialog\n                ',
            available_picker_flags=ObjectPickerTuningFlags.SIM,
            tuning_group=GroupNames.PICKERTUNING),
        'sim_filters':
        TunableList(
            description=
            "\n                A list of tuples of number of sims to find and filter to find\n                them.  If there aren't enough sims to be found from a filter\n                then the filter is used to create the sims.  Sims that are\n                found from one filter are placed into the black list for\n                running the next filter in order to make sure that sims don't\n                double dip creating one filter to the next.\n                ",
            tunable=TunableTuple(
                number_of_sims=TunableRange(
                    description=
                    '\n                        The number of sims to find using the filter.  If no\n                        sims are found then sims will be created to fit the\n                        filter.\n                        ',
                    tunable_type=int,
                    default=1,
                    minimum=1),
                filter=TunableSimFilter.TunableReference(
                    description=
                    '\n                        Sim filter that is used to create find the number of\n                        sims that we need for this filter request.\n                        '
                ),
                description=
                '\n                    Tuple of number of sims that we want to find and filter\n                    that will be used to find them.\n                    '
            ),
            tuning_group=GroupNames.PICKERTUNING),
        'actor_continuation':
        TunableContinuation(
            description=
            '\n                A continuation that is pushed when the acting sim is selected.\n                ',
            locked_args={'actor': ParticipantType.Actor},
            tuning_group=GroupNames.PICKERTUNING)
    }

    def __init__(self, *args, **kwargs):
        super().__init__(
            choice_enumeration_strategy=SimPickerEnumerationStrategy(),
            *args,
            **kwargs)
        self._picked_sim_id = None
        self.sim_ids = []

    def _run_interaction_gen(self, timeline):
        yield self._get_valid_choices_gen(timeline)
        self._show_picker_dialog(self.sim,
                                 target_sim=self.sim,
                                 target=self.target)
        yield element_utils.run_child(timeline,
                                      element_utils.soft_sleep_forever())
        if self._picked_sim_id is None:
            self.remove_liability(PaymentLiability.LIABILITY_TOKEN)
            return False
        picked_item_set = {self._picked_sim_id}
        self.interaction_parameters['picked_item_ids'] = frozenset(
            picked_item_set)
        self.push_tunable_continuation(self.actor_continuation,
                                       picked_item_ids=picked_item_set)
        return True

    def _get_valid_choices_gen(self, timeline):
        self.sim_ids = []
        requesting_sim_info = self.sim.sim_info
        blacklist = {
            sim_info.id
            for sim_info in services.sim_info_manager().instanced_sims_gen(
                allow_hidden_flags=ALL_HIDDEN_REASONS)
        }
        for sim_filter in self.sim_filters:
            for _ in range(sim_filter.number_of_sims):
                sim_infos = services.sim_filter_service(
                ).submit_matching_filter(
                    1,
                    sim_filter.filter,
                    None,
                    blacklist_sim_ids=blacklist,
                    requesting_sim_info=requesting_sim_info,
                    allow_yielding=False,
                    zone_id=0)
                for sim_info in sim_infos:
                    self.sim_ids.append(sim_info.id)
                    blacklist.add(sim_info.id)
                yield element_utils.run_child(
                    timeline, element_utils.sleep_until_next_tick_element())

    @flexmethod
    def create_row(cls, inst, tag):
        return SimPickerRow(sim_id=tag, tag=tag)

    @flexmethod
    def picker_rows_gen(cls, inst, target, context, **kwargs):
        if inst is not None:
            for sim_id in inst.sim_ids:
                logger.info('AdoptionPicker: add sim_id:{}', sim_id)
                row = inst.create_row(sim_id)
                yield row

    def _pre_perform(self, *args, **kwargs):
        if self.sim.household.free_slot_count == 0:
            self.cancel(
                FinishingType.FAILED_TESTS,
                cancel_reason_msg="There aren't any free household slots.")
            return
        self.add_liability(ADOPTION_LIABILTIY, AdoptionLiability())
        return super()._pre_perform(*args, **kwargs)

    def on_choice_selected(self, choice_tag, **kwargs):
        sim_id = choice_tag
        if sim_id is not None:
            self._picked_sim_id = sim_id
            self.add_liability(
                SIM_FILTER_GLOBAL_BLACKLIST_LIABILITY,
                SimFilterGlobalBlacklistLiability(
                    (sim_id, ), SimFilterGlobalBlacklistReason.ADOPTION))
        self.trigger_soft_stop()