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        '
    )
Exemple #2
0
class CoworkerMixin:
    COWORKER_RELATIONSHIP_BIT = RelationshipBit.TunableReference(
        description='\n        The relationship bit for coworkers.\n        ')

    def add_coworker_relationship_bit(self):
        if not self.has_coworkers:
            return
        sim_info_manager = services.sim_info_manager()
        for target in sim_info_manager.values():
            if self._sim_info is target:
                continue
            if not target.career_tracker is None:
                if target.career_tracker.get_career_by_uid(
                        self.guid64) is None:
                    continue
                add_coworker_relationship_bit(self._sim_info, target)

    def remove_coworker_relationship_bit(self):
        if not self.has_coworkers:
            return
        for target in self.get_coworker_sim_infos_gen():
            remove_coworker_relationship_bit(self._sim_info, target)

    def get_coworker_sim_infos_gen(self):
        tracker = self._sim_info.relationship_tracker
        for target in tracker.get_target_sim_infos():
            if target is None:
                logger.callstack('SimInfos not all loaded',
                                 level=sims4.log.LEVEL_ERROR)
            else:
                if not tracker.has_bit(target.id,
                                       self.COWORKER_RELATIONSHIP_BIT):
                    continue
                yield target
class DefaultRelationship(HasTunableSingletonFactory, AutoFactoryInit):
    __qualname__ = 'DefaultRelationship'
    FACTORY_TUNABLES = {
        'relationship_tracks':
        TunableList(
            description=
            '\n                A list of relationship track and value pairs.\n                E.g. a spouse has Romantic relationship track value of 75. \n                ',
            tunable=TunableTuple(
                track=RelationshipTrack.TunableReference(
                    description=
                    '\n                        The relationship track that is added to the relationship\n                        between the two sims.'
                ),
                value=Tunable(
                    int,
                    default=0,
                    description=
                    '\n                        The relationship track is set to this value.\n                        '
                ))),
        'relationship_bits':
        TunableSet(
            description=
            '\n                A set of untracked relationship bits that are applied to the\n                relationship between the two sims. These are bits that are\n                provided outside of the relationship_track being set. \n                E.g. everyone in the household should have the Has Met bit\n                and the spouse should have the First Kiss bit.\n                ',
            tunable=RelationshipBit.TunableReference())
    }

    def apply(self, relationship_tracker, target_sim):
        sim_to_target_tracker = relationship_tracker.get_relationship_track_tracker(
            target_sim.sim_id, add=True)
        for data in self.relationship_tracks:
            track = data['track']
            value = data['value']
            relationship_track = sim_to_target_tracker.get_statistic(
                track, True)
            if relationship_track.get_value() < value:
                sim_to_target_tracker.set_value(track, value)
            relationship_track.update_instance_data()
        for bit in self.relationship_bits:
            sim_to_target_tracker.relationship.add_bit(bit)
Exemple #4
0
class Greetings:
    GROUP_GREETINGS = TunableList(
        description=
        '\n        Group greetings play on the Sim relative to a target Sim. These\n        greetings are only played on the actor. The PickedSim participant type\n        will contain all Sims that should greet the actor in return, so you can\n        make a reaction that greets the actor and push it from interactions in\n        this list.\n        ',
        tunable=TunableTuple(
            description=
            '\n            Prioritized greetings. Place content that is pack specific at a\n            higher priority.\n            ',
            priority=Tunable(
                description=
                '\n                The relative priority of this affordance compared to\n                other affordances in this list.\n                ',
                tunable_type=int,
                default=0),
            tests_and_greetings=TunableReference(
                description='\n                ',
                manager=services.get_instance_manager(
                    sims4.resources.Types.SNIPPET),
                class_restrictions=('TunableTestedGreetingGroup', ),
                pack_safe=True)))
    TARGETED_GREETINGS = TunableList(
        description=
        '\n        Targeted greetings play between two Sims and are only available in the\n        scenario that the actor wants to socialize with a target Sim that is\n        not already socializing. These can be touching socials like hugging or\n        hi fives.\n        ',
        tunable=TunableTuple(
            description=
            '\n            Prioritized greetings. Place content that is pack specific at a\n            higher priority.\n            ',
            priority=Tunable(
                description=
                '\n                The relative priority of this affordance compared to\n                other affordances in this list.\n                ',
                tunable_type=int,
                default=0),
            tests_and_greetings=TunableReference(
                manager=services.get_instance_manager(
                    sims4.resources.Types.SNIPPET),
                class_restrictions=('TunableTestedGreetingTargeted', ),
                pack_safe=True)))
    GREETED_RELATIONSHIP_BIT = RelationshipBit.TunableReference(
        description=
        '\n        The relationship bit between greeted Sims.\n        ')
class PregnancyTracker(SimInfoTracker):
    PREGNANCY_COMMODITY_MAP = TunableMapping(
        description=
        '\n        The commodity to award if conception is successful.\n        ',
        key_type=TunableEnumEntry(
            description=
            '\n            Species these commodities are intended for.\n            ',
            tunable_type=Species,
            default=Species.HUMAN,
            invalid_enums=(Species.INVALID, )),
        value_type=TunableReference(
            description=
            '\n            The commodity reference controlling pregnancy.\n            ',
            pack_safe=True,
            manager=services.get_instance_manager(
                sims4.resources.Types.STATISTIC)))
    PREGNANCY_TRAIT = TunableReference(
        description=
        '\n        The trait that all pregnant Sims have during pregnancy.\n        ',
        manager=services.trait_manager())
    PREGNANCY_ORIGIN_TRAIT_MAPPING = TunableMapping(
        description=
        '\n        A mapping from PregnancyOrigin to a set of traits to be added at the\n        start of the pregnancy, and removed at the end of the pregnancy.\n        ',
        key_type=PregnancyOrigin,
        value_type=TunableTuple(
            description=
            '\n            A tuple of the traits that should be added/removed with a pregnancy\n            that has this origin, and the content pack they are associated with.\n            ',
            traits=TunableSet(
                description=
                '\n                The traits to be added/removed.\n                ',
                tunable=Trait.TunablePackSafeReference()),
            pack=TunableEnumEntry(
                description=
                '\n                The content pack associated with this set of traits. If the pack\n                is uninstalled, the pregnancy will be auto-completed.\n                ',
                tunable_type=Pack,
                default=Pack.BASE_GAME)))
    PREGNANCY_RATE = TunableRange(
        description='\n        The rate per Sim minute of pregnancy.\n        ',
        tunable_type=float,
        default=0.001,
        minimum=EPSILON)
    MULTIPLE_OFFSPRING_CHANCES = TunableList(
        description=
        '\n        A list defining the probabilities of multiple births.\n        ',
        tunable=TunableTuple(
            size=Tunable(
                description=
                '\n                The number of offspring born.\n                ',
                tunable_type=int,
                default=1),
            weight=Tunable(
                description=
                '\n                The weight, relative to other outcomes.\n                ',
                tunable_type=float,
                default=1),
            npc_dialog=UiDialogOk.TunableFactory(
                description=
                '\n                A dialog displayed when a NPC Sim gives birth to an offspring\n                that was conceived by a currently player-controlled Sim. The\n                dialog is specifically used when this number of offspring is\n                generated.\n                \n                Three tokens are passed in: the two parent Sims and the\n                offspring\n                ',
                locked_args={'text_tokens': None}),
            modifiers=TunableMultiplier.TunableFactory(
                description=
                '\n                A tunable list of test sets and associated multipliers to apply\n                to the total chance of this number of potential offspring.\n                '
            ),
            screen_slam_one_parent=OptionalTunable(
                description=
                '\n                Screen slam to show when only one parent is available.\n                Localization Tokens: Sim A - {0.SimFirstName}\n                ',
                tunable=TunableScreenSlamSnippet()),
            screen_slam_two_parents=OptionalTunable(
                description=
                '\n                Screen slam to show when both parents are available.\n                Localization Tokens: Sim A - {0.SimFirstName}, Sim B -\n                {1.SimFirstName}\n                ',
                tunable=TunableScreenSlamSnippet())))
    MONOZYGOTIC_OFFSPRING_CHANCE = TunablePercent(
        description=
        '\n        The chance that each subsequent offspring of a multiple birth has the\n        same genetics as the first offspring.\n        ',
        default=50)
    GENDER_CHANCE_STAT = TunableReference(
        description=
        '\n        A commodity that determines the chance that an offspring is female. The\n        minimum value guarantees the offspring is male, whereas the maximum\n        value guarantees it is female.\n        ',
        manager=services.statistic_manager())
    BIRTHPARENT_BIT = RelationshipBit.TunableReference(
        description=
        '\n        The bit that is added on the relationship from the Sim to any of its\n        offspring.\n        '
    )
    AT_BIRTH_TESTS = TunableGlobalTestSet(
        description=
        '\n        Tests to run between the pregnant sim and their partner, at the time of\n        birth. If any test fails, the the partner sim will not be set as the\n        other parent. This is intended to prevent modifications to the partner\n        sim during the time between impregnation and birth that would make the\n        partner sim an invalid parent (age too young, relationship incestuous, etc).\n        '
    )
    PREGNANCY_ORIGIN_MODIFIERS = TunableMapping(
        description=
        '\n        Define any modifiers that, given the origination of the pregnancy,\n        affect certain aspects of the generated offspring.\n        ',
        key_type=TunableEnumEntry(
            description=
            '\n            The origin of the pregnancy.\n            ',
            tunable_type=PregnancyOrigin,
            default=PregnancyOrigin.DEFAULT,
            pack_safe=True),
        value_type=TunableTuple(
            description=
            '\n            The aspects of the pregnancy modified specifically for the specified\n            origin.\n            ',
            default_relationships=TunableTuple(
                description=
                '\n                Override default relationships for the parents.\n                ',
                father_override=OptionalTunable(
                    description=
                    '\n                    If set, override default relationships for the father.\n                    ',
                    tunable=TunableEnumEntry(
                        description=
                        '\n                        The default relationships for the father.\n                        ',
                        tunable_type=DefaultGenealogyLink,
                        default=DefaultGenealogyLink.FamilyMember)),
                mother_override=OptionalTunable(
                    description=
                    '\n                    If set, override default relationships for the mother.\n                    ',
                    tunable=TunableEnumEntry(
                        description=
                        '\n                        The default relationships for the mother.\n                        ',
                        tunable_type=DefaultGenealogyLink,
                        default=DefaultGenealogyLink.FamilyMember))),
            trait_entries=TunableList(
                description=
                '\n                Sets of traits that might be randomly applied to each generated\n                offspring. Each group is individually randomized.\n                ',
                tunable=TunableTuple(
                    description=
                    '\n                    A set of random traits. Specify a chance that a trait from\n                    the group is selected, and then specify a set of traits.\n                    Only one trait from this group may be selected. If the\n                    chance is less than 100%, no traits could be selected.\n                    ',
                    chance=TunablePercent(
                        description=
                        '\n                        The chance that a trait from this set is selected.\n                        ',
                        default=100),
                    traits=TunableList(
                        description=
                        '\n                        The set of traits that might be applied to each\n                        generated offspring. Specify a weight for each trait\n                        compared to other traits in the same set.\n                        ',
                        tunable=TunableTuple(
                            description=
                            '\n                            A weighted trait that might be applied to the\n                            generated offspring. The weight is relative to other\n                            entries within the same set.\n                            ',
                            weight=Tunable(
                                description=
                                '\n                                The relative weight of this trait compared to\n                                other traits within the same set.\n                                ',
                                tunable_type=float,
                                default=1),
                            trait=Trait.TunableReference(
                                description=
                                '\n                                A trait that might be applied to the generated\n                                offspring.\n                                ',
                                pack_safe=True)))))))

    def __init__(self, sim_info):
        self._sim_info = sim_info
        self._clear_pregnancy_data()
        self._completion_callback_listener = None
        self._completion_alarm_handle = None

    @property
    def account(self):
        return self._sim_info.account

    @property
    def is_pregnant(self):
        if self._seed:
            return True
        return False

    @property
    def offspring_count(self):
        return max(len(self._offspring_data), 1)

    @property
    def offspring_count_override(self):
        return self._offspring_count_override

    @offspring_count_override.setter
    def offspring_count_override(self, value):
        self._offspring_count_override = value

    def _get_parent(self, sim_id):
        sim_info_manager = services.sim_info_manager()
        if sim_id in sim_info_manager:
            return sim_info_manager.get(sim_id)

    def get_parents(self):
        if self._parent_ids:
            parent_a = self._get_parent(self._parent_ids[0])
            parent_b = self._get_parent(self._parent_ids[1]) or parent_a
            return (parent_a, parent_b)
        return (None, None)

    def get_partner(self):
        (owner, partner) = self.get_parents()
        if partner is not owner:
            return partner

    def start_pregnancy(self,
                        parent_a,
                        parent_b,
                        pregnancy_origin=PregnancyOrigin.DEFAULT):
        if self.is_pregnant:
            return
        if not parent_a.incest_prevention_test(parent_b):
            return
        self._seed = random.randint(1, MAX_UINT32)
        self._parent_ids = (parent_a.id, parent_b.id)
        self._offspring_data = []
        self._origin = pregnancy_origin
        self.enable_pregnancy()

    def enable_pregnancy(self):
        if self.is_pregnant:
            if not self._is_enabled:
                pregnancy_commodity_type = self.PREGNANCY_COMMODITY_MAP.get(
                    self._sim_info.species)
                tracker = self._sim_info.get_tracker(pregnancy_commodity_type)
                pregnancy_commodity = tracker.get_statistic(
                    pregnancy_commodity_type, add=True)
                pregnancy_commodity.add_statistic_modifier(self.PREGNANCY_RATE)
                threshold = sims4.math.Threshold(pregnancy_commodity.max_value,
                                                 operator.ge)
                self._completion_callback_listener = tracker.create_and_add_listener(
                    pregnancy_commodity.stat_type, threshold,
                    self._on_pregnancy_complete)
                if threshold.compare(pregnancy_commodity.get_value()):
                    self._on_pregnancy_complete()
                tracker = self._sim_info.get_tracker(self.GENDER_CHANCE_STAT)
                tracker.add_statistic(self.GENDER_CHANCE_STAT)
                self._sim_info.add_trait(self.PREGNANCY_TRAIT)
                traits_pack_tuple = self.PREGNANCY_ORIGIN_TRAIT_MAPPING.get(
                    self._origin)
                if traits_pack_tuple is not None:
                    for trait in traits_pack_tuple.traits:
                        self._sim_info.add_trait(trait)
                self._is_enabled = True

    def _on_pregnancy_complete(self, *_, **__):
        if not self.is_pregnant:
            return
        if self._sim_info.is_npc:
            current_zone = services.current_zone()
            if not current_zone.is_zone_running or self._sim_info.is_instanced(
                    allow_hidden_flags=ALL_HIDDEN_REASONS):
                if self._completion_alarm_handle is None:
                    self._completion_alarm_handle = alarms.add_alarm(
                        self,
                        clock.interval_in_sim_minutes(1),
                        self._on_pregnancy_complete,
                        repeating=True,
                        cross_zone=True)
            else:
                self._create_and_name_offspring()
                self._show_npc_dialog()
                self.clear_pregnancy()

    def complete_pregnancy(self):
        services.get_event_manager().process_event(
            TestEvent.OffspringCreated,
            sim_info=self._sim_info,
            offspring_created=self.offspring_count)
        for tuning_data in self.MULTIPLE_OFFSPRING_CHANCES:
            if tuning_data.size == self.offspring_count:
                (parent_a, parent_b) = self.get_parents()
                if parent_a is parent_b:
                    screen_slam = tuning_data.screen_slam_one_parent
                else:
                    screen_slam = tuning_data.screen_slam_two_parents
                if screen_slam is not None:
                    screen_slam.send_screen_slam_message(
                        self._sim_info, parent_a, parent_b)
                break

    def _clear_pregnancy_data(self):
        self._seed = 0
        self._parent_ids = []
        self._offspring_data = []
        self._offspring_count_override = None
        self._origin = PregnancyOrigin.DEFAULT
        self._is_enabled = False

    def clear_pregnancy_visuals(self):
        if self._sim_info.pregnancy_progress:
            self._sim_info.pregnancy_progress = 0

    def clear_pregnancy(self):
        pregnancy_commodity_type = self.PREGNANCY_COMMODITY_MAP.get(
            self._sim_info.species)
        tracker = self._sim_info.get_tracker(pregnancy_commodity_type)
        if tracker is not None:
            stat = tracker.get_statistic(pregnancy_commodity_type, add=True)
            if stat is not None:
                stat.set_value(stat.min_value)
                stat.remove_statistic_modifier(self.PREGNANCY_RATE)
            if self._completion_callback_listener is not None:
                tracker.remove_listener(self._completion_callback_listener)
                self._completion_callback_listener = None
        tracker = self._sim_info.get_tracker(self.GENDER_CHANCE_STAT)
        if tracker is not None:
            tracker.remove_statistic(self.GENDER_CHANCE_STAT)
        if self._sim_info.has_trait(self.PREGNANCY_TRAIT):
            self._sim_info.remove_trait(self.PREGNANCY_TRAIT)
        traits_pack_tuple = self.PREGNANCY_ORIGIN_TRAIT_MAPPING.get(
            self._origin)
        if traits_pack_tuple is not None:
            for trait in traits_pack_tuple.traits:
                if self._sim_info.has_trait(trait):
                    self._sim_info.remove_trait(trait)
        if self._completion_alarm_handle is not None:
            alarms.cancel_alarm(self._completion_alarm_handle)
            self._completion_alarm_handle = None
        self.clear_pregnancy_visuals()
        self._clear_pregnancy_data()

    def _create_and_name_offspring(self, on_create=None):
        self.create_offspring_data()
        for offspring_data in self.get_offspring_data_gen():
            offspring_data.first_name = self._get_random_first_name(
                offspring_data)
            sim_info = self.create_sim_info(offspring_data)
            if on_create is not None:
                on_create(sim_info)

    def validate_partner(self):
        impregnator = self.get_partner()
        if impregnator is None:
            return
        resolver = DoubleSimResolver(self._sim_info, impregnator)
        if not self.AT_BIRTH_TESTS.run_tests(resolver):
            self._parent_ids = (self._sim_info.id, self._sim_info.id)

    def create_sim_info(self, offspring_data):
        self.validate_partner()
        (parent_a, parent_b) = self.get_parents()
        sim_creator = SimCreator(age=offspring_data.age,
                                 gender=offspring_data.gender,
                                 species=offspring_data.species,
                                 first_name=offspring_data.first_name,
                                 last_name=offspring_data.last_name)
        household = self._sim_info.household
        zone_id = household.home_zone_id
        (sim_info_list, _) = SimSpawner.create_sim_infos(
            (sim_creator, ),
            household=household,
            account=self.account,
            zone_id=zone_id,
            generate_deterministic_sim=True,
            creation_source='pregnancy')
        sim_info = sim_info_list[0]
        sim_info.world_id = services.get_persistence_service(
        ).get_world_id_from_zone(zone_id)
        for trait in tuple(sim_info.trait_tracker.personality_traits):
            sim_info.remove_trait(trait)
        for trait in offspring_data.traits:
            sim_info.add_trait(trait)
        sim_info.apply_genetics(parent_a,
                                parent_b,
                                seed=offspring_data.genetics)
        sim_info.resend_extended_species()
        sim_info.resend_physical_attributes()
        default_track_overrides = {}
        mother = parent_a if parent_a.gender == Gender.FEMALE else parent_b
        father = parent_a if parent_a.gender == Gender.MALE else parent_b
        if self._origin in self.PREGNANCY_ORIGIN_MODIFIERS:
            father_override = self.PREGNANCY_ORIGIN_MODIFIERS[
                self._origin].default_relationships.father_override
            if father_override is not None:
                default_track_overrides[father] = father_override
            mother_override = self.PREGNANCY_ORIGIN_MODIFIERS[
                self._origin].default_relationships.mother_override
            if mother_override is not None:
                default_track_overrides[mother] = mother_override
        self.initialize_sim_info(
            sim_info,
            parent_a,
            parent_b,
            default_track_overrides=default_track_overrides)
        self._sim_info.relationship_tracker.add_relationship_bit(
            sim_info.id, self.BIRTHPARENT_BIT)
        return sim_info

    @staticmethod
    def initialize_sim_info(sim_info,
                            parent_a,
                            parent_b,
                            default_track_overrides=None):
        sim_info.add_parent_relations(parent_a, parent_b)
        if sim_info.household is not parent_a.household:
            parent_a.household.add_sim_info_to_household(sim_info)
        sim_info.set_default_relationships(
            reciprocal=True, default_track_overrides=default_track_overrides)
        services.sim_info_manager().set_default_genealogy(
            sim_infos=(sim_info, ))
        parent_generation = max(
            parent_a.generation,
            parent_b.generation if parent_b is not None else 0)
        sim_info.generation = parent_generation + 1 if sim_info.is_played_sim else parent_generation
        services.get_event_manager().process_event(TestEvent.GenerationCreated,
                                                   sim_info=sim_info)
        client = services.client_manager().get_client_by_household_id(
            sim_info.household_id)
        if client is not None:
            client.add_selectable_sim_info(sim_info)
        parent_b_sim_id = parent_b.sim_id if parent_b is not None else 0
        RelgraphService.relgraph_add_child(parent_a.sim_id, parent_b_sim_id,
                                           sim_info.sim_id)

    @classmethod
    def select_traits_for_offspring(cls,
                                    offspring_data,
                                    parent_a,
                                    parent_b,
                                    num_traits,
                                    origin=PregnancyOrigin.DEFAULT,
                                    random=random):
        traits = []
        personality_trait_slots = num_traits

        def _add_trait_if_possible(selected_trait):
            nonlocal personality_trait_slots
            if selected_trait in traits:
                return False
            if any(t.is_conflicting(selected_trait) for t in traits):
                return False
            if selected_trait.is_personality_trait:
                if not personality_trait_slots:
                    return False
                personality_trait_slots -= 1
            traits.append(selected_trait)
            return True

        if origin in cls.PREGNANCY_ORIGIN_MODIFIERS:
            trait_entries = cls.PREGNANCY_ORIGIN_MODIFIERS[
                origin].trait_entries
            for trait_entry in trait_entries:
                if random.random() >= trait_entry.chance:
                    continue
                selected_trait = pop_weighted(
                    [(t.weight, t.trait) for t in trait_entry.traits
                     if t.trait.is_valid_trait(offspring_data)],
                    random=random)
                if selected_trait is not None:
                    _add_trait_if_possible(selected_trait)
        if parent_a is not None:
            if parent_b is not None:
                for inherited_trait_entries in parent_a.trait_tracker.get_inherited_traits(
                        parent_b):
                    selected_trait = pop_weighted(
                        list(inherited_trait_entries), random=random)
                    if selected_trait is not None:
                        _add_trait_if_possible(selected_trait)
        if not personality_trait_slots:
            return traits
        personality_traits = get_possible_traits(offspring_data)
        random.shuffle(personality_traits)
        while True:
            current_trait = personality_traits.pop()
            if _add_trait_if_possible(current_trait):
                break
            if not personality_traits:
                return traits
        if not personality_trait_slots:
            return traits
        traits_a = set(parent_a.trait_tracker.personality_traits)
        traits_b = set(parent_b.trait_tracker.personality_traits)
        shared_parent_traits = list(
            traits_a.intersection(traits_b) - set(traits))
        random.shuffle(shared_parent_traits)
        while personality_trait_slots:
            while shared_parent_traits:
                current_trait = shared_parent_traits.pop()
                if current_trait in personality_traits:
                    personality_traits.remove(current_trait)
                did_add_trait = _add_trait_if_possible(current_trait)
                if did_add_trait:
                    if not personality_trait_slots:
                        return traits
        remaining_parent_traits = list(
            traits_a.symmetric_difference(traits_b) - set(traits))
        random.shuffle(remaining_parent_traits)
        while personality_trait_slots:
            while remaining_parent_traits:
                current_trait = remaining_parent_traits.pop()
                if current_trait in personality_traits:
                    personality_traits.remove(current_trait)
                did_add_trait = _add_trait_if_possible(current_trait)
                if did_add_trait:
                    if not personality_trait_slots:
                        return traits
        while personality_trait_slots:
            while personality_traits:
                current_trait = personality_traits.pop()
                _add_trait_if_possible(current_trait)
        return traits

    def create_offspring_data(self):
        r = random.Random()
        r.seed(self._seed)
        if self._offspring_count_override is not None:
            offspring_count = self._offspring_count_override
        else:
            offspring_count = pop_weighted([
                (p.weight *
                 p.modifiers.get_multiplier(SingleSimResolver(self._sim_info)),
                 p.size) for p in self.MULTIPLE_OFFSPRING_CHANCES
            ],
                                           random=r)
        offspring_count = min(self._sim_info.household.free_slot_count + 1,
                              offspring_count)
        species = self._sim_info.species
        age = self._sim_info.get_birth_age()
        aging_data = AgingTuning.get_aging_data(species)
        num_personality_traits = aging_data.get_personality_trait_count(age)
        self._offspring_data = []
        for offspring_index in range(offspring_count):
            if offspring_index and r.random(
            ) < self.MONOZYGOTIC_OFFSPRING_CHANCE:
                gender = self._offspring_data[offspring_index - 1].gender
                genetics = self._offspring_data[offspring_index - 1].genetics
            else:
                gender_chance_stat = self._sim_info.get_statistic(
                    self.GENDER_CHANCE_STAT)
                if gender_chance_stat is None:
                    gender_chance = 0.5
                else:
                    gender_chance = (gender_chance_stat.get_value() -
                                     gender_chance_stat.min_value) / (
                                         gender_chance_stat.max_value -
                                         gender_chance_stat.min_value)
                gender = Gender.FEMALE if r.random(
                ) < gender_chance else Gender.MALE
                genetics = r.randint(1, MAX_UINT32)
            last_name = SimSpawner.get_last_name(self._sim_info.last_name,
                                                 gender, species)
            offspring_data = PregnancyOffspringData(age,
                                                    gender,
                                                    species,
                                                    genetics,
                                                    last_name=last_name)
            (parent_a, parent_b) = self.get_parents()
            offspring_data.traits = self.select_traits_for_offspring(
                offspring_data,
                parent_a,
                parent_b,
                num_personality_traits,
                origin=self._origin)
            self._offspring_data.append(offspring_data)

    def get_offspring_data_gen(self):
        for offspring_data in self._offspring_data:
            yield offspring_data

    def _get_random_first_name(self, offspring_data):
        tries_left = 10

        def is_valid(first_name):
            nonlocal tries_left
            if not first_name:
                return False
            tries_left -= 1
            if tries_left and any(sim.first_name == first_name
                                  for sim in self._sim_info.household):
                return False
            elif any(sim.first_name == first_name
                     for sim in self._offspring_data):
                return False
            return True

        first_name = None
        while not is_valid(first_name):
            first_name = SimSpawner.get_random_first_name(
                offspring_data.gender, offspring_data.species)
        return first_name

    def assign_random_first_names_to_offspring_data(self):
        for offspring_data in self.get_offspring_data_gen():
            offspring_data.first_name = self._get_random_first_name(
                offspring_data)

    def _show_npc_dialog(self):
        for tuning_data in self.MULTIPLE_OFFSPRING_CHANCES:
            if tuning_data.size == self.offspring_count:
                npc_dialog = tuning_data.npc_dialog
                if npc_dialog is not None:
                    for parent in self.get_parents():
                        if parent is None:
                            logger.error(
                                'Pregnancy for {} has a None parent for IDs {}. Please file a DT with a save attached.',
                                self._sim_info, ','.join(
                                    str(parent_id)
                                    for parent_id in self._parent_ids))
                            return
                        parent_instance = parent.get_sim_instance()
                        if parent_instance is not None:
                            if parent_instance.client is not None:
                                additional_tokens = list(
                                    itertools.chain(self.get_parents(),
                                                    self._offspring_data))
                                dialog = npc_dialog(
                                    parent_instance,
                                    DoubleSimResolver(additional_tokens[0],
                                                      additional_tokens[1]))
                                dialog.show_dialog(
                                    additional_tokens=additional_tokens)
                return

    def save(self):
        data = SimObjectAttributes_pb2.PersistablePregnancyTracker()
        data.seed = self._seed
        data.origin = self._origin
        data.parent_ids.extend(self._parent_ids)
        return data

    def load(self, data):
        self._seed = int(data.seed)
        try:
            self._origin = PregnancyOrigin(data.origin)
        except KeyError:
            self._origin = PregnancyOrigin.DEFAULT
        self._parent_ids.clear()
        self._parent_ids.extend(data.parent_ids)

    def refresh_pregnancy_data(self, on_create=None):
        if not self.is_pregnant:
            self.clear_pregnancy()
            return
        traits_pack_tuple = self.PREGNANCY_ORIGIN_TRAIT_MAPPING.get(
            self._origin)
        if traits_pack_tuple is not None and not is_available_pack(
                traits_pack_tuple.pack):
            self._create_and_name_offspring(on_create=on_create)
            self.clear_pregnancy()
        self.enable_pregnancy()

    def on_lod_update(self, old_lod, new_lod):
        if new_lod == SimInfoLODLevel.MINIMUM:
            self.clear_pregnancy()
Exemple #6
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)
Exemple #7
0
class CheatWeddingTuning:
    __qualname__ = 'CheatWeddingTuning'
    CHEAT_ENGAGED_RELATIONSHIP_BITS = TunableList(
        description=
        '\n        Relationship bits added to the sims that are being cheated into engaged status.\n        ',
        tunable=RelationshipBit.TunableReference())
class ContentScoreMixin:
    INSTANCE_TUNABLES = {
        'content_score':
        OptionalTunable(
            description=
            '\n            If enabled, the interaction will be scored.\n            Otherwise, scoring will be ignored.\n            ',
            tunable=TunableTuple(
                base_score=Tunable(
                    description=
                    ' \n                    Base score when determining the content set value of any interaction. \n                    This is the base value used before any modification to content score.\n            \n                    Modification to the content score for this interaction can come from\n                    topics and moods.\n            \n                    USAGE: If you would like this mixer to more likely show up no\n                    matter the topic and mood ons the sims tune this value higher.\n                                            \n                    Formula being used to determine the autonomy score is Score =\n                    Avg(Uc, Ucs) * W * SW, Where Uc is the commodity score, Ucs is the\n                    content set score, W is the weight tuned the on mixer, and SW is\n                    the weight tuned on the super interaction.\n                    ',
                    tuning_group=GroupNames.AUTONOMY,
                    tunable_type=int,
                    default=0),
                social_context_preference=TunableMapping(
                    description=
                    '\n                    A mapping of social contexts that will adjust the content score for\n                    this interaction. This is used conjunction with base_score.\n                    ',
                    tuning_group=GroupNames.AUTONOMY,
                    key_type=SocialContextBit.TunableReference(pack_safe=True),
                    value_type=Tunable(tunable_type=float, default=0)),
                relationship_bit_preference=TunableMapping(
                    description=
                    '\n                    A mapping of relationship bits that will adjust the content score\n                    for this interaction. This is used conjunction with\n                    base_score.\n                    ',
                    tuning_group=GroupNames.AUTONOMY,
                    key_type=RelationshipBit.TunableReference(pack_safe=True),
                    value_type=Tunable(tunable_type=float, default=0)),
                trait_preference=TunableMapping(
                    description=
                    '\n                    A mapping of traits that will adjust the content score for\n                    this interaction. This is used conjunction with base_score.\n                    ',
                    tuning_group=GroupNames.AUTONOMY,
                    key_type=Trait.TunableReference(pack_safe=True),
                    value_type=Tunable(tunable_type=float, default=0)),
                buff_preference=TunableMapping(
                    description=
                    '\n                    A mapping of buffs that will adjust the content score for\n                    this interaction. This is used in conjunction with base_score.\n                    ',
                    tuning_group=GroupNames.AUTONOMY,
                    key_type=TunableReference(
                        manager=services.get_instance_manager(
                            sims4.resources.Types.BUFF),
                        pack_safe=True),
                    value_type=Tunable(tunable_type=float, default=0)),
                buff_target_preference=TunableMapping(
                    description=
                    '\n                    A mapping of buffs on the target that will adjust the \n                    content score for this interaction. This is used in conjunction \n                    with base_score.\n                    Preferably, this will be combined with buff_preference\n                    and merged with a participant type.\n                    ',
                    tuning_group=GroupNames.AUTONOMY,
                    key_type=TunableReference(
                        manager=services.get_instance_manager(
                            sims4.resources.Types.BUFF),
                        pack_safe=True),
                    value_type=Tunable(tunable_type=float, default=0)),
                test_gender_preference=Tunable(
                    description=
                    '\n                    If this is set, a gender preference test will be run between \n                    the actor and target sims. If it fails, the social score will be\n                    modified by a large negative penalty tuned with the tunable:\n                    GENDER_PREF_CONTENT_SCORE_PENALTY\n                    ',
                    tuning_group=GroupNames.AUTONOMY,
                    tunable_type=bool,
                    default=False),
                topic_preferences=TunableSet(
                    description=
                    ' \n                    A set of topics that will increase the content score for this \n                    interaction.  If a sim has a topic that exist in this\n                    set, a value tuned in that topic will increase the content\n                    score.  This is used conjunction with base_score.\n                    ',
                    tunable=TunableReference(
                        description=
                        '\n                        The Topic this interaction gets bonus score for. Amount of\n                        score is tuned on the Topic.\n                        ',
                        manager=services.get_instance_manager(
                            sims4.resources.Types.TOPIC)),
                    tuning_group=GroupNames.AUTONOMY),
                mood_preference=TunableMapping(
                    description=
                    "\n                    A mapping of moods that will adjust the content score for this \n                    interaction.  If sim's mood exist in this mapping, the\n                    value mapped to mood will add to the content score.  This is\n                    used conjunction with base_score.\n                    ",
                    key_type=TunableReference(
                        manager=services.get_instance_manager(
                            sims4.resources.Types.MOOD)),
                    value_type=Tunable(tunable_type=float, default=0),
                    tuning_group=GroupNames.AUTONOMY),
                front_page_cooldown=OptionalTunable(
                    description=
                    '\n                    If Enabled, when you run this mixer, it will get a penalty\n                    applied to the front page score of this interaction for a tunable\n                    amount of time. If The interaction is run more than once, the\n                    cooldown will be re-applied, and the penalty will stack making\n                    the mixer less likely to be on the front page as you execute it\n                    more.\n                    ',
                    tunable=TunableTuple(
                        interval=TunableInterval(
                            description=
                            '\n                            Time in minutes until the penalty on the front page score\n                            expires.\n                            ',
                            tunable_type=TunableSimMinute,
                            default_lower=1,
                            default_upper=1,
                            minimum=0),
                        penalty=Tunable(
                            description=
                            '\n                            For the duration of the tuned interval, this penalty\n                            will be applied to the score used to determine which\n                            interactions are visible on the front page of the pie\n                            menu. The higher this number, the less likely it will\n                            be to see the interaction at the top level.\n                            ',
                            tunable_type=int,
                            default=0)),
                    tuning_group=GroupNames.AUTONOMY)),
            enabled_by_default=True)
    }

    @classmethod
    def get_base_content_set_score(cls, **kwargs):
        return cls.content_score.base_score

    @classmethod
    def get_content_score(cls,
                          sim,
                          resolver,
                          internal_aops,
                          gsi_logging=None,
                          **kwargs):
        if cls.content_score is None:
            return 0
        base_score = cls.get_base_content_set_score(**kwargs)
        if sim is None:
            logger.error('Sim is None when trying to get content score for {}',
                         cls)
            return base_score
        buff_score_adjustment = sim.get_actor_scoring_modifier(cls, resolver)
        topic_score = sum(
            topic.score_for_sim(sim)
            for topic in cls.content_score.topic_preferences)
        score_modifier = sum(
            cls.get_score_modifier(sim, internal_aop.target)
            for internal_aop in internal_aops)
        front_page_cooldown_penalty = sim.get_front_page_penalty(cls)
        club_service = services.get_club_service()
        if club_service is not None:
            club_rules_modifier = sum(
                club_service.get_front_page_bonus_for_mixer(sim.sim_info, aop)
                for aop in internal_aops)
        else:
            club_rules_modifier = 0
        total_score = base_score + buff_score_adjustment + topic_score + score_modifier + front_page_cooldown_penalty + club_rules_modifier
        if gsi_logging is not None:
            if cls not in gsi_logging:
                gsi_logging[cls] = {}
            gsi_logging[cls]['scored_aop'] = str(cls)
            gsi_logging[cls]['base_score'] = base_score
            gsi_logging[cls]['buff_score_adjustment'] = buff_score_adjustment
            gsi_logging[cls]['topic_score'] = topic_score
            gsi_logging[cls]['score_modifier'] = score_modifier
            gsi_logging[cls]['total_score'] = total_score
        return total_score
Exemple #9
0
class Relationship:
    __qualname__ = 'Relationship'
    MIN_RELATIONSHIP_VALUE = Tunable(
        float, -100, description='The minimum value any relationship can be.')
    MAX_RELATIONSHIP_VALUE = Tunable(
        float, 100, description='The maximum value any relationship can be.')
    DEFAULT_RELATIONSHIP_VALUE = Tunable(
        float,
        0,
        description='The default value for relationship track scores.')
    DEFAULT_SHORT_TERM_CONTEXT_TRACK = RelationshipTrack.TunableReference(
        description=
        '\n        If no short-term context tracks exist for a relationship, use this\n        default as the prevailing track.\n        '
    )
    DELAY_UNTIL_RELATIONSHIP_IS_CULLED = Tunable(
        description=
        '\n                                                    The amount of time, in sim minutes, that it takes before \n                                                    a relationship is culled once all of the tracks have reached\n                                                    convergence.\n                                                    ',
        tunable_type=int,
        default=10)
    MARRIAGE_RELATIONSHIP_BIT = RelationshipBit.TunableReference(
        description=
        "\n        The marriage relationship bit. This tuning references the relationship bit signifying that \n        the sim is a spouse to someone. Whenever this bit is added to a sim's relationship, it has \n        the side effect of updating the spouse_sim_id on a sim's relationship tracker. If the bit \n        goes away, the field is cleared. \n        "
    )
    SIGNIFICANT_OTHER_RELATIONSHIP_BIT = RelationshipBit.TunableReference(
        description=
        '\n        The significant other relationship bit. This tuning references the relationship bit signifying that \n        the sim is a significant other to someone.\n        '
    )

    def __init__(self, tracker, sim_id, target_sim_id):
        self._tracker = tracker
        self._sim_id = sim_id
        self._target_sim_id = target_sim_id
        self._bits = {}
        self._cached_depth = 0
        self._cached_depth_dirty = True
        self._bit_timeouts = []
        self._bit_track_tracker = RelationshipTrackTracker(self)
        self._level_change_watcher_id = self._bit_track_tracker.add_watcher(
            self._value_changed)
        self._knowledge = None
        self._culling_alarm_handle = None
        self.bit_added_buffs = defaultdict(list)

    def __repr__(self):
        if services.sim_info_manager():
            return 'Relationship: {} & {}'.format(self.find_sim_info(),
                                                  self.find_target_sim_info())
        return 'Relationship: {} & {}'.format(self.sim_id, self.target_sim_id)

    def destroy(self, notify_client=True):
        if notify_client:
            self._send_destroy_message_to_client()
        self._bit_track_tracker.remove_watcher(self._level_change_watcher_id)
        self._level_change_watcher_id = None
        self._destroy_culling_alarm()
        self._tracker = None
        self._bits.clear()
        self.bit_added_buffs.clear()
        self._bit_timeouts.clear()
        self._bit_track_tracker.destroy()
        self._bit_track_tracker = None
        self._knowledge = None

    def _value_changed(self, stat_type, old_value, new_value):
        if stat_type.causes_delayed_removal_on_convergence:
            self._destroy_culling_alarm()

    @property
    def ID(self):
        return self.relationship_id

    @property
    def relationship_id(self):
        return self._target_sim_id

    def find_sim_info(self):
        sim_info = services.sim_info_manager().get(self.sim_id)
        return sim_info

    def find_target_sim_info(self):
        target_sim_info = services.sim_info_manager().get(self.target_sim_id)
        return target_sim_info

    def find_sim(self):
        sim_info = self.find_sim_info()
        if sim_info is not None:
            return sim_info.get_sim_instance()

    def find_target_sim(self):
        target_sim_info = self.find_target_sim_info()
        if target_sim_info is not None:
            return target_sim_info.get_sim_instance()

    def _find_matching_relationship(self):
        target_sim_info = self.find_target_sim_info()
        if target_sim_info is None:
            logger.error("Couldn't find matching relationship object for {}.",
                         self)
            return
        target_relationship = target_sim_info.relationship_tracker._find_relationship(
            self._sim_id)
        return target_relationship

    @property
    def sim(self):
        logger.error('Deprecated: Use find_sim() or find_sim_info() instead.')
        return self.find_sim()

    @property
    def target(self):
        logger.error(
            'Deprecated: Use find_target_sim() or find_target_sim_info() instead.'
        )
        return self.find_target_sim()

    @property
    def sim_id(self):
        return self._sim_id

    @property
    def target_sim_id(self):
        return self._target_sim_id

    @property
    def bit_track_tracker(self):
        return self._bit_track_tracker

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

    def get_knowledge(self, initialize=False):
        if initialize and self._knowledge is None:
            self._knowledge = SimKnowledge(self)
        return self._knowledge

    def get_persistance_protocol_buffer(self):
        save_data = protocols.PersistableRelationship()
        save_data.target_id = self._target_sim_id
        for bit in self._bits:
            while bit.persisted:
                save_data.bits.append(bit.guid64)
        for (bit_id, buff_ids) in self.bit_added_buffs.items():
            with ProtocolBufferRollback(
                    save_data.bit_added_buffs) as bit_added_buff:
                bit_added_buff.bit_id = bit_id
                bit_added_buff.buff_ids.extend(buff_ids)
        for timeout in self._bit_timeouts:
            timeout_proto_buffer = save_data.timeouts.add()
            timeout_proto_buffer.timeout_bit_id_hash = timeout.bit.guid64
            timeout_proto_buffer.elapsed_time = timeout.get_elapsed_time()
        for track in self._bit_track_tracker:
            while track.persisted:
                track_proto_buffer = save_data.tracks.add()
                track_proto_buffer.track_id = track.type_id()
                track_proto_buffer.value = track.get_value()
                track_proto_buffer.visible = track.visible_to_client
        if self._knowledge is not None:
            save_data.knowledge = self._knowledge.get_save_data()
        return save_data

    def load(self, sim_info, rel_data):
        try:
            track_manager = services.get_instance_manager(
                sims4.resources.Types.STATISTIC)
            try:
                self._bit_track_tracker.suppress_callback_setup_during_load = True
                for track_data in rel_data.tracks:
                    track_type = track_manager.get(track_data.track_id)
                    track_inst = self._bit_track_tracker.add_statistic(
                        track_type)
                    if track_inst is not None:
                        track_inst.set_value(track_data.value)
                        track_inst.update_instance_data()
                        track_inst.visible_to_client = track_data.visible
                        track_inst.fixup_callbacks_during_load()
                    else:
                        logger.warn(
                            'Failed to load track {} on sim {}.  This is valid if the tuning has changed.',
                            track_type,
                            sim_info,
                            owner='rez')
            finally:
                self._bit_track_tracker.suppress_callback_setup_during_load = False
            bit_manager = services.get_instance_manager(
                sims4.resources.Types.RELATIONSHIP_BIT)
            logger.assert_raise(
                bit_manager, 'Unable to retrieve relationship bit manager.')
            bit_list = [
                bit_manager.get(bit_instance_id)
                for bit_instance_id in rel_data.bits
            ]
            for bit_added_buff in rel_data.bit_added_buffs:
                self.bit_added_buffs[bit_added_buff.bit_id] = list(
                    bit_added_buff.buff_ids)
            while len(bit_list):
                bit = bit_list.pop()
                if bit is None:
                    logger.error('Loading None bit for sim {}.',
                                 sim_info,
                                 owner='rez')
                    continue
                if self.has_bit(bit):
                    continue
                while not self.add_bit(
                        bit,
                        False,
                        bit_list,
                        bit_added_buffs=self.bit_added_buffs.get(
                            bit.guid64, None)):
                    logger.warn(
                        'Failed to load relationship bit {} for sim {}.  This is valid if tuning has changed.',
                        bit, sim_info)
                    continue
            if rel_data.timeouts is not None:
                for timeout_save in rel_data.timeouts:
                    bit = bit_manager.get(timeout_save.timeout_bit_id_hash)
                    timeout_data = self._find_timeout_data_by_bit(bit)
                    if timeout_data is not None:
                        self.remove_bit(bit, False)
                    else:
                        logger.warn(
                            'Attempting to load timeout value on bit {} with no timeout.  This is valid if tuning has changed.',
                            bit)
            while rel_data.knowledge is not None:
                self._knowledge = SimKnowledge(self)
                self._knowledge.load(rel_data.knowledge)
        except Exception:
            logger.exception(
                'Exception thrown while loading relationship data for Sim {}',
                sim_info,
                owner='rez')

    def add_neighbor_bit_if_necessary(self, sim_info):
        target_sim_info = self.find_target_sim_info()
        if target_sim_info is None:
            return
        if sim_info.household is None or target_sim_info.household is None:
            return
        home_zone_id = sim_info.household.home_zone_id
        target_home_zone_id = target_sim_info.household.home_zone_id
        if home_zone_id == target_home_zone_id:
            return
        if home_zone_id == 0 or target_home_zone_id == 0:
            return
        sim_home_zone_proto_buffer = services.get_persistence_service(
        ).get_zone_proto_buff(home_zone_id)
        target_sim_home_zone_proto_buffer = services.get_persistence_service(
        ).get_zone_proto_buff(target_home_zone_id)
        if sim_home_zone_proto_buffer is None or target_sim_home_zone_proto_buffer is None:
            logger.error(
                'Invalid zone protocol buffer in Relationship.add_neighbor_bit_if_necessary()'
            )
            return
        if sim_home_zone_proto_buffer.world_id != target_sim_home_zone_proto_buffer.world_id:
            return
        self.add_bit(global_relationship_tuning.RelationshipGlobalTuning.
                     NEIGHBOR_RELATIONSHIP_BIT,
                     notify_client=False)
        target_relationship = self._tracker._find_relationship(sim_info.id,
                                                               create=False)
        if target_relationship is not None:
            target_relationship.add_bit(
                global_relationship_tuning.RelationshipGlobalTuning.
                NEIGHBOR_RELATIONSHIP_BIT,
                notify_client=False)

    def send_relationship_info(self, deltas=None):
        self._notify_client(deltas)

    def get_track_score(self, track):
        return self._bit_track_tracker.get_user_value(track)

    def set_track_score(self, value, track):
        self._bit_track_tracker.set_value(track, value)

    def add_track_score(self, increment, track):
        self._bit_track_tracker.add_value(track, increment)

    def enable_selectable_sim_track_decay(self, to_enable=True):
        self._bit_track_tracker.enable_selectable_sim_track_decay(to_enable)

    def get_track_utility_score(self, track):
        track_inst = self._bit_track_tracker.get_statistic(track)
        if track_inst is not None:
            return track_inst.autonomous_desire
        return track.autonomous_desire

    def get_track(self, track, add=False):
        return self._bit_track_tracker.get_statistic(track, add)

    def get_highest_priority_track_bit(self):
        highest_priority_bit = None
        for track in self._bit_track_tracker:
            bit = track.get_active_bit()
            if not bit:
                pass
            while highest_priority_bit is None or bit.priority > highest_priority_bit.priority:
                highest_priority_bit = bit
        return highest_priority_bit

    def get_prevailing_short_term_context_track(self):
        tracks = [
            track for track in self._bit_track_tracker
            if track.is_short_term_context
        ]
        if tracks:
            return max(tracks, key=lambda t: abs(t.get_value()))
        return self.get_track(self.DEFAULT_SHORT_TERM_CONTEXT_TRACK, add=True)

    def track_reached_convergence(self, track_instance):
        if track_instance.causes_delayed_removal_on_convergence and self._can_cull_relationship(
        ):
            logger.debug('{} has been marked for culling.', self)
            self._create_culling_alarm()
        if track_instance.is_visible:
            logger.debug('Notifying client that {} has reached convergence.',
                         self)
            self._notify_client()

    def apply_social_group_decay(self):
        for track in self._bit_track_tracker:
            track.apply_social_group_decay()
        target_relationship = self._find_matching_relationship()
        if target_relationship is not None:
            for track in target_relationship.bit_track_tracker:
                track.apply_social_group_decay()
        else:
            logger.warn(
                "Couldn't apply social group decay to both sides of the relationship for {}",
                self)
            self.find_sim().log_sim_info(logger.warn)
            self.find_target_sim().log_sim_info(logger.warn)

    def remove_social_group_decay(self):
        for track in self._bit_track_tracker:
            track.remove_social_group_decay()
        target_relationship = self._find_matching_relationship()
        if target_relationship is not None:
            for track in target_relationship.bit_track_tracker:
                track.remove_social_group_decay()
        else:
            logger.warn(
                "Couldn't remove social group decay to both sides of the relationship for {}",
                self)

    def add_bit(self,
                bit,
                notify_client=True,
                pending_bits=None,
                bit_added_buffs=None):
        logger.assert_raise(
            bit is not None,
            'Error: Sim Id: {} trying to add a None relationship bit to Sim_Id: {}.'
            .format(self._sim_id, self._target_sim_id))
        compatibility_bit_list = [key for key in self._bits.keys()]
        if pending_bits is not None:
            compatibility_bit_list.extend(pending_bits)
        required_bit_count = len(bit.required_bits)
        bit_to_remove = None
        for curr_bit in compatibility_bit_list:
            if curr_bit is bit:
                logger.debug('Attempting to add duplicate bit {} on {}', bit,
                             self)
                return False
            if required_bit_count and curr_bit in bit.required_bits:
                required_bit_count -= 1
            while bit.group_id != RelationshipBitType.NoGroup and bit.group_id == curr_bit.group_id:
                if bit.priority >= curr_bit.priority:
                    logger.assert_raise(
                        bit_to_remove is None,
                        'Multiple relationship bits of the same type are set on a single relationship: {}'
                        .format(self))
                    bit_to_remove = curr_bit
                else:
                    logger.debug(
                        'Failed to add bit {}; existing bit {} has higher priority for {}',
                        bit, curr_bit, self)
                    return False
        if bit.remove_on_threshold:
            track_val = self._bit_track_tracker.get_value(
                bit.remove_on_threshold.track)
            if bit.remove_on_threshold.threshold.compare(track_val):
                logger.debug(
                    'Failed to add bit {}; track {} meets the removal threshold {} for {}',
                    bit, bit.remove_on_threshold.track,
                    bit.remove_on_threshold.threshold, self)
                return False
        if required_bit_count > 0:
            logger.debug('Failed to add bit {}; required bit count is {}', bit,
                         required_bit_count)
            return False
        if bit_to_remove is not None:
            self.remove_bit(bit_to_remove, False)
        bit_instance = bit()
        if bit_added_buffs is not None:
            bit_instance.bit_added_buffs.extend(bit_added_buffs)
        self._bits[bit] = bit_instance
        self._cached_depth_dirty = True
        logger.debug('Added bit {} for {}', bit, self)
        sim_info = self.find_sim_info()
        if sim_info is not None:
            target_sim_info = self.find_target_sim_info()
            sim = sim_info.get_sim_instance()
            if sim is not None and target_sim_info is not None:
                bit_instance.on_add_to_relationship(sim, target_sim_info, self)
            with telemetry_helper.begin_hook(writer,
                                             TELEMETRY_HOOK_ADD_BIT,
                                             sim=sim_info) as hook:
                hook.write_int(TELEMETRY_FIELD_TARGET_ID, self._target_sim_id)
                hook.write_int(TELEMETRY_FIELD_REL_ID, self.ID)
                hook.write_int(TELEMETRY_FIELD_BIT_ID, bit.guid64)
            try:
                services.get_event_manager().process_event(
                    event_testing.test_events.TestEvent.AddRelationshipBit,
                    sim_info=sim_info,
                    relationship_bit=bit,
                    sim_id=self._sim_id,
                    target_sim_id=self._target_sim_id)
            except Exception:
                logger.warn(
                    'Threw error while attempting to process achievement on bit add.',
                    owner='rez')
        if self.suppress_client_updates or bit is Relationship.MARRIAGE_RELATIONSHIP_BIT:
            if sim_info is not None:
                sim_info.update_spouse_sim_id(self._target_sim_id)
        if bit.timeout > 0:
            timeout_data = self._find_timeout_data_by_bit(bit)
            if timeout_data is None:
                timeout_data = BitTimeoutData(bit,
                                              self._timeout_alarm_callback)
                self._bit_timeouts.append(timeout_data)
            timeout_data.reset_alarm()
        if bit.remove_on_threshold:
            track_type = bit.remove_on_threshold.track
            listener = self._bit_track_tracker.create_and_activate_listener(
                track_type, bit.remove_on_threshold.threshold,
                self._remove_bit_due_to_track_threshold_callback)
            bit_instance.add_conditional_removal_listener(listener)
        if sim_info:
            bit_instance.add_appropriateness_buffs(sim_info)
        if notify_client is True:
            self._notify_client()
        return True

    def remove_bit(self, bit, notify_client=True):
        logger.assert_raise(
            bit is not None,
            'Error: Sim Id: {} trying to remove a None relationship bit to Sim_Id: {}.'
            .format(self._sim_id, self._target_sim_id))
        if bit not in self._bits:
            logger.debug(
                "Attempting to remove bit for {} that doesn't exist: {}", self,
                bit)
            return
        sim_info = self.find_sim_info()
        bit_instance = self._bits[bit]
        if self.suppress_client_updates or sim_info is not None:
            with telemetry_helper.begin_hook(writer,
                                             TELEMETRY_HOOK_REMOVE_BIT,
                                             sim=sim_info) as hook:
                hook.write_int(TELEMETRY_FIELD_TARGET_ID, self._target_sim_id)
                hook.write_int(TELEMETRY_FIELD_REL_ID, self.ID)
                hook.write_int(TELEMETRY_FIELD_BIT_ID, bit.guid64)
                try:
                    services.get_event_manager().process_event(
                        event_testing.test_events.TestEvent.
                        RemoveRelationshipBit,
                        sim_info=sim_info,
                        relationship_bit=bit,
                        sim_id=self._sim_id,
                        target_sim_id=self._target_sim_id)
                except Exception:
                    logger.warn(
                        'Threw error while attempting to process achievement on bit add.  If you used a cheat, this is fine.',
                        owner='rez')
            sim = sim_info.get_sim_instance()
            if sim is not None:
                target_sim_info = self.find_target_sim_info()
                if target_sim_info is not None:
                    bit_instance.on_remove_from_relationship(
                        sim, target_sim_info)
            if bit is Relationship.MARRIAGE_RELATIONSHIP_BIT:
                sim_info.update_spouse_sim_id(None)
        del self._bits[bit]
        self._cached_depth_dirty = True
        logger.debug('Removed bit {} for {}', bit, self)
        timeout_data = self._find_timeout_data_by_bit(bit)
        if timeout_data is not None:
            timeout_data.cancel_alarm()
            self._bit_timeouts.remove(timeout_data)
        if bit.remove_on_threshold:
            listener = bit_instance.remove_conditional_removal_listener()
            if listener is not None:
                self._bit_track_tracker.remove_listener(listener)
            else:
                logger.error(
                    "Bit {} is meant to have a listener on track {} but it doesn't for {}."
                    .format(bit, bit.remove_on_threshold.track, self))
        if sim_info:
            bit_instance.remove_appropriateness_buffs(sim_info)
        if notify_client is True:
            self._notify_client()

    def get_bits(self):
        return self._bits.keys()

    def _timeout_alarm_callback(self, alarm_handle):
        timeout_data = self._find_timeout_data_by_handle(alarm_handle)
        if timeout_data is not None:
            self.remove_bit(timeout_data.bit)
        else:
            logger.error('Failed to find alarm handle in _bit_timeouts list')

    def has_bit(self, bit):
        return any(bit.matches_bit(bit_type) for bit_type in self.get_bits())

    def get_bit_instance(self, bit_type):
        if bit_type in self._bits:
            return self._bits[bit_type]

    def get_highest_priority_bit(self):
        highest_priority_bit = None
        for bit in self._bits.keys():
            while highest_priority_bit is None or bit.priority > highest_priority_bit.priority:
                highest_priority_bit = bit
        return highest_priority_bit

    def add_historical_bits_on_age_up(self, current_age):
        historical_bits_to_add = set()
        for bit in self._bits:
            while bit.historical_bits is not None:
                while True:
                    for historical_bit_data in bit.historical_bits:
                        while historical_bit_data.age_trans_from == current_age:
                            historical_bits_to_add.add(
                                historical_bit_data.new_historical_bit)
        for new_bit in historical_bits_to_add:
            self.add_bit(new_bit)

    def add_relationship_appropriateness_buffs(self):
        sim_info = self.find_sim_info()
        for bit in self._bits.values():
            bit.add_appropriateness_buffs(sim_info)

    def build_printable_string_of_bits(self):
        return '\t\t{}'.format('\n\t\t'.join(map(str, self._bits)))

    def build_printable_string_of_tracks(self):
        ret = ''
        for track in self._bit_track_tracker:
            ret += '\t\t{} = {}; decaying? {}; decay rate: {}\n'.format(
                track, track.get_value(), track.decay_enabled,
                track.get_decay_rate())
        return ret

    @property
    def depth(self):
        if self._cached_depth_dirty:
            self._refresh_depth_cache()
        return self._cached_depth

    def _refresh_depth_cache(self):
        self._cached_depth = 0
        for key in self._bits:
            pass
        self._cached_depth_dirty = False

    def _find_timeout_data_by_bit(self, bit):
        for data in self._bit_timeouts:
            while bit is data.bit:
                return data

    def _find_timeout_data_by_handle(self, alarm_handle):
        for data in self._bit_timeouts:
            while alarm_handle is data.alarm_handle:
                return data

    def _remove_bit_due_to_track_threshold_callback(self, track):
        for bit in self._bits.keys():
            while bit.remove_on_threshold and bit.remove_on_threshold.track is type(
                    track):
                self.remove_bit(bit)
                return
        logger.error(
            "Got a callback to remove a bit for track {}, but one doesn't exist."
            .format(track),
            owner='rez')

    def _build_relationship_update_proto(self, deltas=None):
        msg = commodity_protocol.RelationshipUpdate()
        msg.actor_sim_id = self._sim_id
        msg.target_id.object_id = self._target_sim_id
        msg.target_id.manager_id = services.sim_info_manager().id
        client_tracks = [
            track for track in self._bit_track_tracker
            if track.display_priority > 0
        ]
        client_tracks.sort(key=lambda track: track.display_priority)
        track_bits = set()
        for track in client_tracks:
            if track.visible_to_client:
                with ProtocolBufferRollback(
                        msg.tracks) as relationship_track_update:
                    relationship_track_update.track_score = track.get_value()
                    relationship_track_update.track_bit_id = track.get_bit_for_client(
                    ).guid64
                    relationship_track_update.track_id = track.guid64
                    relationship_track_update.track_popup_priority = track.display_popup_priority
                    relationship_track_update.change_rate = track.get_change_rate(
                    )
                    while deltas is not None:
                        track_delta = deltas.get(track)
                        while track_delta is not None:
                            relationship_track_update.delta = track_delta
            track_bits.add(track.get_bit_for_client().guid64)
        for bit in self._bits.values():
            if not bit.visible:
                pass
            if bit.guid64 in track_bits:
                pass
            msg.bit_ids.append(bit.guid64)
        if self._knowledge is not None:
            msg.num_traits = self._knowledge.num_traits
            for trait in self._knowledge.known_traits:
                msg.known_trait_ids.append(trait.guid64)
        target_sim_info = self.find_target_sim_info()
        if target_sim_info is not None and target_sim_info.spouse_sim_id is not None:
            msg.target_sim_significant_other_id = target_sim_info.spouse_sim_id
        return msg

    def _notify_client(self, deltas=None):
        if self.suppress_client_updates:
            return
        sim_info = self.find_sim_info()
        if sim_info is not None:
            send_relationship_op(
                sim_info, self._build_relationship_update_proto(deltas=deltas))

    def _send_destroy_message_to_client(self):
        msg = commodity_protocol.RelationshipDelete()
        msg.actor_sim_id = self._sim_id
        msg.target_id = self._target_sim_id
        op = GenericProtocolBufferOp(
            DistributorOps_pb2.Operation.SIM_RELATIONSHIP_DELETE, msg)
        distributor = Distributor.instance()
        distributor.add_op(self.find_sim_info(), op)

    def _create_culling_alarm(self):
        self._destroy_culling_alarm()
        time_range = date_and_time.create_time_span(
            minutes=self.DELAY_UNTIL_RELATIONSHIP_IS_CULLED)
        self._culling_alarm_handle = alarms.add_alarm(
            self, time_range, self._cull_relationship_callback)

    def _destroy_culling_alarm(self):
        if self._culling_alarm_handle is not None:
            alarms.cancel_alarm(self._culling_alarm_handle)
            self._culling_alarm_handle = None

    def _cull_relationship_callback(self, _):
        self._destroy_culling_alarm()
        if self._can_cull_relationship():
            target_sim_info = self.find_target_sim_info()
            logger.debug('Culling {}', self)
            target_sim_info.relationship_tracker.destroy_relationship(
                self.sim_id)
            self._tracker.destroy_relationship(self.target_sim_id)
        else:
            logger.warn("Attempting to cull {} but it's no longer allowed.",
                        self)

    def _can_cull_relationship(self):
        sim_info = self.find_sim_info()
        target_sim_info = self.find_target_sim_info()
        if sim_info is not None and target_sim_info is not None and sim_info.household_id == target_sim_info.household_id:
            return False
        if not self._bit_track_tracker.are_all_tracks_that_cause_culling_at_convergence(
        ):
            return False
        for bit in self._bits.values():
            while bit.prevents_relationship_culling:
                return False
        return True
class PregnancyTracker:
    __qualname__ = 'PregnancyTracker'
    PREGNANCY_COMMODITY = TunableReference(
        description=
        '\n        The commodity to award if conception is successful.\n        ',
        manager=services.statistic_manager())
    PREGNANCY_TRAIT = TunableReference(
        description=
        '\n        The trait that all pregnant Sims have during pregnancy.\n        ',
        manager=services.trait_manager())
    PREGNANCY_RATE = TunableRange(
        description='\n        The rate per Sim minute of pregnancy.\n        ',
        tunable_type=float,
        default=0.001,
        minimum=EPSILON)
    PREGNANCY_DIALOG = SimPersonalityAssignmentDialog.TunableFactory(
        description=
        "\n        The dialog that is displayed when an offspring is created. It allows the\n        player to enter a first and last name for the Sim. An additional token\n        is passed in: the offspring's Sim data.\n        ",
        text_inputs=(TEXT_INPUT_FIRST_NAME, TEXT_INPUT_LAST_NAME))
    MULTIPLE_OFFSPRING_CHANCES = TunableList(
        description=
        '\n        A list defining the probabilities of multiple births.\n        ',
        tunable=TunableTuple(
            size=Tunable(
                description=
                '\n                The number of offspring born.\n                ',
                tunable_type=int,
                default=1),
            weight=Tunable(
                description=
                '\n                The weight, relative to other outcomes.\n                ',
                tunable_type=float,
                default=1),
            npc_dialog=UiDialogOk.TunableFactory(
                description=
                '\n                A dialog displayed when a NPC Sim gives birth to an offspring\n                that was conceived by a currently player-controlled Sim. The\n                dialog is specifically used when this number of offspring is\n                generated.\n                \n                Three tokens are passed in: the two parent Sims and the offspring\n                ',
                locked_args={'text_tokens': None}),
            modifiers=TunableMultiplier.TunableFactory(
                description=
                '\n                A tunable list of test sets and associated multipliers to apply to \n                the total chance of this number of potential offspring.\n                '
            ),
            screen_slam_one_parent=OptionalTunable(
                description=
                '\n                Screen slam to show when only one parent is available.\n                Localization Tokens: Sim A - {0.SimFirstName}\n                ',
                tunable=ui.screen_slam.TunableScreenSlamSnippet()),
            screen_slam_two_parents=OptionalTunable(
                description=
                '\n                Screen slam to show when both parents are available.\n                Localization Tokens: Sim A - {0.SimFirstName}, Sim B - {1.SimFirstName}\n                ',
                tunable=ui.screen_slam.TunableScreenSlamSnippet())))
    MONOZYGOTIC_OFFSPRING_CHANCE = TunablePercent(
        description=
        '\n        The chance that each subsequent offspring of a multiple birth has the\n        same genetics as the first offspring.\n        ',
        default=50)
    BIRTHPARENT_BIT = RelationshipBit.TunableReference(
        description=
        '\n        The bit that is added on the relationship from the Sim to any of its\n        offspring.\n        '
    )

    def __init__(self, sim_info):
        self._sim_info = sim_info
        self._last_modified = None
        self.clear_pregnancy()

    @property
    def account(self):
        return self._sim_info.account

    @property
    def is_pregnant(self):
        if self._seed:
            return True
        return False

    @property
    def offspring_count(self):
        return max(len(self._offspring_data), 1)

    def _get_parent(self, sim_id):
        sim_info_manager = services.sim_info_manager()
        if sim_id in sim_info_manager:
            return sim_info_manager.get(sim_id)

    def get_parents(self):
        if self._parent_ids:
            parent_a = self._get_parent(self._parent_ids[0])
            parent_b = self._get_parent(self._parent_ids[1]) or parent_a
            return (parent_a, parent_b)
        return (None, None)

    def get_partner(self):
        (owner, partner) = self.get_parents()
        if partner is not owner:
            return partner

    def start_pregnancy(self, parent_a, parent_b):
        if not self.is_pregnant:
            self._seed = random.randint(1, MAX_UINT32)
            self._parent_ids = (parent_a.id, parent_b.id)
            self._offspring_data = []
            self.enable_pregnancy()

    def enable_pregnancy(self):
        if self.is_pregnant and not self._is_enabled:
            tracker = self._sim_info.get_tracker(self.PREGNANCY_COMMODITY)
            pregnancy_commodity = tracker.get_statistic(
                self.PREGNANCY_COMMODITY, add=True)
            pregnancy_commodity.add_statistic_modifier(self.PREGNANCY_RATE)
            trait_tracker = self._sim_info.trait_tracker
            trait_tracker.add_trait(self.PREGNANCY_TRAIT)
            self._last_modified = None
            self._is_enabled = True

    def update_pregnancy(self):
        if self.is_pregnant:
            if self._last_modified is not None:
                tracker = self._sim_info.get_tracker(self.PREGNANCY_COMMODITY)
                pregnancy_commodity = tracker.get_statistic(
                    self.PREGNANCY_COMMODITY, add=True)
                if pregnancy_commodity.get_value(
                ) >= self.PREGNANCY_COMMODITY.max_value:
                    self.create_offspring_data()
                    for offspring_data in self.get_offspring_data_gen():
                        offspring_data.first_name = self._get_random_first_name(
                            offspring_data)
                        self.create_sim_info(offspring_data)
                    self._show_npc_dialog()
                    self.clear_pregnancy()
                else:
                    delta_time = services.time_service(
                    ).sim_now - self._last_modified
                    delta = self.PREGNANCY_RATE * delta_time.in_minutes()
                    pregnancy_commodity.add_value(delta)
            self._last_modified = services.time_service().sim_now

    def complete_pregnancy(self):
        services.get_event_manager().process_event(
            TestEvent.OffspringCreated,
            sim_info=self._sim_info,
            offspring_created=self.offspring_count)
        for tuning_data in self.MULTIPLE_OFFSPRING_CHANCES:
            while tuning_data.size == self.offspring_count:
                (parent_a, parent_b) = self.get_parents()
                if parent_a is parent_b:
                    screen_slam = tuning_data.screen_slam_one_parent
                else:
                    screen_slam = tuning_data.screen_slam_two_parents
                if screen_slam is not None:
                    screen_slam.send_screen_slam_message(
                        self._sim_info, parent_a, parent_b)
                break

    def clear_pregnancy_visuals(self):
        if self._sim_info.pregnancy_progress:
            self._sim_info.pregnancy_progress = 0

    def clear_pregnancy(self):
        self._seed = 0
        self._parent_ids = []
        self._offspring_data = []
        tracker = self._sim_info.get_tracker(self.PREGNANCY_COMMODITY)
        stat = tracker.get_statistic(self.PREGNANCY_COMMODITY)
        if stat is not None:
            tracker.set_min(self.PREGNANCY_COMMODITY)
            stat.remove_statistic_modifier(self.PREGNANCY_RATE)
        trait_tracker = self._sim_info.trait_tracker
        if trait_tracker.has_trait(self.PREGNANCY_TRAIT):
            trait_tracker.remove_trait(self.PREGNANCY_TRAIT)
        self.clear_pregnancy_visuals()
        self._is_enabled = False

    def create_sim_info(self, offspring_data):
        (parent_a, parent_b) = self.get_parents()
        sim_creator = SimCreator(gender=offspring_data.gender,
                                 age=Age.BABY,
                                 first_name=offspring_data.first_name,
                                 last_name=offspring_data.last_name)
        household = self._sim_info.household
        zone_id = household.home_zone_id
        (sim_info_list, _) = SimSpawner.create_sim_infos(
            (sim_creator, ),
            household=household,
            account=self.account,
            zone_id=zone_id,
            generate_deterministic_sim=True,
            creation_source='pregnancy')
        sim_info = sim_info_list[0]
        generate_offspring(parent_a._base,
                           parent_b._base,
                           sim_info._base,
                           seed=offspring_data.genetics)
        sim_info.resend_physical_attributes()
        trait_tracker = sim_info.trait_tracker
        for trait in tuple(trait_tracker.personality_traits):
            trait_tracker.remove_trait(trait)
        for trait in offspring_data.traits:
            trait_tracker.add_trait(trait)
        self.initialize_sim_info(sim_info, parent_a, parent_b)
        self._sim_info.relationship_tracker.add_relationship_bit(
            sim_info.id, self.BIRTHPARENT_BIT)
        return sim_info

    @staticmethod
    def initialize_sim_info(sim_info, parent_a, parent_b):
        sim_info.add_parent_relations(parent_a, parent_b)
        if sim_info.household is not parent_a.household:
            parent_a.household.add_sim_info_to_household(sim_info)
        services.sim_info_manager()._set_default_genealogy()
        sim_info.set_default_relationships(reciprocal=True)
        client = services.client_manager().get_client_by_household_id(
            sim_info.household_id)
        if client is not None:
            client.selectable_sims.add_selectable_sim_info(sim_info)

    def _select_traits_for_offspring(self, gender):
        traits = []
        num_of_traits = Trait.EQUIP_SLOT_NUMBER_MAP[Age.BABY]
        if num_of_traits == 0:
            return traits
        possible_traits = Trait.get_possible_traits(Age.BABY, gender)
        random.shuffle(possible_traits)
        first_trait = possible_traits.pop()
        traits.append(first_trait)
        while len(traits) < num_of_traits:
            current_trait = possible_traits.pop()
            if not any(
                    trait.is_conflicting(current_trait) for trait in traits):
                traits.append(current_trait)
            while not possible_traits:
                break
                continue
        return traits

    def create_offspring_data(self):
        r = random.Random()
        r.seed(self._seed)
        offspring_count = pop_weighted(
            [(p.weight *
              p.modifiers.get_multiplier(SingleSimResolver(self._sim_info)),
              p.size) for p in self.MULTIPLE_OFFSPRING_CHANCES],
            random=r)
        offspring_count = min(self._sim_info.household.free_slot_count + 1,
                              offspring_count)
        self._offspring_data = []
        for offspring_index in range(offspring_count):
            if offspring_index and r.random(
            ) < self.MONOZYGOTIC_OFFSPRING_CHANCE:
                gender = self._offspring_data[offspring_index - 1].gender
                genetics = self._offspring_data[offspring_index - 1].genetics
            else:
                gender = Gender.MALE if r.random() < 0.5 else Gender.FEMALE
                genetics = r.randint(1, MAX_UINT32)
            last_name = SimSpawner.get_family_name_for_gender(
                self._sim_info.account, self._sim_info.last_name,
                gender == Gender.FEMALE)
            traits = self._select_traits_for_offspring(gender)
            self._offspring_data.append(
                PregnancyOffspringData(gender,
                                       genetics,
                                       last_name=last_name,
                                       traits=traits))

    def get_offspring_data_gen(self):
        for offspring_data in self._offspring_data:
            yield offspring_data

    def _get_random_first_name(self, offspring_data):
        tries_left = 10

        def is_valid(first_name):
            nonlocal tries_left
            if not first_name:
                return False
            tries_left -= 1
            if tries_left and any(sim.first_name == first_name
                                  for sim in self._sim_info.household):
                return False
            if any(sim.first_name == first_name
                   for sim in self._offspring_data):
                return False
            return True

        first_name = None
        while not is_valid(first_name):
            first_name = SimSpawner.get_random_first_name(
                self.account, offspring_data.is_female)
        return first_name

    def _show_npc_dialog(self):
        for tuning_data in self.MULTIPLE_OFFSPRING_CHANCES:
            while tuning_data.size == self.offspring_count:
                npc_dialog = tuning_data.npc_dialog
                if npc_dialog is not None:
                    for parent in self.get_parents():
                        parent_instance = parent.get_sim_instance()
                        while parent_instance is not None and parent_instance.client is not None:
                            additional_tokens = list(
                                itertools.chain(self.get_parents(),
                                                self._offspring_data))
                            dialog = npc_dialog(
                                parent_instance,
                                DoubleSimResolver(additional_tokens[0],
                                                  additional_tokens[1]))
                            dialog.show_dialog(
                                additional_tokens=additional_tokens)
                return

    def save(self):
        data = protocols.PersistablePregnancyTracker()
        data.seed = self._seed
        if self._last_modified is not None:
            self.last_modified = self._last_modified.absolute_ticks()
        data.parent_ids.extend(self._parent_ids)
        return data

    def load(self, data):
        self._seed = int(data.seed)
        if data.HasField('last_modified'):
            self._last_modified = DateAndTime(data.last_modified)
        self._parent_ids.clear()
        self._parent_ids.extend(data.parent_ids)
class DefaultRelationship(HasTunableSingletonFactory, AutoFactoryInit):
    FACTORY_TUNABLES = {
        'relationship_tracks':
        TunableList(
            description=
            '\n            A list of relationship track and value pairs. E.g. a spouse has\n            Romantic relationship track value of 75.\n            ',
            tunable=TunableTuple(
                track=RelationshipTrack.TunableReference(
                    description=
                    '\n                    The relationship track that is added to the relationship\n                    between the two sims.\n                    ',
                    pack_safe=True),
                value=Tunable(
                    description=
                    '\n                    The relationship track is set to this value.\n                    ',
                    tunable_type=int,
                    default=0))),
        'relationship_bits':
        TunableSet(
            description=
            '\n            A set of untracked relationship bits that are applied to the\n            relationship between the two sims. These are bits that are provided\n            outside of the relationship_track being set. E.g. everyone in the\n            household should have the Has Met bit and the spouse should have the\n            First Kiss bit.\n            ',
            tunable=RelationshipBit.TunableReference(pack_safe=True)),
        'random_relationship_bits':
        TunableList(
            description=
            '\n            A list of random relationship bits that can be applied.\n            ',
            tunable=TunableList(
                description=
                '\n                A list of relationship bits and weights that that relationship\n                bit can be chosen to add.\n                ',
                tunable=TunableTuple(
                    description=
                    '\n                    A number of weighted relationship bits that have a chance\n                    of being applied.\n                    ',
                    bit=RelationshipBit.TunableReference(pack_safe=True),
                    weight=TunableMultiplier.TunableFactory(
                        description=
                        '\n                        A tunable list of tests and multipliers to apply to the \n                        weight of this relationship bit being applied.\n                        '
                    ))))
    }

    def apply(self,
              relationship,
              actor_sim_id,
              target_sim_id,
              bits_only=False):
        sim_to_target_tracker = relationship.relationship_track_tracker
        if not bits_only:
            try:
                sim_to_target_tracker.suppress_callback_setup_during_load = True
                sim_to_target_tracker.load_in_progress = True
                for data in self.relationship_tracks:
                    track = data.track
                    value = data.value
                    relationship_track = sim_to_target_tracker.get_statistic(
                        track, True)
                    if relationship_track is None:
                        continue
                    if relationship_track.get_value() < value:
                        sim_to_target_tracker.set_value(
                            track, value, apply_initial_modifier=True)
                    (old_bit,
                     new_bit) = relationship_track.update_instance_data()
                    if old_bit is not None and old_bit is not new_bit:
                        relationship.remove_bit(actor_sim_id, target_sim_id,
                                                old_bit)
                    if new_bit is not None and not relationship.has_bit(
                            actor_sim_id, new_bit):
                        relationship.add_relationship_bit(
                            actor_sim_id, target_sim_id, new_bit)
                    relationship_track.fixup_callbacks_during_load()
            finally:
                sim_to_target_tracker.suppress_callback_setup_during_load = False
                sim_to_target_tracker.load_in_progress = False
        for bit in self.relationship_bits:
            relationship.add_relationship_bit(actor_sim_id, target_sim_id, bit)
        sim_info_manager = services.sim_info_manager()
        actor_sim_info = sim_info_manager.get(actor_sim_id)
        target_sim_info = sim_info_manager.get(target_sim_id)
        resolver = DoubleSimResolver(actor_sim_info, target_sim_info)
        for random_relationships in self.random_relationship_bits:
            weights = []
            for random_bit in random_relationships:
                weight = random_bit.weight.get_multiplier(resolver)
                if weight > 0:
                    weights.append((weight, random_bit.bit))
            if weights:
                selected_bit = sims4.random.weighted_random_item(weights)
                relationship.add_relationship_bit(actor_sim_id, target_sim_id,
                                                  selected_bit)
Exemple #12
0
class Situation(BaseSituation, HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(sims4.resources.Types.SITUATION)):

    @staticmethod
    def _verify_situation_level_tuning(instance_class, tunable_name, source, bronze, gold, silver, tin):
        gold_reward = gold.reward
        if gold_reward is not None and gold_reward.reward_description is None:
            logger.error('Situation "{}" has a Gold tier reward that has no Reward Description tuned. Bronze and Silver are optional, but Gold requires a description.', source, owner='asantos')

    INSTANCE_SUBCLASSES_ONLY = True
    NPC_HOSTED_SITUATION_AGE_WEIGHTING = TunableMapping(description='\n        A map of ages to weights when determining which sim in the household\n        will be selected to receive an invitation.\n        ', key_name='age', key_type=TunableEnumEntry(description='\n            The age of a possible invitee that will be mapped to a weight.\n            ', tunable_type=Age, default=Age.ADULT), value_name='weight', value_type=TunableRange(description='\n            The weight a sim of this age will be chosen to have an event run\n            on them.\n            ', tunable_type=int, default=1, minimum=1))
    INSTANCE_TUNABLES = {'category': TunableEnumEntry(description='\n                The Category that the Situation belongs to.\n                ', tunable_type=SituationCategoryUid, default=SituationCategoryUid.DEFAULT, tuning_group=GroupNames.UI), 'load_open_street_situation_with_selectable_sim': Tunable(description='\n            If the situation has selectable sims, set to True to ensure the\n            situation can load from the open street, False otherwise.\n            \n            Note: The Serialization Option also determines save/load strategy.\n            Check with GPE to verify the situation save/load behavior.\n            ', tunable_type=bool, default=False), '_display_name': TunableLocalizedString(description='\n                Display name for situation\n                ', allow_none=True, tuning_group=GroupNames.UI), 'situation_description': TunableLocalizedString(description='\n                Situation Description\n                ', allow_none=True, tuning_group=GroupNames.UI), 'entitlement': OptionalTunable(description='\n            If enabled, this situation is locked by an entitlement. Otherwise,\n            this situation is available to all players.\n            ', tunable=TunableEntitlement(description='\n                Entitlement required to plan this event.\n                ', tuning_group=GroupNames.UI)), '_default_job': TunableReference(description='\n                The default job for Sims in this situation\n                ', manager=services.situation_job_manager(), allow_none=True), '_resident_job': SituationJob.TunableReference(description='\n                The job to assign to members of the host sims household.\n                Make sure to use the in_family filter term in the filter\n                of the job you reference here.\n                It is okay if this tunable is None.\n                ', allow_none=True), '_icon': TunableResourceKey(description='\n                Icon to be displayed in the situation UI.\n                ', resource_types=sims4.resources.CompoundTypes.IMAGE, default=None, allow_none=True, tuning_group=GroupNames.UI), 'calendar_icon': TunableIconAllPacks(description='\n            Icon to be displayed in the calendar UI.\n            ', allow_none=True, tuning_group=GroupNames.UI), 'calendar_alert_description': OptionalTunable(description='\n            If tuned, there will be a calendar alert description.\n            ', tunable=TunableLocalizedString(description='\n                Description that shows up in the calendar alert.\n                ')), '_level_data': TunableTuple(tin=TunableSituationLevel(description='\n                    Tuning for the Tin level of this situation.  This level has\n                    a score delta of 0 as it is considered the default level\n                    of any situation.\n                    ', locked_args={'medal': SituationMedal.TIN, 'score_delta': 0}), bronze=TunableSituationLevel(description='\n                    Tuning for the Bronze level of this situation.\n                    ', locked_args={'medal': SituationMedal.BRONZE}), silver=TunableSituationLevel(description='\n                    Tuning for the Silver level of this situation.\n                    ', locked_args={'medal': SituationMedal.SILVER}), gold=TunableSituationLevel(description='\n                    Tuning for the Gold level of this situation.\n                    ', locked_args={'medal': SituationMedal.GOLD}), description='\n                    Tuning for the different situation levels and rewards that\n                    are associated with them.\n                    ', verify_tunable_callback=_verify_situation_level_tuning), 'job_display_ordering': OptionalTunable(description='\n            An optional list of the jobs in the order you want them displayed\n            in the Plan an Event UI.\n            ', tunable=TunableList(tunable=TunableReference(manager=services.situation_job_manager())), tuning_group=GroupNames.UI), 'recommended_job_object_notification': ui.ui_dialog_notification.UiDialogNotification.TunableFactory(description='\n            The notification that is displayed when one or more recommended objects\n            for a job are missing.\n            ', locked_args={'text': None}), 'recommended_job_object_text': TunableLocalizedStringFactory(description='\n            The text of the notification that is displayed when one or more recommended\n            objects for a job are missing.\n            \n            The localization tokens for the Text field are:\n            {0.String} = bulleted list of strings for the missing objects\n            ', allow_none=True), '_buff': TunableBuffReference(description='\n                Buff that will get added to sim when commodity is at this\n                current state.\n                ', allow_none=True), '_cost': Tunable(description='\n                The cost of this situation\n                ', tunable_type=int, default=0), 'exclusivity': TunableEnumEntry(description='\n            Defines the exclusivity category for the situation which is used to prevent sims assigned\n            to this situation from being assigned to situations from categories excluded by this\n            category and vice versa.\n            ', tunable_type=situations.bouncer.bouncer_types.BouncerExclusivityCategory, default=situations.bouncer.bouncer_types.BouncerExclusivityCategory.NORMAL), 'main_goal': TunableReference(description='The main goal of the situation. e.g. Get Married.', manager=services.get_instance_manager(sims4.resources.Types.SITUATION_GOAL), allow_none=True, tuning_group=GroupNames.GOALS), 'main_goal_audio_sting': TunableResourceKey(description='\n                The sound to play when the main goal of this situation\n                completes.\n                ', default=None, resource_types=(sims4.resources.Types.PROPX,), tuning_group=GroupNames.AUDIO), '_main_goal_visibility_test': OptionalTunable(description='\n                If enabled then the main goal of the situation will not be\n                visible until this test passes.  If the state of this test no\n                longer becomes true then the main gaol will not become\n                invisible again.\n                \n                Ex. A hospital emergency starting triggers the visiblity of the\n                main goal within the active career event situation.\n                \n                IMPORTANT: The nature of this test can cause performance\n                problems.\n                ', tunable=TunableMainGoalVisibilityTestVariant(), tuning_group=GroupNames.GOALS), 'minor_goal_chains': TunableList(description='\n                A list of goal sets, each one starting a chain of goal sets, for selecting minor goals.\n                The list is in priority order, first being the most important.\n                At most one goal will be selected from each chain.\n                ', tunable=situations.situation_goal_set.SituationGoalSet.TunableReference(), tuning_group=GroupNames.GOALS), 'force_invite_only': Tunable(description='\n                If True, the situation is invite only. Otherwise, it is not.\n                For a date situation, this would be set to True.\n                ', tunable_type=bool, default=False), 'creation_ui_option': TunableEnumEntry(description='\n                Determines if the situation can be created from the Plan Event\n                UI triggered from the phone.\n                \n                NOT_AVAILABLE - situation is not available in the creation UI.\n                \n                AVAILABLE - situation is available in the creation UI.\n                \n                DEBUG_AVAILABLE - situation is only available in the UI if\n                you have used the |situations.allow_debug_situations command\n                \n                SPECIFIED_ONLY - situation is available in the creation UI if\n                that UI is tuned to only look at a subset of situations.\n                ', tunable_type=SituationCreationUIOption, default=SituationCreationUIOption.AVAILABLE, tuning_group=GroupNames.UI), 'audio_sting_on_start': TunableResourceKey(description='\n                The sound to play when the Situation starts.\n                ', default=None, resource_types=(sims4.resources.Types.PROPX,), tuning_group=GroupNames.AUDIO), 'background_audio': OptionalTunable(description='\n                If enabled then we will play audio in the background while this\n                user facing situation is running.\n                ', tunable=TunableResourceKey(description='\n                    Audio that will play throughout the situation in the background\n                    and will end at the end of the situation.\n                    ', default=None, resource_types=(sims4.resources.Types.PROPX,)), tuning_group=GroupNames.AUDIO), 'duration': TunableSimMinute(description='\n                How long the situation will last in sim minutes. 0 means forever.\n                ', default=60), 'duration_randomizer': TunableSimMinute(description="\n            A random time between 0 and this tuned time will be added to the\n            situation's duration.\n            ", default=0, minimum=0), 'max_participants': Tunable(description='\n                Maximum number of Sims the player is allowed to invite to this Situation.\n                ', tunable_type=int, default=16, tuning_group=GroupNames.UI), '_initiating_sim_tests': TunableSituationInitiationSet('\n            A set of tests that will be run on a sim attempting to initiate a\n            situation.  If these tests do not pass than this situation will not\n            be able to be chosen from the UI.\n            '), 'targeted_situation': OptionalTunable(description='\n                If enabled, the situation can be used as a targeted situation,\n                such as a Date.\n                ', tunable=TargetedSituationSpecific.TunableFactory()), 'compatible_venues': TunableList(description='\n                In the Plan an Event UI, lots that are these venues will be\n                added to the list of lots on which the player can throw the\n                event. The player can always choose their own lot and lots of\n                their guests.\n                ', tunable=TunableReference(manager=services.get_instance_manager(sims4.resources.Types.VENUE), pack_safe=True, tuning_group=GroupNames.VENUES)), 'venue_region_must_be_compatible': Tunable(description='\n                If enabled, venues will only be considered if they are in a\n                region that is compatible with the current region (regions with\n                at least one shared tag).\n                ', tunable_type=bool, default=False), 'venue_invitation_message': OptionalTunable(description='\n            If enabled, show a dialog when the situation tries to start on a\n            venue.\n            ', tunable=UiDialogOkCancel.TunableFactory(description="\n                The message that will be displayed when this situation tries to\n                start for the venue.\n                \n                Two additional tokens are passed in: the situation's name and\n                the job's name.\n                "), tuning_group=GroupNames.VENUES), 'venue_situation_player_job': TunableReference(description="\n                The job that the player will be put into when they join in a\n                user_facing special situation at a venue.\n                \n                Note: This must be tuned to allow this situation to be in a\n                venue's special event schedule. The job also must be a part of\n                the Situation.\n                ", manager=services.get_instance_manager(sims4.resources.Types.SITUATION_JOB), allow_none=True, tuning_group=GroupNames.VENUES), 'tags': TunableSet(description='\n                Tags for arbitrary groupings of situation types.\n                ', tunable=TunableEnumWithFilter(tunable_type=Tag, filter_prefixes=['situation'], default=Tag.INVALID, pack_safe=True)), '_relationship_between_job_members': TunableList(description="\n                Whenever a sim joins either job_x or job_y, the sim is granted \n                the tuned relationship bit with every sim in the other job. The\n                relationship bits are added and remain as long as the sims are\n                assigned to the tuned pair of jobs.\n                \n                This creates a relationship between the two sims if one does not exist.\n                \n                E.g. Date situation uses this feature to add bits to the sims'\n                relationship in order to drive autonomous behavior during the \n                lifetime of the date. \n                ", tunable=TunableTuple(job_x=SituationJob.TunableReference(), job_y=SituationJob.TunableReference(), relationship_bits_to_add=TunableSet(description='\n                        A set of RelationshipBits to add to relationship between the sims.\n                        ', tunable=RelationshipBit.TunableReference())), tuning_group=GroupNames.TRIGGERS), '_implies_greeted_status': Tunable(description='\n                If checked then a sim, in this situation, on a residential lot\n                they do not own, is consider greeted on that lot.\n                \n                Greeted status related design documents:\n                //depot/Sims4Projects/docs/Design/Gameplay/HouseholdState/Ungreeted_Lot_Behavior_DD.docx\n                //depot/Sims4Projects/docs/Design/Gameplay/Simulation/Active Lot Changing Edge Cases.docx\n                ', tunable_type=bool, default=False), 'screen_slam_no_medal': OptionalTunable(description='\n            Screen slam to show when this situation is completed and no\n            medal is earned.\n            Localization Tokens: Event Name - {0.String}, Medal Awarded - \n            {1.String}\n            ', tunable=ui.screen_slam.TunableScreenSlamSnippet()), 'screen_slam_bronze': OptionalTunable(description='\n            Screen slam to show when this situation is completed and a\n            bronze medal is earned.\n            Localization Tokens: Event Name - {0.String}, Medal Awarded - \n            {1.String}\n            ', tunable=ui.screen_slam.TunableScreenSlamSnippet()), 'screen_slam_silver': OptionalTunable(description='\n            Screen slam to show when this situation is completed and a\n            silver medal is earned.\n            Localization Tokens: Event Name - {0.String}, Medal Awarded - \n            {1.String}\n            ', tunable=ui.screen_slam.TunableScreenSlamSnippet()), 'screen_slam_gold': OptionalTunable(description='\n            Screen slam to show when this situation is completed and a\n            bronze medal is earned.\n            Localization Tokens: Event Name - {0.String}, Medal Awarded - \n            {1.String}\n            ', tunable=ui.screen_slam.TunableScreenSlamSnippet()), 'time_jump': TunableSituationTimeJumpVariant(description='\n            Determine how the situation handles the zone time being different on\n            load than what it was on save. This is primarily useful for\n            commercial venue background situations and career event situations.\n            ', tuning_group=GroupNames.SPECIAL_CASES), 'can_remove_sims_from_work': Tunable(description='\n            If checked then this situation will cause sims to end work early\n            when they are on the guest list. If unchecked, it will not. This\n            option will not affect active career situations or NPC career\n            situations like tending the bar.\n            ', tunable_type=bool, default=True, tuning_group=GroupNames.SPECIAL_CASES), '_survives_active_household_change': Tunable(description='\n            If checked then this situation will load even if the active\n            household has changed since it was saved. It will attempt to\n            restore Sims to their saved jobs. This is primarily useful for\n            commercial venue background situations.\n            ', tunable_type=bool, default=False, tuning_group=GroupNames.SPECIAL_CASES), '_maintain_sims_consistency': Tunable(description="\n            If checked, Sims in the saved situation that were pushed home \n            because they had been saved in the zone for many Sim hours will \n            be back. Otherwise, we will find replacement.\n            \n            Ex. We don't want to replace Butler with new Sim if previous\n            Butler is no longer in the lot.\n            ", tunable_type=bool, default=False, tuning_group=GroupNames.SPECIAL_CASES), '_hidden_scoring_override': Tunable(description='\n            If checked then even if this situation has its scoring disabled it\n            still will count score and provide rewards to the player.\n            ', tunable_type=bool, default=False, tuning_group=GroupNames.SPECIAL_CASES), '_ensemble': OptionalTunable(description='\n            If enabled then we will keep Sims in a specific ensemble for the\n            duration of the situation.\n            ', tunable=TunableTuple(description='\n                Tunables for putting Sims into ensembles.\n                ', ensemble_type=TunableReference(description='\n                    The type of Ensemble to put the sims into.\n                    ', manager=services.get_instance_manager(sims4.resources.Types.ENSEMBLE), pack_safe=True), remove_before_add=Tunable(description='\n                    If checked then before we add the Sim to the ensemble we\n                    will remove them from from ensembles of the specified type.\n                    This can be used to force Sims into only an ensemble of\n                    Sims in this situation.\n                    ', tunable_type=bool, default=False), ignore_situation_removal=Tunable(description='\n                    If checked then we will not remove the Sim from the\n                    ensemble of this type when the Sim is removed from the\n                    situation.\n                    ', tunable_type=bool, default=True), ensemble_option=TunableEnumEntry(description='\n                    How we want to add Sims to an ensemble:\n                    ONLY_WITHIN_SITUATION: Put the Sims in this situation into\n                    an ensemble of this type.  Every time a sim is added we\n                    try and do this so if the user destroys the ensemble and\n                    then another Sim is spawned for it the ensemble will be\n                    recreated.\n                    \n                    ADD_TO_ACTIVE_HOUSEHOLD: Every time a Sim is spawned for\n                    this situation they are put into an ensemble with the\n                    instanced active household.  This is useful if you want to\n                    put the Sims in a situation with someone who is not in it. \n                    \n                    ADD_TO_HOST: Every time a Sim is spawned for this situation\n                    they are put into an ensemble with the host of the\n                    situation.  This is useful if you want to put the Sims in\n                    a situation with someone who is not in it.\n                    ', tunable_type=EnsembleOption, default=EnsembleOption.ONLY_WITHIN_SITUATION)), tuning_group=GroupNames.SPECIAL_CASES), 'blocks_super_speed_three': Tunable(description='\n            If enabled, this situation will block any requests to go into super\n            speed 3.\n            ', tunable_type=bool, default=False), 'travel_request_behavior': TunableSituationTravelRequestBehaviorVariant(description='\n            Define how this situation handles incoming travel requests from\n            other situations when running as user-facing.\n            '), 'allowed_in_super_speed_3': Tunable(description="\n            If enabled, this situation will skip the super speed 3 rules and\n            be allowed to trigger at that speed.\n            This will only affect walkby's as they are the only restricted\n            by speed 3.\n            ", tunable_type=bool, default=False), 'should_send_on_lot_home_in_super_speed_3': Tunable(description='\n            If enabled, on_lot sims in this situation will not prevent SS3.  If\n            SS3 is triggered they will be sent home.\n            ', tunable_type=bool, default=False), 'super_speed3_replacement_speed': OptionalTunable(description='\n            If enabled and this situation blocks super speed 3, the situation will attempt to request\n            this speed if it is running when super speed 3 tries to kick in.\n            ', tunable=TunableEnumEntry(tunable_type=ClockSpeedMode, invalid_enums=(ClockSpeedMode.PAUSED, ClockSpeedMode.INTERACTION_STARTUP_SPEED, ClockSpeedMode.SUPER_SPEED3), default=ClockSpeedMode.SPEED3)), 'weight_multipliers': TunableMultiplier.TunableFactory(description="\n            Tunable tested multiplier to apply to any weight this situation\n            might have as part of a Situation Curve. These multipliers will be\n            applied globally anywhere this situation is tuned as part of a\n            situation curve (i.e. Walkby Tuning) so it should only be used in\n            cases where you ALWAYS want this multiplier applied.\n            \n            NOTE: You can still tune more multipliers on the individual walk by\n            instances. The multipliers will all stack together.\n            \n            *IMPORTANT* The only participants that work are ones\n            available globally, such as Lot and ActiveHousehold. Only\n            use these participant types or use tests that don't rely\n            on any, such as testing all objects via Object Criteria\n            test or testing active zone with the Zone test.\n            ", locked_args={'base_value': 1}), 'disallows_curfew_violation': Tunable(description='\n            If this is checked then the Sim is unable to violate curfew while\n            in the situation. If this is not checked then the Sim can vioalte\n            curfew as normal.\n            ', tunable_type=bool, default=False), 'suppress_scoring_progress_bar': Tunable(description='\n            If this is checked, UI will no longer show the scoring progress bar\n            and instead show the situation name in its stead.\n            ', tunable_type=bool, default=False, tuning_group=GroupNames.UI), 'highlight_first_incomplete_minor_goal': Tunable(description='\n            If this is checked, we will tell user-facing Situation UI \n            to highlight the first uncompleted minor goal set.\n            \n            Note that gameplay currently does not guard against being able \n            to complete the other goal sets in the situation currently, \n            so the goalsets should be tuned in such a manner \n            that they do not overlap.\n            ', tunable_type=bool, default=False, tuning_group=GroupNames.UI), '_use_spawner_tags_on_travel': Tunable(description='\n            If checked the situation will spawn sims according to its job spawner tags\n            instead of always defaulting to the Arrival spawner.\n            ', tunable_type=bool, default=False)}
    SITUATION_SCORING_REMOVE_INSTANCE_TUNABLES = ('main_goal', '_main_goal_visibility_test', 'minor_goal_chains', 'main_goal_audio_sting', 'highlight_first_incomplete_minor_goal', 'suppress_scoring_progress_bar', '_level_data', 'screen_slam_gold', 'screen_slam_silver', 'screen_slam_bronze', 'screen_slam_no_medal')
    SITUATION_START_FROM_UI_REMOVE_INSTANCE_TUNABLES = ('_cost', 'compatible_venues', 'venue_invitation_message', 'venue_situation_player_job', 'category', 'max_participants', '_initiating_sim_tests', '_icon', 'entitlement', 'job_display_ordering')
    SITUATION_USER_FACING_REMOVE_INSTANCE_TUNABLES = ('_display_name', 'travel_request_behavior', 'recommended_job_object_notification', 'recommended_job_object_text', 'situation_description')
    NON_USER_FACING_REMOVE_INSTANCE_TUNABLES = ('_buff', 'targeted_situation', '_resident_job', '_relationship_between_job_members', 'audio_sting_on_start', 'background_audio', 'force_invite_only') + SITUATION_SCORING_REMOVE_INSTANCE_TUNABLES + SITUATION_START_FROM_UI_REMOVE_INSTANCE_TUNABLES + SITUATION_USER_FACING_REMOVE_INSTANCE_TUNABLES
    SITUATION_EVENT_REMOVE_INSTANCE_TUNABLES = ('_buff', '_cost', 'venue_invitation_message', 'venue_situation_player_job', 'category', 'main_goal', '_main_goal_visibility_test', 'minor_goal_chains', 'highlight_first_incomplete_minor_goal', 'suppress_scoring_progress_bar', 'max_participants', '_initiating_sim_tests', '_icon', 'targeted_situation', '_resident_job', 'situation_description', 'job_display_ordering', 'entitlement', '_relationship_between_job_members', 'main_goal_audio_sting', 'audio_sting_on_start', 'background_audio', '_level_data', '_display_name', 'screen_slam_gold', 'screen_slam_silver', 'screen_slam_bronze', 'screen_slam_no_medal', 'force_invite_only', 'recommended_job_object_notification', 'recommended_job_object_text', 'travel_request_behavior')
    situation_level_data = None
    SituationLevel = collections.namedtuple('SituationLevel', ['min_score_threshold', 'level_data'])

    @classmethod
    def _tuning_loaded_callback(cls):
        cls.situation_level_data = []
        current_score = cls._level_data.tin.score_delta
        cls.situation_level_data.append(Situation.SituationLevel(current_score, cls._level_data.tin))
        current_score += cls._level_data.bronze.score_delta
        cls.situation_level_data.append(Situation.SituationLevel(current_score, cls._level_data.bronze))
        current_score += cls._level_data.silver.score_delta
        cls.situation_level_data.append(Situation.SituationLevel(current_score, cls._level_data.silver))
        current_score += cls._level_data.gold.score_delta
        cls.situation_level_data.append(Situation.SituationLevel(current_score, cls._level_data.gold))

    @classmethod
    def _verify_tuning_callback(cls):
        if cls._resident_job is not None and cls._resident_job.filter is None:
            logger.error('Resident Job: {} has no filter,', cls._resident_job, owner='manus')
        if cls.targeted_situation is not None and (cls.targeted_situation.target_job is None or cls.targeted_situation.actor_job is None):
            logger.error('target_job and actor_job are required if targeted_situation is enabled.', owner='manus')
        tuned_jobs = frozenset(cls.get_tuned_jobs())
        for job_relationships in cls.relationship_between_job_members:
            if job_relationships.job_x not in tuned_jobs:
                logger.error('job_x: {} has relationship tuning but is not functionally used in situation {}.', job_relationships.job_x, cls, owner='manus')
            if job_relationships.job_y not in tuned_jobs:
                logger.error('job_y: {} has relationship tuning but is not functionally used in situation {}.', job_relationships.job_y, cls, owner='manus')
            if len(job_relationships.relationship_bits_to_add) == 0:
                logger.error("relationship_bits_to_add cannot be empty for situation {}'s job pairs {} and {}.", cls, job_relationships.job_x, job_relationships.job_y, owner='manus')
            else:
                for bit in job_relationships.relationship_bits_to_add:
                    if bit is None:
                        logger.error("relationship_bits_to_add cannot contain empty bit for situation {}'s job pairs {} and {}.", cls, job_relationships.job_x, job_relationships.job_y, owner='manus')

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._duration_alarm_handle = None
        self._goal_tracker = None
        self._dynamic_goals = self._seed.extra_kwargs.get('dynamic_goals', None)

    @classproperty
    def allow_user_facing_goals(cls):
        return cls.main_goal is not None or len(cls.minor_goal_chains) > 0

    @classmethod
    def level_data_gen(cls):
        for level in cls.situation_level_data:
            yield level

    @classmethod
    def fake_perform_job(cls):
        pass

    @classmethod
    def get_level_data(cls, medal:SituationMedal=SituationMedal.TIN):
        if cls.situation_level_data is None:
            return
        return cls.situation_level_data[medal].level_data

    @classmethod
    def get_level_min_threshold(cls, medal:SituationMedal=SituationMedal.TIN):
        if cls.situation_level_data is None:
            return
        return cls.situation_level_data[medal].min_score_threshold

    @classmethod
    def get_level_icon(cls, medal:SituationMedal=SituationMedal.TIN):
        if cls.situation_level_data is None:
            return
        return cls.situation_level_data[medal].level_data.icon

    @classmethod
    def get_possible_zone_ids_for_situation(cls, host_sim_info=None, guest_ids=None):
        possible_zones = []
        venue_manager = services.get_instance_manager(sims4.resources.Types.VENUE)
        venue_service = services.current_zone().venue_service
        for venue_tuning in cls.compatible_venues:
            if venue_tuning.is_residential:
                if host_sim_info is not None:
                    home_zone_id = host_sim_info.household.home_zone_id
                    home_venue_tuning = venue_manager.get(build_buy.get_current_venue(home_zone_id))
                    if home_venue_tuning.is_residential:
                        possible_zones.append(home_zone_id)
                if guest_ids is not None:
                    for guest_id in guest_ids:
                        guest_id = int(guest_id)
                        guest_info = services.sim_info_manager().get(guest_id)
                        if guest_info is not None:
                            guest_zone_id = guest_info.household.home_zone_id
                            if guest_zone_id is not None and guest_zone_id and guest_zone_id not in possible_zones:
                                guest_venue_tuning = venue_manager.get(build_buy.get_current_venue(guest_zone_id))
                                if guest_venue_tuning.is_residential:
                                    possible_zones.append(guest_zone_id)
                            travel_group = guest_info.travel_group
                            if travel_group is not None:
                                travel_group_zone_id = travel_group.zone_id
                                if travel_group_zone_id is not None and travel_group_zone_id and travel_group_zone_id not in possible_zones:
                                    possible_zones.append(travel_group_zone_id)
                    else:
                        possible_zones.extend(venue_service.get_zones_for_venue_type_gen(venue_tuning))
            else:
                possible_zones.extend(venue_service.get_zones_for_venue_type_gen(venue_tuning))
        return possible_zones

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

    @classmethod
    def resident_job(cls):
        return cls._resident_job

    @classmethod
    def get_prepopulated_job_for_sims(cls, sim, target_sim_id=None):
        if target_sim_id and cls.targeted_situation is not None:
            sim_info = services.sim_info_manager().get(target_sim_id)
            if sim_info is None:
                return
            else:
                prepopulated = [(sim.id, cls.targeted_situation.actor_job.guid64), (target_sim_id, cls.targeted_situation.target_job.guid64)]
                return prepopulated

    def _display_role_objects_notification(self, sim, bullets):
        text = self.recommended_job_object_text(bullets)
        notification = self.recommended_job_object_notification(sim, text=lambda *_, **__: text)
        notification.show_dialog()

    @property
    def pie_menu_icon(self):
        return self._pie_menu_icon

    @classproperty
    def display_name(self):
        return self._display_name

    @property
    def description(self):
        return self.situation_description

    @classproperty
    def icon(self):
        return self._icon

    @property
    def start_audio_sting(self):
        return self.audio_sting_on_start

    @property
    def audio_background(self):
        return self.background_audio

    def get_target_object(self):
        pass

    def get_created_object(self):
        pass

    @property
    def end_audio_sting(self):
        current_level = self.get_level()
        level_data = self.get_level_data(current_level)
        if level_data is not None and level_data.audio_sting_on_end is not None:
            return level_data.audio_sting_on_end
        else:
            return

    @classproperty
    def relationship_between_job_members(cls):
        return cls._relationship_between_job_members

    @classproperty
    def implies_greeted_status(cls):
        return cls._implies_greeted_status

    @classmethod
    def cost(cls):
        return cls._cost

    @classproperty
    def survives_active_household_change(cls):
        return cls._survives_active_household_change

    @classproperty
    def maintain_sims_consistency(cls):
        return cls._maintain_sims_consistency

    def _get_duration(self):
        if self._seed.duration_override is not None:
            return self._seed.duration_override
        return self.duration + random.randint(0, self.duration_randomizer)

    def _get_remaining_time(self):
        if self._duration_alarm_handle is None:
            return
        return self._duration_alarm_handle.get_remaining_time()

    def _get_remaining_time_for_gsi(self):
        return self._get_remaining_time()

    def _get_remaining_time_in_minutes(self):
        time_span = self._get_remaining_time()
        if time_span is None:
            return 0
        return time_span.in_minutes()

    def _get_goal_tracker(self):
        return self._goal_tracker

    def _save_custom(self, seed):
        super()._save_custom(seed)
        if self._goal_tracker is not None:
            self._goal_tracker.save_to_seed(seed)

    def start_situation(self):
        super().start_situation()
        self._set_duration_alarm()
        if self.is_user_facing:
            if self.should_track_score:
                if self._dynamic_goals is None:
                    self._goal_tracker = situations.situation_goal_tracker.SituationGoalTracker(self)
                else:
                    self._goal_tracker = situations.dynamic_situation_goal_tracker.DynamicSituationGoalTracker(self)

    def _load_situation_states_and_phases(self):
        super()._load_situation_states_and_phases()
        self._set_duration_alarm()
        if not self._seed.goal_tracker_seedling:
            return
        if self._seed.goal_tracker_seedling.goal_tracker_type == GoalTrackerType.STANDARD_GOAL_TRACKER:
            self._goal_tracker = situations.situation_goal_tracker.SituationGoalTracker(self)
        elif self._seed.goal_tracker_seedling.goal_tracker_type == GoalTrackerType.DYNAMIC_GOAL_TRACKER:
            self._goal_tracker = situations.dynamic_situation_goal_tracker.DynamicSituationGoalTracker(self)

    def change_duration(self, duration):
        if not self.is_running:
            logger.error("Trying to change the duration of a situation {} that's not running.", self)
        self._set_duration_alarm(duration_override=duration)
        if self.is_user_facing:
            self.add_situation_duration_change_op()

    def _set_duration_alarm(self, duration_override=None):
        if duration_override is not None:
            duration = duration_override
        else:
            duration = self._get_duration()
        self.set_end_time(duration)
        if duration > 0:
            if self._duration_alarm_handle is not None:
                alarms.cancel_alarm(self._duration_alarm_handle)
            self._duration_alarm_handle = alarms.add_alarm(self, clock.interval_in_sim_minutes(duration), self._situation_timed_out)

    def _cancel_duration_alarm(self):
        if self.is_user_facing:
            logger.error('Canceling duration alarm for a User-Facing Situation {}', self, owner='rmccord')
        if self._duration_alarm_handle is not None:
            alarms.cancel_alarm(self._duration_alarm_handle)

    def pre_destroy(self):
        pass

    def _destroy(self):
        if self._duration_alarm_handle is not None:
            alarms.cancel_alarm(self._duration_alarm_handle)
        if self._goal_tracker is not None:
            self._goal_tracker.destroy()
            self._goal_tracker = None
        super()._destroy()

    def _situation_timed_out(self, _):
        logger.debug('Situation time expired: {}', self)
        self._self_destruct()

    @classmethod
    def is_situation_available(cls, initiating_sim, target_sim_id=0):
        is_targeted = cls.targeted_situation is not None and cls.targeted_situation.target_job is not None
        if is_targeted and target_sim_id:
            if not cls.targeted_situation.target_job.can_sim_be_given_job(target_sim_id, initiating_sim.sim_info):
                return TestResult(False)
        elif (target_sim_id == 0) != (is_targeted == False):
            return TestResult(False)
        single_sim_resolver = event_testing.resolver.SingleSimResolver(initiating_sim.sim_info)
        return cls._initiating_sim_tests.run_tests(single_sim_resolver)

    @classmethod
    def get_predefined_guest_list(cls):
        pass

    @classmethod
    def is_venue_location_valid(cls, zone_id):
        compatible_region = services.current_region() if cls.venue_region_must_be_compatible else None
        return services.current_zone().venue_service.is_zone_valid_for_venue_type(zone_id, cls.compatible_venues, compatible_region=compatible_region)

    @classmethod
    def get_venue_location(cls):
        compatible_region = services.current_region() if cls.venue_region_must_be_compatible else None
        (zone_id, _) = services.current_zone().venue_service.get_zone_and_venue_type_for_venue_types(cls.compatible_venues, compatible_region=compatible_region)
        return zone_id

    @classmethod
    def has_venue_location(cls):
        compatible_region = services.current_region() if cls.venue_region_must_be_compatible else None
        return services.current_zone().venue_service.has_zone_for_venue_type(cls.compatible_venues, compatible_region=compatible_region)

    @classproperty
    def main_goal_visibility_test(cls):
        return cls._main_goal_visibility_test

    @classproperty
    def _ensemble_data(cls):
        return cls._ensemble

    @property
    def should_track_score(self):
        return self.scoring_enabled or self._hidden_scoring_override

    @property
    def should_give_rewards(self):
        return self.scoring_enabled or self._hidden_scoring_override

    def is_in_joinable_state(self):
        return True

    @property
    def custom_event_keys(self):
        return [type(self)] + list(self.tags)

    @classproperty
    def use_spawner_tags_on_travel(cls):
        return cls._use_spawner_tags_on_travel
class AgingTransition(HasTunableSingletonFactory, AutoFactoryInit):

    class _AgeTransitionShowDialog(HasTunableSingletonFactory, AutoFactoryInit):
        FACTORY_TUNABLES = {'dialog': SimPersonalityAssignmentDialog.TunableFactory(locked_args={'phone_ring_type': PhoneRingType.NO_RING})}

        def __call__(self, sim_info, **kwargs):

            def on_response(dlg):
                if dlg.accepted:
                    sim_info.resend_trait_ids()

            dialog = self.dialog(sim_info, assignment_sim_info=sim_info, resolver=SingleSimResolver(sim_info))
            dialog.show_dialog(on_response=on_response, **kwargs)

    class _AgeTransitionShowNotification(HasTunableSingletonFactory, AutoFactoryInit):
        FACTORY_TUNABLES = {'dialog': UiDialogNotification.TunableFactory()}

        def __call__(self, sim_info, **__):
            dialog = self.dialog(sim_info, resolver=SingleSimResolver(sim_info))
            dialog.show_dialog()

    FACTORY_TUNABLES = {'age_up_warning_notification': OptionalTunable(tunable=UiDialogNotification.TunableFactory(description='\n                Notification to show up when Age Up is impending.\n                ', tuning_group=GroupNames.UI)), 'age_up_available_notification': OptionalTunable(tunable=UiDialogNotification.TunableFactory(description='\n                Notification to show when Age Up is ready.\n                ', tuning_group=GroupNames.UI)), '_age_duration': TunableLiteralOrRandomValue(description="\n            The time, in Sim days, required for a Sim to be eligible to\n            transition from this age to the next one.\n            \n            If a random range is specified, the random value will be seeded on\n            the Sim's ID.\n            ", tunable_type=float, default=1), '_use_initial_age_randomization': Tunable(description="\n            If checked, instead of randomizing the duration of each individual age,\n            the sims's initial age progress will be randomly offset on first load. \n            ", tunable_type=bool, default=False), 'age_transition_warning': Tunable(description='\n            Number of Sim days prior to the transition a Sim will get a warning\n            of impending new age.\n            ', tunable_type=float, default=1), 'age_transition_delay': Tunable(description='\n            Number of Sim days after transition time elapsed before auto- aging\n            occurs.\n            ', tunable_type=float, default=1), 'age_transition_dialog': TunableVariant(description='\n            The dialog or notification that is displayed when the Sim ages up.\n            ', show_dialog=_AgeTransitionShowDialog.TunableFactory(), show_notification=_AgeTransitionShowNotification.TunableFactory(), locked_args={'no_dialog': None}, default='no_dialog', tuning_group=GroupNames.UI), 'age_trait': TunableReference(description="\n            The age trait that corresponds to this Sim's age\n            ", manager=services.get_instance_manager(sims4.resources.Types.TRAIT)), 'relbit_based_loot': TunableList(description='\n            List of loots given based on bits set in existing relationships.\n            Applied after per household member loot.\n            ', tunable=TunableTuple(description='\n                Loot given to sim aging up (actor) and each sim (target) with a\n                "chained" relationship via recursing through the list of relbit\n                sets.\n                ', relationship=TunableList(description='\n                    List specifying a series of relationship(s) to recursively \n                    traverse to find the desired target sim.  i.e. to find \n                    "cousins", we get all the "parents".  And then we \n                    get "aunts/uncles" by getting the "siblings" of those \n                    "parents".  And then we finally get the "cousins" by \n                    getting the "children" of those "aunts/uncles".\n                    \n                    So:\n                     Set of "parent" bitflag(s)\n                     Set of "sibling" bitflag(s)\n                     Set of "children" bitflag(s)\n                    \n                    Can also find direct existing relationships by only having a\n                    single entry in the list.\n                    ', tunable=TunableSet(description='\n                        Set of relbits to use for this relationship.\n                        ', tunable=RelationshipBit.TunableReference(description='\n                            The relationship bit between greeted Sims.\n                            ', pack_safe=True))), loot=TunableList(description='\n                    Loot given between sim aging up and sims with the previously\n                    specified chain of relbits. (may create a relationship).\n                    ', tunable=LootActions.TunableReference(description='\n                        A loot action given to sim aging up.\n                        ', pack_safe=True))), tuning_group=GroupNames.TRIGGERS), 'per_household_member_loot': TunableList(description="\n            Loots given between sim aging up (actor) and each sim in that sims\n            household (target).  Applied before relbit based loot'\n            ", tunable=LootActions.TunableReference(description='\n                A loot action given between sim aging up (actor) and each sim in\n                that sims household (target).\n                ', pack_safe=True), tuning_group=GroupNames.TRIGGERS), 'single_sim_loot': TunableList(description='\n            Loots given to sim aging up (actor). Last loot applied.\n            ', tunable=LootActions.TunableReference(description='\n                A loot action given to sim aging up.\n                ', pack_safe=True), tuning_group=GroupNames.TRIGGERS)}

    def get_age_duration(self, sim_info):
        if self._use_initial_age_randomization:
            return (self._age_duration.upper_bound + self._age_duration.lower_bound)/2
        return self._get_random_age_duration(sim_info)

    def _get_random_age_duration(self, sim_info):
        return self._age_duration.random_float(seed=(self.age_trait.guid64, sim_info.sim_id))

    def get_randomized_progress(self, sim_info, age_progress):
        if self._use_initial_age_randomization:
            previous_age_duration = self._get_random_age_duration(sim_info)
            current_age_duration = self.get_age_duration(sim_info)
            if sims4.math.almost_equal(previous_age_duration, current_age_duration):
                return age_progress
            age_progress = current_age_duration + age_progress - previous_age_duration
            if age_progress < 0:
                age_progress = self._age_duration.upper_bound - self._age_duration.lower_bound + age_progress
        return age_progress

    def _apply_aging_transition_relbit_loot(self, source_info, cur_info, relbit_based_loot, level):
        if level == len(relbit_based_loot.relationship):
            resolver = DoubleSimResolver(source_info, cur_info)
            for loot in relbit_based_loot.loot:
                loot.apply_to_resolver(resolver)
            return
        relationship_tracker = cur_info.relationship_tracker
        for target_sim_id in relationship_tracker.target_sim_gen():
            if set(relationship_tracker.get_all_bits(target_sim_id)) & relbit_based_loot.relationship[level]:
                new_sim_info = services.sim_info_manager().get(target_sim_id)
                self._apply_aging_transition_relbit_loot(source_info, new_sim_info, relbit_based_loot, level + 1)

    def apply_aging_transition_loot(self, sim_info):
        if self.per_household_member_loot:
            for member_info in sim_info.household.sim_info_gen():
                if member_info is sim_info:
                    continue
                resolver = DoubleSimResolver(sim_info, member_info)
                for household_loot in self.per_household_member_loot:
                    household_loot.apply_to_resolver(resolver)
        for relbit_based_loot in self.relbit_based_loot:
            self._apply_aging_transition_relbit_loot(sim_info, sim_info, relbit_based_loot, 0)
        resolver = SingleSimResolver(sim_info)
        for loot in self.single_sim_loot:
            loot.apply_to_resolver(resolver)

    def show_age_transition_dialog(self, sim_info, **kwargs):
        if self.age_transition_dialog is not None:
            self.age_transition_dialog(sim_info, **kwargs)
class SocialMixerInteraction(SocialInteractionMixin, MixerInteraction):
    __qualname__ = 'SocialMixerInteraction'
    REMOVE_INSTANCE_TUNABLES = ('basic_reserve_object', 'basic_focus')
    basic_reserve_object = None
    GENDER_PREF_CONTENT_SCORE_PENALTY = Tunable(
        description=
        '\n        Penalty applied to content score when the social fails the gender preference test.\n        ',
        tunable_type=int,
        default=-1500)
    INSTANCE_TUNABLES = {
        'base_score':
        Tunable(
            description=
            ' \n            Base score when determining the content set value of this mixer\n            based on other mixers of the super affordance. This is the base\n            value used before any modification to content score.\n    \n            Modification to the content score for this affordance can come from\n            topics and moods\n    \n            USAGE: If you would like this mixer to more likely show up no matter\n            the topic and mood ons the sims tune this value higher.\n                                    \n            Formula being used to determine the autonomy score is Score =\n            Avg(Uc, Ucs) * W * SW, Where Uc is the commodity score, Ucs is the\n            content set score, W is the weight tuned the on mixer, and SW is the\n            weight tuned on the super interaction.\n            ',
            tuning_group=GroupNames.AUTONOMY,
            tunable_type=int,
            default=0),
        'social_context_preference':
        TunableMapping(
            description=
            '\n            A mapping of social contexts that will adjust the content score for\n            this mixer interaction. This is used conjunction with base_score.\n            ',
            tuning_group=GroupNames.AUTONOMY,
            key_type=SocialContextBit.TunableReference(),
            value_type=Tunable(tunable_type=float, default=0)),
        'relationship_bit_preference':
        TunableMapping(
            description=
            '\n            A mapping of relationship bits that will adjust the content score for\n            this mixer interaction. This is used conjunction with base_score.\n            ',
            tuning_group=GroupNames.AUTONOMY,
            key_type=RelationshipBit.TunableReference(),
            value_type=Tunable(tunable_type=float, default=0)),
        'trait_preference':
        TunableMapping(
            description=
            '\n            A mapping of traits that will adjust the content score for\n            this mixer interaction. This is used conjunction with base_score.\n            ',
            tuning_group=GroupNames.AUTONOMY,
            key_type=Trait.TunableReference(),
            value_type=Tunable(tunable_type=float, default=0)),
        'buff_preference':
        TunableMapping(
            description=
            '\n            A mapping of buffs that will adjust the content score for\n            this mixer interaction. This is used conjunction with base_score.\n            ',
            tuning_group=GroupNames.AUTONOMY,
            key_type=Buff.TunableReference(),
            value_type=Tunable(tunable_type=float, default=0)),
        'test_gender_preference':
        Tunable(
            description=
            '\n            If this is set, a gender preference test will be run between\n            the actor and target sims. If it fails, the social score will be\n            modified by a large negative penalty tuned with the tunable:\n            GENDER_PREF_CONTENT_SCORE_PENALTY\n            ',
            tuning_group=GroupNames.AUTONOMY,
            tunable_type=bool,
            default=False),
        'outcome':
        TunableOutcome(allow_multi_si_cancel=True)
    }

    def __init__(self, target, context, *args, **kwargs):
        super().__init__(target, context, *args, **kwargs)

    @classmethod
    def _add_auto_constraint(cls, participant_type, auto_constraint):
        raise RuntimeError(
            '[bhill] This function is believed to be dead code and is scheduled for pruning. If this exception has been raised, the code is not dead and this exception should be removed.'
        )

    @classproperty
    def is_social(cls):
        return True

    @property
    def social_group(self):
        if self.super_interaction is not None:
            return self.super_interaction.social_group

    @staticmethod
    def _tunable_tests_enabled():
        return tunable_tests_enabled

    @classmethod
    def get_base_content_set_score(cls):
        return cls.base_score

    @classmethod
    def _test(cls, target, context, *args, **kwargs):
        if context.sim is target:
            return TestResult(False,
                              'Social Mixer Interactions cannot target self!')
        pick_target = context.pick.target if context.source == context.SOURCE_PIE_MENU else None
        if target is None and context.sim is pick_target:
            return TestResult(False,
                              'Social Mixer Interactions cannot target self!')
        return MixerInteraction._test(target, context, *args, **kwargs)

    @classmethod
    def get_score_modifier(cls, sim, target):
        if cls.test_gender_preference:
            gender_pref_test = GenderPreferenceTest(ParticipantType.Actor,
                                                    ParticipantType.TargetSim,
                                                    ignore_reciprocal=True)
            resolver = DoubleSimResolver(sim.sim_info, target.sim_info)
            result = resolver(gender_pref_test)
            if not result:
                return cls.GENDER_PREF_CONTENT_SCORE_PENALTY
        social_context_preference = 0
        relationship_bit_preference = 0
        trait_preference = 0
        buff_preference = 0
        if target is not None:
            sims = set(
                itertools.chain.from_iterable(
                    group for group in sim.get_groups_for_sim_gen()
                    if target in group))
            if sims:
                social_context = SocialContextTest.get_overall_short_term_context_bit(
                    *sims)
            else:
                relationship_track = sim.relationship_tracker.get_relationship_prevailing_short_term_context_track(
                    target.id)
                if relationship_track is not None:
                    social_context = relationship_track.get_active_bit()
                else:
                    social_context = None
            social_context_preference = cls.social_context_preference.get(
                social_context, 0)
            if cls.relationship_bit_preference:
                relationship_bit_preference = sum(
                    cls.relationship_bit_preference.get(rel_bit, 0)
                    for rel_bit in sim.relationship_tracker.get_all_bits(
                        target_sim_id=target.id))
            if cls.trait_preference:
                trait_preference = sum(
                    cls.trait_preference.get(trait, 0)
                    for trait in sim.trait_tracker.equipped_traits)
            if cls.buff_preference:
                buff_preference = sum(
                    score for (buff, score) in cls.buff_preference.items()
                    if sim.has_buff(buff))
        score_modifier = super().get_score_modifier(
            sim, target
        ) + social_context_preference + relationship_bit_preference + trait_preference + buff_preference
        return score_modifier

    def should_insert_in_queue_on_append(self):
        if super().should_insert_in_queue_on_append():
            return True
        if self.super_affordance is None:
            logger.error(
                '{} being added to queue without a super interaction or super affordance',
                self)
            return False
        ui_group_tag = self.super_affordance.visual_type_override_data.group_tag
        if ui_group_tag == tag.Tag.INVALID:
            return False
        for si in self.sim.si_state:
            while si.visual_type_override_data.group_tag == ui_group_tag:
                return True
        return False

    def get_asm(self, *args, **kwargs):
        return Interaction.get_asm(self, *args, **kwargs)

    def perform_gen(self, timeline):
        if self.social_group is None:
            raise AssertionError(
                'Social mixer interaction {} has no social group. [bhill]'.
                format(self))
        result = yield super().perform_gen(timeline)
        return result

    def build_basic_elements(self, sequence=()):
        sequence = super().build_basic_elements(sequence=sequence)
        if self.super_interaction.social_group is not None:
            listen_animation_factory = self.super_interaction.listen_animation
        else:
            listen_animation_factory = None
            for group in self.sim.get_groups_for_sim_gen():
                si = group.get_si_registered_for_sim(self.sim)
                while si is not None:
                    listen_animation_factory = si.listen_animation
                    break
        if listen_animation_factory is not None:
            for sim in self.required_sims():
                if sim is self.sim:
                    pass
                sequence = listen_animation_factory(sim.animation_interaction,
                                                    sequence=sequence)
                sequence = with_skippable_animation_time((sim, ),
                                                         sequence=sequence)

        def defer_cancel_around_sequence_gen(s, timeline):
            deferred_sis = []
            for sim in self.required_sims():
                while not (sim is self.sim or self.social_group is None):
                    if sim not in self.social_group:
                        pass
                    sis = self.social_group.get_sis_registered_for_sim(sim)
                    while sis:
                        deferred_sis.extend(sis)
            with self.super_interaction.cancel_deferred(deferred_sis):
                result = yield element_utils.run_child(timeline, s)
                return result

        sequence = functools.partial(defer_cancel_around_sequence_gen,
                                     sequence)
        if self.target_type & TargetType.ACTOR:
            return element_utils.build_element(sequence)
        if self.target_type & TargetType.TARGET and self.target is not None:
            sequence = self.social_group.with_target_focus(
                self.sim, self.sim, self.target, sequence)
        elif self.social_group is not None:
            sequence = self.social_group.with_social_focus(
                self.sim, self.sim, self.required_sims(), sequence)
        else:
            for social_group in self.sim.get_groups_for_sim_gen():
                sequence = social_group.with_social_focus(
                    self.sim, self.sim, self.required_sims(), sequence)
        communicable_buffs = collections.defaultdict(list)
        for sim in self.required_sims():
            for buff in sim.Buffs:
                while buff.communicable:
                    communicable_buffs_sim = communicable_buffs[sim]
                    communicable_buffs_sim.append(buff)
        for (sim, communicable_buffs_sim) in communicable_buffs.items():
            for other_sim in self.required_sims():
                if other_sim is sim:
                    pass
                resolver = DoubleSimResolver(sim.sim_info, other_sim.sim_info)
                for buff in communicable_buffs_sim:
                    buff.communicable.apply_to_resolver(resolver)
        return element_utils.build_element(sequence)

    def cancel_parent_si_for_participant(self, participant_type,
                                         finishing_type, cancel_reason_msg,
                                         **kwargs):
        social_group = self.social_group
        if social_group is None:
            return
        participants = self.get_participants(participant_type)
        for sim in participants:
            while sim is not None:
                social_group.remove(sim)
        group_tag = self.super_interaction.visual_type_override_data.group_tag
        if group_tag != Tag.INVALID:
            for si in self.sim.si_state:
                while si is not self.super_interaction and si.visual_type_override_data.group_tag == group_tag:
                    social_group = si.social_group
                    if social_group is not None:
                        while True:
                            for sim in participants:
                                while sim in social_group:
                                    social_group.remove(sim)

    @flexmethod
    def get_participants(cls,
                         inst,
                         participant_type,
                         sim=DEFAULT,
                         **kwargs) -> set:
        inst_or_cls = inst if inst is not None else cls
        result = super(MixerInteraction,
                       inst_or_cls).get_participants(participant_type,
                                                     sim=sim,
                                                     **kwargs)
        result = set(result)
        sim = inst.sim if sim is DEFAULT else sim
        if inst is not None and inst.social_group is None and (
                participant_type & ParticipantType.AllSims
                or participant_type & ParticipantType.Listeners):
            if inst is not None and inst.target_type & TargetType.GROUP:
                while True:
                    for other_sim in itertools.chain(
                            *list(sim.get_groups_for_sim_gen())):
                        if other_sim is sim:
                            pass
                        if other_sim.ignore_group_socials(
                                excluded_group=inst.social_group):
                            pass
                        result.add(other_sim)
        return tuple(result)

    def _trigger_interaction_start_event(self):
        super()._trigger_interaction_start_event()
        target_sim = self.get_participant(ParticipantType.TargetSim)
        if target_sim is not None:
            services.get_event_manager().process_event(
                test_events.TestEvent.InteractionStart,
                sim_info=target_sim.sim_info,
                interaction=self,
                custom_keys=self.get_keys_to_process_events())
            self._register_target_event_auto_update()

    def required_resources(self):
        resources = super().required_resources()
        resources.add(self.social_group)
        return resources