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        '
    )
class CasStoriesAnswer(HasTunableReference,
                       metaclass=HashedTunedInstanceMetaclass,
                       manager=services.get_instance_manager(
                           sims4.resources.Types.CAS_STORIES_ANSWER)):
    INSTANCE_TUNABLES = {
        'text':
        TunableLocalizedStringFactory(
            description='\n            The text of this answer.\n            ',
            export_modes=ExportModes.ClientBinary,
            tuning_group=GroupNames.UI),
        'weightings':
        TunableList(
            description=
            '\n            A list of objects to apply weightings to if this answer is \n            selected. Weight is the weight that shoudld be added to the chance \n            to receive this object.  In the latter case a trait will be \n            selected from the trait chooser based on its cumulative weighting \n            throughout the CAS Stories survey.\n            ',
            tunable=TunableTuple(weighted_object=TunableVariant(
                trait=Trait.TunableReference(),
                trait_chooser=CasStoriesTraitChooser.TunableReference(),
                aspiration_track=TunableReference(
                    manager=services.get_instance_manager(
                        sims4.resources.Types.ASPIRATION_TRACK)),
                default='trait'),
                                 weight=Tunable(tunable_type=float,
                                                default=0.0),
                                 export_class_name='CASAnswerWeightings'),
            export_modes=ExportModes.ClientBinary)
    }
Exemple #3
0
class TraitToDefinitionPickerInteraction(ObjectDefinitionPickerInteraction):
    INSTANCE_TUNABLES = {
        'trait_to_definition_id':
        TunableMapping(
            description=
            '\n            Backward mapping of trait to what umbrella the sim carries\n            ',
            key_type=Trait.TunableReference(
                description=
                '\n                Trait to look for\n                '),
            value_type=TunableReference(
                description=
                '\n                The object must have this definition.\n                ',
                manager=services.definition_manager())),
        'disabled_toolip':
        TunableLocalizedStringFactory(
            description=
            '\n            Tooltip that displays if the sim currently has the trait in the mapping.\n            '
        )
    }

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

    def _create_dialog(self, owner, target_sim=None, target=None, **kwargs):
        traits_to_find = self.trait_to_definition_id.keys()
        for trait in traits_to_find:
            if not owner.has_trait(trait):
                continue
            self._selected_definition_id = self.trait_to_definition_id[trait]
            break
        return super()._create_dialog(owner,
                                      target_sim=target_sim,
                                      target=target,
                                      **kwargs)

    @flexmethod
    def create_row(cls, inst, row_obj, context=DEFAULT, target=DEFAULT):
        inst_or_cls = inst if inst is not None else cls
        is_disabled = inst_or_cls._selected_definition_id is not None and row_obj.id == inst_or_cls._selected_definition_id.id
        icon_info = IconInfoData(obj_def_id=row_obj.id,
                                 obj_geo_hash=row_obj.thumbnail_geo_state_hash,
                                 obj_material_hash=row_obj.material_variant)
        row = ObjectPickerRow(
            object_id=row_obj.id,
            def_id=row_obj.id,
            icon_info=icon_info,
            tag=row_obj,
            is_enable=not is_disabled,
            row_tooltip=inst_or_cls.disabled_toolip if is_disabled else None,
            name=LocalizationHelperTuning.get_object_name(row_obj))
        inst_or_cls._test_continuation(row, context=context, target=target)
        return row
 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
Exemple #5
0
class CasStoriesTraitChooser(
        HasTunableReference,
        metaclass=HashedTunedInstanceMetaclass,
        manager=services.get_instance_manager(
            sims4.resources.Types.CAS_STORIES_TRAIT_CHOOSER)):
    INSTANCE_TUNABLES = {
        'traits':
        TunableMapping(
            description=
            '\n            A mapping between the weighting value and the trait that will be\n            assigned. The keys of this map are thresholds. Example: if the\n            desired behavior would be to assign trait_a if the weighting w is \n            between 0.0 and 1.0 and trait_b if w > 1.0, then this map should \n            have two entries: (0.0, trait_a), (1.0, trait_b). The weighting of \n            the lowest weighted trait should always be 0.0, and a weighting of\n            0.0 will always select the lowest trait by convention (although the\n            thresholds are otherwise non-inclusive).\n            ',
            key_type=float,
            value_type=Trait.TunableReference(),
            key_name='weighting_threshold',
            value_name='trait_to_assign',
            tuple_name='CasStoriesTraitChooserThresholds',
            minlength=1,
            export_modes=(ExportModes.ClientBinary, ))
    }
 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
Exemple #7
0
class OccultTracker:
    OCCULT_DATA = TunableMapping(
        description=
        "\n        A mapping of occult types to data that affect a Sim's behavior.\n        ",
        key_type=TunableEnumEntry(
            description=
            '\n            The occult type that this entry applies to.\n            ',
            tunable_type=OccultType,
            default=OccultType.HUMAN),
        value_type=TunableTuple(
            description=
            '\n            Occult data specific to this occult type.\n            ',
            fallback_outfit_category=TunableEnumEntry(
                description=
                '\n                The outfit category to default to when the sim changes occult forms\n                and is unable to stay in the same outfit.\n                ',
                tunable_type=OutfitCategory,
                default=OutfitCategory.EVERYDAY),
            occult_form_outfit_category_blacklist=OptionalTunable(
                description=
                '\n                Blacklist outfit categories which are not supported by the occult.  For example: Mermaids\n                are always swimming, so everyday wear is not supported.\n                ',
                tunable=TunableBlacklist(
                    description=
                    '\n                    The list of forbidden outfits for this occult form.\n                    ',
                    tunable=OutfitCategory)),
            occult_trait=Trait.TunableReference(
                description=
                '\n                The trait that all Sims that have this occult are equipped with.\n                ',
                pack_safe=True),
            current_occult_trait=OptionalTunable(
                description=
                '\n                If enabled then this occult will have an alternate form controlled by a trait.\n                ',
                tunable=Trait.TunableReference(
                    description=
                    '\n                    That trait that all Sims currently in this occult are equipped\n                    with.\n                    ',
                    pack_safe=True)),
            part_occult_trait=Trait.TunableReference(
                description=
                '\n                If not None, this allows the tuning of a trait to identify \n                a Sim that is partly this occult.\n                The trait that identifies a Sim that is partly occult. For any\n                part occult trait, we will apply genetics that are half occult\n                and half non-occult.\n                ',
                allow_none=True,
                pack_safe=True),
            additional_occult_traits=TunableSet(
                description=
                "\n                A list of traits that will also be applied to a Sim of this\n                occult type. These will only be applied if this Sim is the full\n                occult type and not just a partial occult. It also doesn't\n                matter if they are in their current occult form or not. These\n                traits will be applied regardless.\n                ",
                tunable=Trait.TunableReference(pack_safe=True)),
            add_current_occult_trait_to_babies=Tunable(
                description=
                '\n                If True, babies will automatically be given the tuned Current\n                Occult Trait when the tuned Occult Trait is added to them. This\n                is currently only meant for aliens.\n                ',
                tunable_type=bool,
                default=True),
            generate_new_human_form_on_add=Tunable(
                description=
                '\n                If True, humans being given this occult for the first time will\n                have a new human form generated for them (i.e. Aliens need a new\n                human form when they change to an alien). If false, their\n                human/base form will remain the same (i.e. Vampires should\n                remain similar in appearance).\n                ',
                tunable_type=bool,
                default=True),
            primary_buck_type=TunableEnumEntry(
                description=
                '\n                The primary buck type for this occult. For example, this is \n                "Powers" for vampires.\n                ',
                tunable_type=BucksType,
                default=BucksType.INVALID,
                pack_safe=True),
            secondary_buck_type=TunableEnumEntry(
                description=
                '\n                The secondary buck type for this occult. For example, this is \n                "Weaknesses" for vampires.\n                ',
                tunable_type=BucksType,
                default=BucksType.INVALID,
                pack_safe=True),
            experience_statistic=TunableReference(
                description=
                '\n                    A reference to a ranked statistic to be used for tracking\n                    the experience and level/ranking up.\n                    ',
                manager=services.get_instance_manager(
                    sims4.resources.Types.STATISTIC),
                class_restrictions=('RankedStatistic', ),
                pack_safe=True,
                allow_none=True),
            cas_add_occult_tooltip=TunableLocalizedString(
                description=
                '\n                This is the tooltip shown on the cas add occult button\n                ',
                allow_none=True),
            cas_alternative_form_add_tooltip=TunableLocalizedString(
                description=
                '\n                This is the tooltip shown on the cas add alternaive form button\n                ',
                allow_none=True),
            cas_alternative_form_delete_tooltip=TunableLocalizedString(
                description=
                '\n                This is the tooltip shown on the cas delete alternaive form button\n                ',
                allow_none=True),
            cas_alternative_form_delete_confirmation=TunableLocalizedString(
                description=
                '\n                This is the text shown in the dialog when deleting the alternative form\n                ',
                allow_none=True),
            cas_alternative_form_sim_name_tooltip=TunableLocalizedString(
                description=
                '\n                This is the tooltip shown on the cas sim skewer alternative form icon\n                ',
                allow_none=True),
            cas_alternative_form_copy_tooltip=TunableLocalizedString(
                description=
                "\n                This is the tooltip shown on the cas copy to alternative form\n                button. If this is None, it is assumed the player shouldn't be\n                able to copy from their base form into their alternative form.\n                ",
                allow_none=True),
            cas_base_form_copy_tooltip=TunableLocalizedString(
                description=
                "\n                This is the tooltip shown on the cas copy to base form button.\n                If this is None, it is assumed the player shouldn't be able to\n                copy from their alternative form into their base form.\n                ",
                allow_none=True),
            cas_base_form_link_tooltip=TunableLocalizedString(
                description=
                '\n                The tooltip shown in CAS when the base form is selected.\n                ',
                allow_none=True),
            cas_alternative_form_link_tooltip=TunableLocalizedString(
                description=
                '\n                The tooltip shown in CAS when the alternative form is selected.\n                ',
                allow_none=True),
            cas_alternative_form_copy_options_heading=TunableLocalizedString(
                description=
                '\n                This is the header text shown on the cas copy to alternative form options panel\n                ',
                allow_none=True),
            cas_disabled_while_in_alternative_form_tooltip=
            TunableLocalizedString(
                description=
                '\n                This is the tooltip shown on any cas panels that are disabled\n                when editing the occult alternative form\n                ',
                allow_none=True),
            cas_molecule_disabled_while_in_alternative_form_tooltip=
            TunableLocalizedString(
                description=
                "\n                This is the tooltip shown on the molecule when it's disabled due\n                to the current Sim being in their alternate occult form.\n                ",
                allow_none=True),
            cas_default_to_alternative_form=Tunable(
                description=
                '\n                If checked, this occult type will default to their alternative\n                form when first added to CAS. If left unchecked, the Sim will\n                default to their base form like usual.\n                ',
                tunable_type=bool,
                default=False),
            cas_can_delete_alternative_form=Tunable(
                description=
                "\n                If checked, the Sim's alternative form can be deleted in CAS. If\n                unchecked, the alternative form can't be deleted. If the Occult \n                doesn't have an alternative form, this is ignored.\n                ",
                tunable_type=bool,
                default=False),
            cas_invalid_age_warning_title=TunableLocalizedString(
                description=
                '\n                The title for the dialog when an occult Sim attempts to enter an \n                invalid age (i.e. aging a Vampire down to child).\n                ',
                allow_none=True),
            cas_invalid_age_warning_body=TunableLocalizedString(
                description=
                '\n                The body text for the dialog when an occult Sim attempts to enter an \n                invalid age (i.e. aging a Vampire down to child).\n                ',
                allow_none=True),
            min_age_for_occult_ui=TunableEnumEntry(
                description=
                '\n                The minimum age a sim can be in order for occult-specific UI to \n                be used.\n                ',
                tunable_type=Age,
                default=Age.BABY),
            export_class_name='OccultTrackerItem'),
        export_modes=ExportModes.All,
        tuple_name='OccultDataTuple')
    VAMPIRE_DAYWALKER_PERK = TunableTuple(
        description='\n        Perks for daywalker vampire Sim.\n        ',
        trait=TunablePackSafeReference(
            description='\n            Trait for vampire.\n            ',
            manager=services.get_instance_manager(
                sims4.resources.Types.TRAIT)),
        perk=TunablePackSafeReference(
            description=
            '\n            Buck type for the daywalker vampire.\n            ',
            manager=services.get_instance_manager(
                sims4.resources.Types.BUCKS_PERK)))

    def __init__(self, sim_info):
        self._sim_info = sim_info
        self._sim_info_map = dict()
        self._pending_occult_type = None
        self._occult_form_available = True

    def __repr__(self):
        return '<OccultTracker: {} ({}) @{}>'.format(
            str(self._sim_info), self._sim_info.occult_types,
            self._sim_info.current_occult_types)

    @classmethod
    def get_fallback_outfit_category(cls, occult_type):
        if occult_type == OccultType.HUMAN:
            return OutfitCategory.EVERYDAY
        return cls.OCCULT_DATA[occult_type].fallback_outfit_category

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

    @property
    def is_occult_form_available(self):
        return self._occult_form_available

    def add_occult_type(self, occult_type):
        if not self.has_occult_type(occult_type):
            self._sim_info.occult_types |= occult_type
            self._update_occult_traits()

    def add_occult_for_premade_sim(self, occult_sim_info, occult_type):
        if self._sim_info_map:
            logger.error(
                'Trying to add occult data for premade sim, {}, but the sim already has occult sim infos in the sim info map of their occult tracker. Data might be lost here!',
                self._sim_info)
            self._sim_info_map.clear()
        current_form_sim_info = self._create_new_sim_info_base_wrapper(
            self._sim_info)
        self._add_sim_info_to_map(current_form_sim_info)
        occult_sim_info._base.current_occult_types = occult_type
        self._sim_info.occult_types |= occult_type
        self._add_sim_info_to_map(occult_sim_info)
        self._update_occult_traits()

    def remove_occult_type(self, occult_type):
        if occult_type == self._sim_info.current_occult_types:
            self.switch_to_occult_type(OccultType.HUMAN)
        if self.has_occult_type(occult_type):
            self._sim_info.occult_types &= ~occult_type
            self._update_occult_traits()
            self._sim_info._base.remove_invalid_face_parts()
            self._sim_info.resend_physical_attributes()
            self._sim_info.resend_current_outfit()
        if occult_type in self._sim_info_map:
            del self._sim_info_map[occult_type]

    def switch_to_occult_type(self, occult_type):
        if occult_type not in self._sim_info_map:
            self.add_occult_type(occult_type)
            self._generate_sim_info(occult_type)
        if self._sim_info.current_occult_types != occult_type:
            self._switch_to_occult_type_internal(occult_type)
            self._update_occult_traits()

    def set_pending_occult_type(self, occult_type):
        self._pending_occult_type = occult_type

    @staticmethod
    def _get_occult_outfit_blacklist(occult_type):
        if occult_type in OccultTracker.OCCULT_DATA:
            return OccultTracker.OCCULT_DATA[
                occult_type].occult_form_outfit_category_blacklist

    @staticmethod
    def _is_outfit_category_forbidden(outfit_category, blacklist):
        return blacklist is not None and not blacklist.test_item(
            outfit_category)

    def _switch_to_occult_type_internal(self, occult_type):
        current_outfit = self._sim_info.get_current_outfit()
        current_occult_types = self._sim_info.current_occult_types
        current_sim_info = self._sim_info_map[current_occult_types]
        current_sim_info.load_outfits(self._sim_info.save_outfits())
        self._sim_info.current_occult_types = occult_type
        occult_sim_info = self._sim_info_map[occult_type]
        self._copy_shared_attributes(occult_sim_info, occult_type,
                                     self._sim_info, current_occult_types)
        SimInfoBaseWrapper.copy_physical_attributes(self._sim_info._base,
                                                    occult_sim_info)
        self._sim_info.load_outfits(occult_sim_info.save_outfits())
        if self._sim_info.has_outfit(current_outfit):
            self._sim_info.set_current_outfit(current_outfit)
        else:
            (outfit_category, outfit_index) = current_outfit
            if outfit_category != OutfitCategory.BATHING:
                if not self._sim_info.has_outfit(
                    (outfit_category, outfit_index)):
                    outfit_category = self.get_fallback_outfit_category(
                        occult_type)
                    outfit_index = 0
                self._sim_info.set_current_outfit(
                    (outfit_category, outfit_index))
        self._sim_info.appearance_tracker.evaluate_appearance_modifiers()
        sim_instance = self._sim_info.get_sim_instance()
        if sim_instance is not None:
            sim_instance.on_outfit_changed(self._sim_info,
                                           self._sim_info.get_current_outfit())
        self._sim_info.resend_physical_attributes()
        self._sim_info.resend_current_outfit()
        self._sim_info.force_resend_suntan_data()

    def has_occult_type(self, occult_type):
        if self._sim_info.occult_types & occult_type:
            return True
        return False

    def get_occult_sim_info(self, occult_type):
        return self._sim_info_map.get(occult_type)

    def _create_new_sim_info_base_wrapper(self, original_sim_info):
        sim_info = SimInfoBaseWrapper(
            gender=original_sim_info.gender,
            age=original_sim_info.age,
            species=original_sim_info.species,
            first_name=original_sim_info.first_name,
            last_name=original_sim_info.last_name,
            breed_name=original_sim_info.breed_name,
            full_name_key=original_sim_info.full_name_key,
            breed_name_key=original_sim_info.breed_name_key)
        SimInfoBaseWrapper.copy_physical_attributes(sim_info._base,
                                                    original_sim_info)
        return sim_info

    def _add_sim_info_to_map(self, sim_info):
        occult_type = sim_info._base.current_occult_types
        if occult_type in self._sim_info_map.keys():
            logger.error(
                "Adding a sim info to the occult tracker's sim info map that already exists. Sim: {}, Duplicate Occult Type: {}",
                self._sim_info, occult_type)
        self._sim_info_map[occult_type] = sim_info

    def _generate_sim_info(self, occult_type, generate_new=True):
        if not self._sim_info_map and occult_type != OccultType.HUMAN:
            generate_new_human_form = self.OCCULT_DATA[
                occult_type].generate_new_human_form_on_add
            self._generate_sim_info(OccultType.HUMAN,
                                    generate_new=generate_new_human_form)
        sim_info = self._create_new_sim_info_base_wrapper(self._sim_info)
        sim_info._base.current_occult_types = occult_type
        if generate_new:
            self._copy_trait_ids(sim_info, self._sim_info)
            generate_occult_siminfo(sim_info._base, sim_info._base,
                                    occult_type)
            outfit_category_blacklist = OccultTracker._get_occult_outfit_blacklist(
                occult_type)
            for outfit_category in REGULAR_OUTFIT_CATEGORIES:
                if not OccultTracker._is_outfit_category_forbidden(
                        outfit_category, outfit_category_blacklist):
                    sim_info.generate_outfit(outfit_category=outfit_category)
            self._copy_shared_attributes(sim_info, occult_type, self._sim_info,
                                         self._sim_info.current_occult_types)
        self._add_sim_info_to_map(sim_info)
        return sim_info

    def has_any_occult_or_part_occult_trait(self):
        for trait_data in self.OCCULT_DATA.values():
            if self.sim_info.has_trait(trait_data.occult_trait):
                return True
            if trait_data.part_occult_trait is not None:
                if self.sim_info.has_trait(trait_data.part_occult_trait):
                    return True
        return False

    @staticmethod
    def _copy_trait_ids(sim_info_a, sim_info_b):
        if any(trait.is_gender_option_trait
               for trait in sim_info_b.trait_tracker):
            sim_info_a._base.base_trait_ids = sim_info_b.trait_ids

    def _copy_shared_attributes(self, sim_info_dst, occult_type_dst,
                                sim_info_src, occult_type_src):
        sim_info_dst.physique = sim_info_src.physique
        OccultTracker._copy_trait_ids(sim_info_dst, sim_info_src)
        fallback_outfit_category = self.get_fallback_outfit_category(
            occult_type_dst)
        outfit_category_blacklist_dst = OccultTracker._get_occult_outfit_blacklist(
            occult_type_dst)
        outfit_category_blacklist_src = OccultTracker._get_occult_outfit_blacklist(
            occult_type_src)
        for outfit_category in HIDDEN_OUTFIT_CATEGORIES:
            if not OccultTracker._is_outfit_category_forbidden(
                    outfit_category, outfit_category_blacklist_dst):
                if OccultTracker._is_outfit_category_forbidden(
                        outfit_category, outfit_category_blacklist_src):
                    continue
                sim_info_dst.generate_merged_outfits_for_category(
                    sim_info_src,
                    outfit_category,
                    outfit_flags=BodyTypeFlag.CLOTHING_ALL,
                    fallback_outfit_category=fallback_outfit_category)

    def _update_occult_traits(self):
        for (occult_type, trait_data) in self.OCCULT_DATA.items():
            if self.has_occult_type(occult_type):
                self._sim_info.add_trait(trait_data.occult_trait)
                for additional_trait in trait_data.additional_occult_traits:
                    self._sim_info.add_trait(additional_trait)
                if self._sim_info.current_occult_types == occult_type:
                    if trait_data.current_occult_trait is not None:
                        self._sim_info.add_trait(
                            trait_data.current_occult_trait)
                        self._sim_info.remove_trait(
                            trait_data.current_occult_trait)
                else:
                    self._sim_info.remove_trait(
                        trait_data.current_occult_trait)
                    self._sim_info.remove_trait(
                        trait_data.current_occult_trait)
                    self._sim_info.remove_trait(trait_data.occult_trait)
                    for additional_trait in trait_data.additional_occult_traits:
                        self._sim_info.remove_trait(additional_trait)
            else:
                self._sim_info.remove_trait(trait_data.current_occult_trait)
                self._sim_info.remove_trait(trait_data.occult_trait)
                for additional_trait in trait_data.additional_occult_traits:
                    self._sim_info.remove_trait(additional_trait)
        if not self._sim_info.occult_types:
            self._sim_info.add_trait(OccultTuning.NO_OCCULT_TRAIT)
        else:
            self._sim_info.remove_trait(OccultTuning.NO_OCCULT_TRAIT)

    def apply_occult_age(self, age):
        if not self._sim_info_map:
            return SimInfoBaseWrapper.apply_age(self.sim_info, age)
        for (occult_type, sim_info) in self._sim_info_map.items():
            if occult_type == self._sim_info.current_occult_types:
                SimInfoBaseWrapper.apply_age(self.sim_info, age)
                SimInfoBaseWrapper.apply_age(sim_info, age)
                SimInfoBaseWrapper.copy_physical_attributes(
                    sim_info, self.sim_info)
            else:
                SimInfoBaseWrapper.apply_age(sim_info, age)

    def validate_appropriate_occult(self, sim, occult_form_before_reset):
        if self._sim_info.current_occult_types == OccultType.HUMAN and self.has_occult_type(
                OccultType.MERMAID):
            if sim.routing_surface.type == SurfaceType.SURFACETYPE_POOL and occult_form_before_reset == OccultType.MERMAID:
                self.switch_to_occult_type(OccultType.MERMAID)
        elif self._sim_info.current_occult_types == OccultType.MERMAID and self.has_occult_type(
                OccultType.HUMAN
        ) and sim.routing_surface.type != SurfaceType.SURFACETYPE_POOL:
            self.switch_to_occult_type(OccultType.HUMAN)

    def apply_occult_genetics(self, parent_a, parent_b, seed, **kwargs):
        r = random.Random()
        r.seed(seed)
        if r.random() < 0.5:
            occult_tracker_a = parent_a.occult_tracker
            occult_tracker_b = parent_b.occult_tracker
        else:
            occult_tracker_a = parent_b.occult_tracker
            occult_tracker_b = parent_a.occult_tracker
        parent_a_normal = occult_tracker_a.get_occult_sim_info(
            OccultType.HUMAN) or occult_tracker_a.sim_info
        parent_b_normal = occult_tracker_b.get_occult_sim_info(
            OccultType.HUMAN) or occult_tracker_b.sim_info
        normal_sim_info = self.get_occult_sim_info(
            OccultType.HUMAN) or self._sim_info
        SimInfoBaseWrapper.apply_genetics(normal_sim_info,
                                          parent_a_normal,
                                          parent_b_normal,
                                          seed=seed,
                                          **kwargs)
        for (occult_type, trait_data) in self.OCCULT_DATA.items():
            if self.has_occult_type(occult_type):
                parent_info_a = occult_tracker_a.get_occult_sim_info(
                    occult_type) or occult_tracker_a.sim_info
                parent_info_b = occult_tracker_b.get_occult_sim_info(
                    occult_type) or occult_tracker_b.sim_info
                offspring_info = self.get_occult_sim_info(
                    occult_type) or self._generate_sim_info(occult_type)
                if occult_type == self._sim_info.current_occult_types:
                    SimInfoBaseWrapper.apply_genetics(self._sim_info,
                                                      parent_info_a,
                                                      parent_info_b,
                                                      seed=seed,
                                                      **kwargs)
                    SimInfoBaseWrapper.copy_physical_attributes(
                        offspring_info, self._sim_info)
                else:
                    SimInfoBaseWrapper.apply_genetics(offspring_info,
                                                      parent_info_a,
                                                      parent_info_b,
                                                      seed=seed,
                                                      **kwargs)
            if trait_data.part_occult_trait is not None:
                if self._sim_info.has_trait(trait_data.part_occult_trait):
                    if occult_tracker_a.has_occult_type(occult_type):
                        parent_info_a = occult_tracker_a.get_occult_sim_info(
                            occult_type) or parent_a_normal
                        parent_info_b = parent_b_normal
                    else:
                        parent_info_a = parent_a_normal
                        parent_info_b = occult_tracker_b.get_occult_sim_info(
                            occult_type) or parent_b_normal
                    SimInfoBaseWrapper.apply_genetics(normal_sim_info,
                                                      parent_info_a,
                                                      parent_info_b,
                                                      seed=seed,
                                                      **kwargs)
        if not self._sim_info.current_occult_types:
            SimInfoBaseWrapper.copy_physical_attributes(
                normal_sim_info, self._sim_info)

    def on_all_traits_loaded(self):
        if self._sim_info_map:
            self._update_occult_traits()
            self._switch_to_occult_type_internal(
                self._sim_info.current_occult_types)
        else:
            self._sim_info.add_trait(OccultTuning.NO_OCCULT_TRAIT)
        for (occult_type, sim_info) in self._sim_info_map.items():
            if occult_type != self._sim_info.current_occult_types:
                sim_info.update_gender_for_traits(
                    gender_override=self._sim_info.gender,
                    trait_ids_override=self._sim_info.trait_ids)
        self._sim_info.update_gender_for_traits()

    def post_load(self):
        if self._pending_occult_type:
            self.switch_to_occult_type(self._pending_occult_type)
            self._pending_occult_type = None

    def get_current_occult_types(self):
        return self._sim_info.current_occult_types

    def get_anim_overrides(self):
        return {'hasOccultForm': self._occult_form_available}

    def on_sim_ready_to_simulate(self, sim):
        for (occult_type, trait_data) in self.OCCULT_DATA.items():
            if self.has_occult_type(occult_type):
                exp_stat = trait_data.experience_statistic
                stat = self._sim_info.commodity_tracker.get_statistic(exp_stat)
                if stat is not None:
                    stat.on_sim_ready_to_simulate()
        self.validate_appropriate_occult(sim, None)

    def save(self):
        data = protocols.PersistableOccultTracker()
        data.occult_types = self._sim_info.occult_types
        data.current_occult_types = self._sim_info.current_occult_types
        data.occult_form_available = self._occult_form_available
        if self._pending_occult_type is not None:
            data.pending_occult_type = self._pending_occult_type
        for (occult_type, sim_info) in self._sim_info_map.items():
            with ProtocolBufferRollback(
                    data.occult_sim_infos) as sim_info_data:
                self._copy_shared_attributes(
                    sim_info, occult_type, self._sim_info,
                    self._sim_info.current_occult_types)
                sim_info_data.occult_type = occult_type
                sim_info_data.outfits = sim_info.save_outfits()
                SimInfoBaseWrapper.copy_physical_attributes(
                    sim_info_data, sim_info)
        return data

    def load(self, data):
        self._sim_info.occult_types = data.occult_types or OccultType.HUMAN
        self._sim_info.current_occult_types = data.current_occult_types or OccultType.HUMAN
        self._pending_occult_type = data.pending_occult_type
        self._occult_form_available = data.occult_form_available
        occult_data_map = {}
        for sim_info_data in data.occult_sim_infos:
            occult_data_map[sim_info_data.occult_type] = sim_info_data
        for occult_type in OccultType:
            if occult_type != OccultType.HUMAN and occult_type not in self.OCCULT_DATA:
                self._sim_info.occult_types &= ~occult_type
                if self._sim_info.current_occult_types == occult_type:
                    self._sim_info.current_occult_types = OccultType.HUMAN
                if self._pending_occult_type == occult_type:
                    self._pending_occult_type = None
            elif occult_type in occult_data_map:
                sim_info_data = occult_data_map[occult_type]
                sim_info = self._generate_sim_info(sim_info_data.occult_type,
                                                   generate_new=False)
                if occult_type == self._sim_info.current_occult_types:
                    SimInfoBaseWrapper.copy_physical_attributes(
                        sim_info_data, self._sim_info._base)
                else:
                    sim_info.load_outfits(sim_info_data.outfits)
                    SimInfoBaseWrapper.copy_physical_attributes(
                        sim_info._base, sim_info_data)
            elif occult_type != OccultType.HUMAN:
                if self.has_occult_type(occult_type):
                    if occult_type == self._sim_info.current_occult_types:
                        self._generate_sim_info(occult_type,
                                                generate_new=False)
Exemple #8
0
class DetectiveCareer(Career):
    INSTANCE_TUNABLES = {
        'crime_scene_events':
        TunableList(
            description=
            '\n            The career events for each of the different types of crime scene.\n            ',
            tunable=TunableReference(manager=services.get_instance_manager(
                sims4.resources.Types.CAREER_EVENT)),
            tuning_group=GroupNames.CAREER),
        'text_clues':
        TunableList(
            description=
            "\n            A list of groups of mutually exclusive clues that the player can\n            discover in the course of solving a crime. Only one clue will be\n            chosen from each group. (e.g. if all hair-color clues are in one\n            group, only one hair-color clue will be chosen so there aren't\n            conflicting clues)\n            ",
            tunable=TunableList(
                description=
                '\n                A group of mutually incompatible clues. Only one clue will be\n                chosen from this group.\n                ',
                tunable=TunableReference(
                    description=
                    '\n                    The clue information and filter term.\n                    ',
                    manager=services.get_instance_manager(
                        sims4.resources.Types.DETECTIVE_CLUE))),
            tuning_group=GroupNames.CAREER),
        'clue_incompatibility':
        TunableMapping(
            description=
            '\n            Clues that are incompatible with each other.\n            ',
            key_name='clue',
            key_type=TunableReference(
                description=
                '\n                The clue that is incompatible with other clues.\n                ',
                manager=services.get_instance_manager(
                    sims4.resources.Types.DETECTIVE_CLUE)),
            value_name='incompatible_clues',
            value_type=TunableList(
                description=
                '\n                The clues that are incompatible with the clue used as the\n                key here.\n                ',
                tunable=TunableReference(manager=services.get_instance_manager(
                    sims4.resources.Types.DETECTIVE_CLUE))),
            tuning_group=GroupNames.CAREER),
        'number_of_clues':
        TunableRange(
            description=
            '\n            The number of clues per crime that the player will be given.\n            ',
            tunable_type=int,
            default=5,
            minimum=1,
            tuning_group=GroupNames.CAREER),
        'number_of_decoys_per_undiscovered_clue':
        TunableRange(
            description=
            '\n            The number of Sims to spawn as decoys for each clue that the\n            detective has not yet discovered.\n            ',
            tunable_type=int,
            default=2,
            minimum=1,
            tuning_group=GroupNames.CAREER),
        'criminal_filter':
        DynamicSimFilter.TunableReference(
            description=
            '\n            The filter to use when spawning a criminal. The filter terms are a\n            randomly generated set of clues.\n            ',
            tuning_group=GroupNames.CAREER),
        'criminal_trait':
        Trait.TunableReference(
            description=
            '\n            A trait that is awarded to the criminal. The trait is added when the\n            criminal is selected, and is removed when a new criminal is selected\n            or the career is quit by the Sim.\n            ',
            tuning_group=GroupNames.CAREER),
        'decoy_filter':
        DynamicSimFilter.TunableReference(
            description=
            '\n            The filter to use when spawning decoys. The filter terms are a\n            subset of the discovered clues.\n            ',
            tuning_group=GroupNames.CAREER)
    }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._used_clues = []
        self._unused_clues = []
        self._case_start_time_in_minutes = 0
        self.crime_scene_event_id = None
        self.active_criminal_sim_id = 0

    @classmethod
    def _tuning_loaded_callback(cls):
        super()._tuning_loaded_callback()
        incompatibility = defaultdict(list)
        for (clue, incompatible_clues) in cls.clue_incompatibility.items():
            for incompatible_clue in incompatible_clues:
                incompatibility[clue].append(incompatible_clue)
                incompatibility[incompatible_clue].append(clue)
        cls.clue_incompatibility = frozendict(incompatibility)

    @classmethod
    def _verify_tuning_callback(cls):
        super()._verify_tuning_callback()
        if len(cls.text_clues) < cls.number_of_clues:
            logger.error(
                'Only {} sets of detective clues have been tuned, but at least {} are required.',
                len(cls.text_clues), cls.number_of_clues)

    def get_custom_gsi_data(self):
        custom_data = {}
        for (clue_index, clue) in enumerate(self._unused_clues):
            custom_data['Clue #{}'.format(clue_index)] = str(clue)
        for (clue_index, clue) in enumerate(self._used_clues):
            custom_data['Used Clue #{}'.format(clue_index)] = str(clue)
        if self.active_criminal_sim_id:
            criminal_sim_info = services.sim_info_manager().get(
                self.active_criminal_sim_id)
            if criminal_sim_info is not None:
                custom_data['Criminal'] = str(criminal_sim_info)
        return custom_data

    def quit_career(self, *args, **kwargs):
        self._clear_crime_data()
        return super().quit_career(*args, **kwargs)

    def _clear_crime_data(self):
        if self.active_criminal_sim_id:
            self.send_detective_telemetry(TELEMETRY_HOOK_DETECTIVE_CASE_END)
            criminal_sim_info = services.sim_info_manager().get(
                self.active_criminal_sim_id)
            if criminal_sim_info is not None:
                criminal_sim_info.remove_trait(self.criminal_trait)
        self._used_clues = []
        self._unused_clues = []

    def create_new_crime_data(self):
        self._clear_crime_data()
        incompatible_clues = set()
        clue_groups = list(self.text_clues)
        random.shuffle(clue_groups)
        for clue_group in clue_groups:
            clue_group = list(set(clue_group) - incompatible_clues)
            if not clue_group:
                continue
            clue = random.choice(clue_group)
            self._unused_clues.append(clue)
            incompatible_clues.update(self.clue_incompatibility.get(clue, ()))
        self._case_start_time_in_minutes = int(
            services.time_service().sim_now.absolute_minutes())
        self.crime_scene_event_id = None
        self.active_criminal_sim_id = self._create_criminal(
            tuple(clue.filter_term for clue in self._unused_clues))
        self.send_detective_telemetry(TELEMETRY_HOOK_DETECTIVE_CASE_START)

    def pop_unused_clue(self):
        if self._unused_clues:
            clue = random.choice(self._unused_clues)
            self._unused_clues.remove(clue)
            self._used_clues.append(clue)
            return clue

    def get_crime_scene_career_event(self):
        if not self.crime_scene_event_id:
            self.crime_scene_event_id = random.choice(
                self.crime_scene_events).guid64
        career_event_manager = services.get_instance_manager(
            sims4.resources.Types.CAREER_EVENT)
        return career_event_manager.get(self.crime_scene_event_id)

    def get_decoy_sim_ids_for_apb(self, persisted_sim_ids=None):
        decoys = []
        decoy_count = len(
            self._unused_clues) * self.number_of_decoys_per_undiscovered_clue
        if decoy_count == 0:
            return decoys
        blacklist_sim_ids = {self.sim_info.id}
        if self.active_criminal_sim_id:
            blacklist_sim_ids.add(self.active_criminal_sim_id)
        used_clue_filter_terms = tuple(clue.get_decoy_filter_term()
                                       for clue in self._used_clues)
        decoy_filter = self.decoy_filter(filter_terms=used_clue_filter_terms)
        sim_filter_service = services.sim_filter_service()
        filter_result = sim_filter_service.submit_matching_filter(
            number_of_sims_to_find=decoy_count,
            sim_filter=decoy_filter,
            sim_constraints=persisted_sim_ids,
            requesting_sim_info=self._sim_info,
            blacklist_sim_ids=blacklist_sim_ids,
            continue_if_constraints_fail=True,
            allow_yielding=False,
            gsi_source_fn=self.get_sim_filter_gsi_name)
        decoys.extend(f.sim_info.id for f in filter_result)
        return decoys

    def get_sim_filter_gsi_name(self):
        return str(self)

    def get_discovered_clues(self):
        return self._used_clues

    def _create_criminal(self, filter_terms):
        criminal_filter = self.criminal_filter(filter_terms=filter_terms)
        criminals = services.sim_filter_service().submit_matching_filter(
            sim_filter=criminal_filter,
            requesting_sim_info=self._sim_info,
            blacklist_sim_ids=set((self.active_criminal_sim_id, )),
            allow_yielding=False,
            gsi_source_fn=self.get_sim_filter_gsi_name)
        if criminals:
            criminal_sim_info = criminals[0].sim_info
            criminal_sim_info.add_trait(self.criminal_trait)
            return criminal_sim_info.sim_id
        logger.error('No criminal was spawned.', trigger_breakpoint=True)
        return 0

    def create_criminal_fixup(self):
        self.active_criminal_sim_id = self._create_criminal(
            tuple(clue.filter_term for clue in itertools.chain(
                self._used_clues, self._unused_clues)))
        return self.active_criminal_sim_id

    def get_persistable_sim_career_proto(self):
        proto = super().get_persistable_sim_career_proto()
        proto.detective_data = SimObjectAttributes_pb2.DetectiveCareerData()
        proto.detective_data.active_criminal_sim_id = self.active_criminal_sim_id if self.active_criminal_sim_id is not None else 0
        proto.detective_data.unused_clue_ids.extend(
            clue.guid64 for clue in self._unused_clues)
        proto.detective_data.used_clue_ids.extend(clue.guid64
                                                  for clue in self._used_clues)
        proto.detective_data.crime_scene_event_id = self.crime_scene_event_id if self.crime_scene_event_id is not None else 0
        proto.detective_data.case_start_time_in_minutes = self._case_start_time_in_minutes
        return proto

    def load_from_persistable_sim_career_proto(self, proto, skip_load=False):
        super().load_from_persistable_sim_career_proto(proto,
                                                       skip_load=skip_load)
        self._unused_clues = []
        self._used_clues = []
        clue_manager = services.get_instance_manager(
            sims4.resources.Types.DETECTIVE_CLUE)
        for clue_id in proto.detective_data.unused_clue_ids:
            clue = clue_manager.get(clue_id)
            if clue is None:
                logger.info(
                    'Trying to load unavailable DETECTIVE_CLUE resource: {}',
                    clue_id)
            else:
                self._unused_clues.append(clue)
        for clue_id in proto.detective_data.used_clue_ids:
            clue = clue_manager.get(clue_id)
            if clue is None:
                logger.info(
                    'Trying to load unavailable DETECTIVE_CLUE resource: {}',
                    clue_id)
            else:
                self._used_clues.append(clue)
        self.active_criminal_sim_id = proto.detective_data.active_criminal_sim_id
        self.crime_scene_event_id = proto.detective_data.crime_scene_event_id
        self._case_start_time_in_minutes = proto.detective_data.case_start_time_in_minutes

    def send_detective_telemetry(self, hook_tag):
        with telemetry_helper.begin_hook(detective_telemetry_writer,
                                         hook_tag,
                                         sim_info=self.sim_info) as hook:
            hook.write_int(TELEMETRY_DETECTIVE_CRIMINAL_ID,
                           self.active_criminal_sim_id)
            if hook_tag == TELEMETRY_HOOK_DETECTIVE_CASE_END and self._case_start_time_in_minutes != 0:
                now = int(services.time_service().sim_now.absolute_minutes())
                duration = now - self._case_start_time_in_minutes
                hook.write_int(TELEMETRY_DETECTIVE_CRIME_DURATION, duration)
Exemple #9
0
class KnowOtherSimTraitOp(BaseTargetedLootOperation):
    TRAIT_SPECIFIED = 0
    TRAIT_RANDOM = 1
    TRAIT_ALL = 2
    FACTORY_TUNABLES = {
        'traits':
        TunableVariant(
            description=
            '\n            The traits that the subject may learn about the target.\n            ',
            specified=TunableTuple(
                description=
                '\n                Specify individual traits that can be learned.\n                ',
                locked_args={'learned_type': TRAIT_SPECIFIED},
                potential_traits=TunableList(
                    description=
                    '\n                    A list of traits that the subject may learn about the target.\n                    ',
                    tunable=Trait.TunableReference())),
            random=TunableTuple(
                description=
                '\n                Specify a random number of traits to learn.\n                ',
                locked_args={'learned_type': TRAIT_RANDOM},
                count=TunableRange(
                    description=
                    '\n                    The number of potential traits the subject may learn about\n                    the target.\n                    ',
                    tunable_type=int,
                    default=1,
                    minimum=1)),
            all=TunableTuple(
                description=
                "\n                The subject Sim may learn all of the target's traits.\n                ",
                locked_args={'learned_type': TRAIT_ALL}),
            default='specified'),
        'notification':
        OptionalTunable(
            description=
            "\n            Specify a notification that will be displayed for every subject if\n            information is learned about each individual target_subject. This\n            should probably be used only if you can ensure that target_subject\n            does not return multiple participants. The first two additional\n            tokens are the Sim and target Sim, respectively. A third token\n            containing a string with a bulleted list of trait names will be a\n            String token in here. If you are learning multiple traits, you\n            should probably use it. If you're learning a single trait, you can\n            get away with writing specific text that does not use this token.\n            ",
            tunable=NotificationElement.TunableFactory(
                locked_args={'recipient_subject': None})),
        'notification_no_more_traits':
        OptionalTunable(
            description=
            '\n            Specify a notification that will be displayed when a Sim knows\n            all traits of another target Sim.\n            ',
            tunable=NotificationElement.TunableFactory(
                locked_args={'recipient_subject': None}))
    }

    def __init__(self, *args, traits, notification,
                 notification_no_more_traits, **kwargs):
        super().__init__(*args, **kwargs)
        self.traits = traits
        self.notification = notification
        self.notification_no_more_traits = notification_no_more_traits

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

    @staticmethod
    def _select_traits(knowledge, trait_tracker, random_count=None):
        traits = tuple(trait for trait in trait_tracker.personality_traits
                       if trait not in knowledge.known_traits)
        if random_count is not None and traits:
            return random.sample(traits, min(random_count, len(traits)))
        return traits

    def _apply_to_subject_and_target(self, subject, target, resolver):
        knowledge = subject.relationship_tracker.get_knowledge(target.sim_id,
                                                               initialize=True)
        if knowledge is None:
            return
        trait_tracker = target.trait_tracker
        if self.traits.learned_type == self.TRAIT_SPECIFIED:
            traits = tuple(trait for trait in self.traits.potential_traits
                           if trait_tracker.has_trait(trait)
                           if trait not in knowledge.known_traits)
        elif self.traits.learned_type == self.TRAIT_ALL:
            traits = self._select_traits(knowledge, trait_tracker)
        elif self.traits.learned_type == self.TRAIT_RANDOM:
            traits = self._select_traits(knowledge,
                                         trait_tracker,
                                         random_count=self.traits.count)
            if not traits and self.notification_no_more_traits is not None:
                interaction = resolver.interaction
                if interaction is not None:
                    self.notification_no_more_traits(
                        interaction).show_notification(
                            additional_tokens=(subject, target),
                            recipients=(subject, ),
                            icon_override=IconInfoData(obj_instance=target))
        for trait in traits:
            knowledge.add_known_trait(trait)
        if traits:
            interaction = resolver.interaction
            if interaction is not None and self.notification is not None:
                trait_string = LocalizationHelperTuning.get_bulleted_list(
                    None, *(trait.display_name(target) for trait in traits))
                self.notification(interaction).show_notification(
                    additional_tokens=(subject, target, trait_string),
                    recipients=(subject, ),
                    icon_override=IconInfoData(obj_instance=target))
class LifeSkillStatistic(HasTunableReference,
                         LifeSkillDisplayMixin,
                         TunedContinuousStatistic,
                         metaclass=HashedTunedInstanceMetaclass,
                         manager=services.get_instance_manager(
                             sims4.resources.Types.STATISTIC)):
    REMOVE_INSTANCE_TUNABLES = ('initial_value', )
    INSTANCE_TUNABLES = {
        'min_value_tuning':
        Tunable(description=
                '\n            The minimum value for this stat.\n            ',
                tunable_type=float,
                default=-100,
                export_modes=ExportModes.All),
        'max_value_tuning':
        Tunable(description=
                '\n            The maximum value for this stat.\n            ',
                tunable_type=float,
                default=100,
                export_modes=ExportModes.All),
        'initial_tuning':
        TunableLiteralOrRandomValue(
            description=
            '\n            The initial value of this stat.  Can be a single value or range.\n            ',
            tunable_type=float,
            default=0,
            minimum=-100),
        'initial_test_based_modifiers':
        TunableList(
            description=
            '\n            List of tuples containing test and a random value. If the test passes,\n            a random value is added to the already random initial value. \n            ',
            tunable=TunableTuple(
                description=
                '\n                A container for test and the corresponding random value.\n                ',
                initial_value_test=TunableTestSet(
                    description=
                    '\n                    If test passes, then the random value tuned will be applied\n                    to the initial value. \n                    '
                ),
                initial_modified_value=TunableLiteralOrRandomValue(
                    description=
                    '\n                    The initial value of this stat.  Can be a single value or range.\n                    ',
                    tunable_type=float,
                    default=0,
                    minimum=-100))),
        'age_to_remove_stat':
        TunableEnumEntry(
            description=
            '\n            When sim reaches this age, this stat will be removed permanently. \n            ',
            tunable_type=Age,
            default=Age.YOUNGADULT),
        'missing_career_decay_rate':
        Tunable(
            description=
            '\n            How much this life skill decay by if sim is late for school/work.\n            ',
            tunable_type=float,
            default=0.0),
        'trait_on_age_up_list':
        TunableList(
            description=
            '\n            A list of trait that will be applied on age up if this commodity \n            falls within the range specified in this tuple.\n            It also contains other visual information like VFX and notification.\n            ',
            tunable=TunableTuple(
                description=
                '\n                A container for the range and corresponding information.\n                ',
                export_class_name='TunableTraitOnAgeUpTuple',
                life_skill_range=TunableInterval(
                    description=
                    '\n                    If the commodity is in this range on age up, the trait\n                    will be applied. \n                    The vfx and notification will be played every time the \n                    range is crossed.\n                    ',
                    tunable_type=float,
                    default_lower=0,
                    default_upper=100,
                    export_modes=ExportModes.All),
                age_up_info=OptionalTunable(
                    description=
                    "\n                    If enabled, this trait will be added on age up given the specified age. \n                    Otherwise, no trait will be added.\n                    We don't use loot because UI needs this trait exported for display.\n                    ",
                    enabled_name='enabled_age_up_info',
                    tunable=TunableTuple(
                        export_class_name='TunableAgeUpInfoTuple',
                        age_to_apply_trait=TunableEnumEntry(
                            description=
                            '\n                            When sim reaches this age, this trait will be added on age up.\n                            ',
                            tunable_type=Age,
                            default=Age.YOUNGADULT),
                        life_skill_trait=Trait.TunableReference(
                            description=
                            '\n                            Trait that is added on age up.\n                            ',
                            pack_safe=True)),
                    export_modes=ExportModes.All),
                in_range_notification=
                OptionalTunable(tunable=TunableUiDialogNotificationSnippet(
                    description=
                    '\n                        Notification that is sent when the commodity reaches this range.\n                        '
                )),
                out_of_range_notification=
                OptionalTunable(tunable=TunableUiDialogNotificationSnippet(
                    description=
                    '\n                        Notification that is sent when the commodity exits this range.\n                        '
                )),
                vfx_triggered=TunablePlayEffectVariant(
                    description=
                    '\n                    Vfx to play on the sim when commodity enters this threshold.\n                    ',
                    tuning_group=GroupNames.ANIMATION),
                in_range_buff=OptionalTunable(tunable=TunableBuffReference(
                    description=
                    '\n                        Buff that is added when sim enters this threshold.\n                        '
                )))),
        'headline':
        TunableReference(
            description=
            '\n            The headline that we want to send down when this life skill updates.\n            ',
            manager=services.get_instance_manager(
                sims4.resources.Types.HEADLINE),
            tuning_group=GroupNames.UI)
    }

    def __init__(self, tracker):
        self._vfx = None
        super().__init__(tracker, self.get_initial_value())
        self._last_update_value = None
        if not tracker.load_in_progress:
            self._apply_initial_value_modifier()

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

    @classmethod
    def get_initial_value(cls):
        return cls.initial_tuning.random_int()

    def _apply_initial_value_modifier(self):
        initial_value = self._value
        resolver = SingleSimResolver(self.tracker.owner)
        for initial_modifier in self.initial_test_based_modifiers:
            if initial_modifier.initial_value_test.run_tests(resolver):
                initial_value += initial_modifier.initial_modified_value.random_float(
                )
        self.set_value(initial_value, from_add=True)

    def _update_value(self):
        old_value = self._value
        super()._update_value()
        new_value = self._value
        self._evaluate_threshold(old_value=old_value, new_value=new_value)

    def _evaluate_threshold(self, old_value=0, new_value=0, from_load=False):
        old_infos = []
        new_infos = []
        for range_info in self.trait_on_age_up_list:
            if old_value in range_info.life_skill_range:
                old_infos.append(range_info)
            if new_value in range_info.life_skill_range:
                new_infos.append(range_info)
        old_infos_set = set(old_infos)
        new_infos_set = set(new_infos)
        out_ranges = old_infos_set - new_infos_set
        in_ranges = new_infos_set - old_infos_set
        owner = self.tracker.owner
        is_household_sim = owner.is_selectable and owner.valid_for_distribution
        if not from_load:
            for out_range in out_ranges:
                if out_range.out_of_range_notification is not None and is_household_sim:
                    dialog = out_range.out_of_range_notification(
                        owner, resolver=SingleSimResolver(owner))
                    dialog.show_dialog(additional_tokens=(owner, ))
                if out_range.in_range_buff is not None:
                    owner.Buffs.remove_buff_by_type(
                        out_range.in_range_buff.buff_type)
        for in_range in in_ranges:
            if in_range.in_range_notification is not None and not from_load and is_household_sim:
                dialog = in_range.in_range_notification(
                    owner, resolver=SingleSimResolver(owner))
                dialog.show_dialog(additional_tokens=(owner, ))
            if in_range.vfx_triggered is not None and not from_load and is_household_sim:
                if self._vfx is not None:
                    self._vfx.stop(immediate=True)
                    self._vfx = None
                sim = owner.get_sim_instance(
                    allow_hidden_flags=ALL_HIDDEN_REASONS)
                if sim is not None:
                    self._vfx = in_range.vfx_triggered(sim)
                    self._vfx.start()
            if in_range.in_range_buff is not None:
                owner.Buffs.add_buff(
                    in_range.in_range_buff.buff_type,
                    buff_reason=in_range.in_range_buff.buff_reason)

    def _on_statistic_modifier_changed(self, notify_watcher=True):
        super()._on_statistic_modifier_changed(notify_watcher=notify_watcher)
        self.create_and_send_commodity_update_msg(is_rate_change=False)

    @constproperty
    def remove_on_convergence():
        return False

    def set_value(self,
                  value,
                  *args,
                  from_load=False,
                  interaction=None,
                  **kwargs):
        old_value = self._value
        super().set_value(value,
                          *args,
                          from_load=from_load,
                          interaction=interaction,
                          **kwargs)
        new_value = self._value
        self._evaluate_threshold(old_value=old_value,
                                 new_value=new_value,
                                 from_load=from_load)
        if from_load:
            return
        self.create_and_send_commodity_update_msg(is_rate_change=False,
                                                  from_add=kwargs.get(
                                                      'from_add', False))

    def on_remove(self, on_destroy=False):
        super().on_remove(on_destroy=on_destroy)
        if self._vfx is not None:
            self._vfx.stop(immediate=True)
            self._vfx = None

    def save_statistic(self, commodities, skills, ranked_statistics, tracker):
        message = protocols.Commodity()
        message.name_hash = self.guid64
        message.value = self.get_saved_value()
        if self._time_of_last_value_change:
            message.time_of_last_value_change = self._time_of_last_value_change.absolute_ticks(
            )
        commodities.append(message)

    def create_and_send_commodity_update_msg(self,
                                             is_rate_change=True,
                                             allow_npc=False,
                                             from_add=False):
        current_value = self.get_value()
        change_rate = self.get_change_rate()
        life_skill_msg = Commodities_pb2.LifeSkillUpdate()
        life_skill_msg.sim_id = self.tracker.owner.id
        life_skill_msg.life_skill_id = self.guid64
        life_skill_msg.curr_value = current_value
        life_skill_msg.rate_of_change = change_rate
        life_skill_msg.is_from_add = from_add
        send_sim_life_skill_update_message(self.tracker.owner, life_skill_msg)
        if self._last_update_value is None:
            value_to_send = change_rate
        else:
            value_to_send = current_value - self._last_update_value
        self._last_update_value = current_value
        if value_to_send != 0 and not from_add:
            self.headline.send_headline_message(self.tracker.owner,
                                                value_to_send)

    def create_and_send_life_skill_delete_msg(self):
        life_skill_msg = Commodities_pb2.LifeSkillDelete()
        life_skill_msg.sim_id = self.tracker.owner.id
        life_skill_msg.life_skill_id = self.guid64
        send_sim_life_skill_delete_message(self.tracker.owner, life_skill_msg)
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 #12
0
class ProtestSituation(SituationComplexCommon):
    INSTANCE_TUNABLES = {
        'number_of_protesters':
        TunableInterval(
            description=
            '\n                The number of other protesters to bring to the situation.\n                \n                This is an inclusive min/max range.\n                ',
            tunable_type=float,
            minimum=1,
            default_lower=3,
            default_upper=5,
            tuning_group=GroupNames.SITUATION),
        'protester_job':
        SituationJob.TunablePackSafeReference(
            description=
            '\n                The SituationJob for the Protester.\n                ',
            tuning_group=GroupNames.SITUATION),
        'protester_role':
        RoleState.TunablePackSafeReference(
            description=
            '\n                The SituationRole for the Protester.\n                ',
            tuning_group=GroupNames.SITUATION),
        'protester_search_filter':
        DynamicSimFilter.TunablePackSafeReference(
            description=
            '\n                Sim filter used to find sims or conform them into protesters.\n                We will select the cause for the protesters at runtime \n                from the specified weighted causes list below.\n                ',
            tuning_group=GroupNames.SITUATION),
        'protesting_situation_state':
        _ProtestingState.TunableFactory(
            description=
            '\n                The protest state.  Interactions of interest should be set \n                to interactions that may be run in order to end the situation.\n                ',
            tuning_group=GroupNames.SITUATION),
        'protestables':
        TunableList(
            description=
            '\n            List of possible protests and the signs for them.\n            These will be picked from based off the cause\n            ',
            tunable=TunableTuple(
                description=
                '\n                A protestable.  It is a cause and the sign to use for the cause.\n                ',
                sign_definition=TunableReference(
                    description=
                    '\n                    The definition of a protester flag.\n                    ',
                    manager=services.definition_manager()),
                cause=Trait.TunableReference(
                    description=
                    '\n                    The trait associated with this flag.\n                    ',
                    pack_safe=True)),
            tuning_group=GroupNames.SITUATION),
        'weighted_causes':
        TunableList(
            description=
            '\n            A weighted list of causes to choose for the protest.  We will pick\n            a random cause from this list as the subject of the protest.\n            ',
            tunable=TunableTuple(cause=Trait.TunablePackSafeReference(
                description=
                '\n                    The cause that this protest will promote/protest.\n                    '
            ),
                                 weight=Tunable(tunable_type=int, default=1)),
            tuning_group=GroupNames.SITUATION)
    }
    REMOVE_INSTANCE_TUNABLES = Situation.NON_USER_FACING_REMOVE_INSTANCE_TUNABLES

    @classmethod
    def _states(cls):
        return (SituationStateData(1,
                                   _ProtestingState,
                                   factory=cls.protesting_situation_state), )

    @classmethod
    def _get_tuned_job_and_default_role_state_tuples(cls):
        return [(cls.protester_job, cls.protester_role)]

    @classmethod
    def get_predefined_guest_list(cls):
        weighted_causes = tuple(
            (item.weight, item.cause) for item in cls.weighted_causes)
        cause = sims4.random.weighted_random_item(weighted_causes)
        protester_filter = cls.protester_search_filter(
            filter_terms=(TraitFilterTerm(invert_score=False,
                                          minimum_filter_score=0,
                                          trait=cause,
                                          ignore_if_wrong_pack=False), ))
        num_protesters_to_request = cls.number_of_protesters.random_int()
        instanced_sim_ids = [
            sim.sim_info.id
            for sim in services.sim_info_manager().instanced_sims_gen()
        ]
        household_sim_ids = [
            sim_info.id
            for sim_info in services.active_household().sim_info_gen()
        ]
        situation_manager = services.get_zone_situation_manager()
        global_auto_fill_blacklist = situation_manager.get_auto_fill_blacklist(
        )
        blacklist_sim_ids = set(
            itertools.chain(instanced_sim_ids, household_sim_ids,
                            global_auto_fill_blacklist))
        protester_results = services.sim_filter_service(
        ).submit_matching_filter(
            sim_filter=protester_filter,
            callback=None,
            allow_yielding=False,
            number_of_sims_to_find=num_protesters_to_request,
            blacklist_sim_ids=blacklist_sim_ids,
            gsi_source_fn=cls.get_sim_filter_gsi_name)
        if not protester_results:
            return
        guest_list = SituationGuestList(invite_only=True)
        for result in protester_results:
            guest_list.add_guest_info(
                SituationGuestInfo(result.sim_info.sim_id, cls.protester_job,
                                   RequestSpawningOption.MUST_SPAWN,
                                   BouncerRequestPriority.BACKGROUND_LOW))
        return guest_list

    def start_situation(self):
        super().start_situation()
        protestable = self.find_protestable_using_guest_list()
        initial_state = self.protesting_situation_state()
        if protestable:
            initial_state.set_sign_definition(protestable.sign_definition)
        self._change_state(initial_state)

    def _choose_protestable_from_sim(self, sim):
        possible_protests = [
            protestable for protestable in self.protestables
            if sim.has_trait(protestable.cause)
        ]
        if not possible_protests:
            return
        return random.choice(possible_protests)

    def find_protestable_using_guest_list(self):
        for guest in self._guest_list.get_guest_infos_for_job(
                self.protester_job):
            sim_info = services.sim_info_manager().get(guest.sim_id)
            if sim_info is not None:
                return self._choose_protestable_from_sim(sim_info)
Exemple #13
0
class ObjectPart(HasTunableReference,
                 metaclass=TunedInstanceMetaclass,
                 manager=services.object_part_manager()):
    INSTANCE_TUNABLES = {
        'supported_posture_types':
        TunablePostureTypeListSnippet(
            description=
            '\n            The postures supported by this part. If empty, assumes all postures\n            are supported.\n            '
        ),
        'supported_affordance_data':
        TunableTuple(
            description=
            '\n            Define affordance compatibility for this part.\n            ',
            compatibility=TunableAffordanceFilterSnippet(
                description=
                '\n                Affordances supported by the part\n                '
            ),
            consider_mixers=Tunable(
                description=
                '\n                If checked, mixers are filtered through this compatibility\n                check. If unchecked, all mixers are assumed to be valid to run\n                on this part.\n                ',
                tunable_type=bool,
                default=False)),
        'blacklisted_buffs':
        TunableList(
            description=
            '\n            A list of buffs that will disable this part as a candidate to run an\n            interaction.\n            ',
            tunable=TunableReference(
                description=
                '\n               Reference to a buff to disable the part.\n               ',
                manager=services.get_instance_manager(
                    sims4.resources.Types.BUFF),
                pack_safe=True)),
        'trait_requirements':
        TunableWhiteBlackList(
            description=
            '\n            Trait blacklist and whitelist requirements to pick this part.\n            ',
            tunable=Trait.TunableReference(
                description=
                '\n               Reference to the trait white/blacklists.\n               ',
                pack_safe=True)),
        'subroot':
        TunableReference(
            description=
            '\n            The reference of the subroot definition in the part.\n            ',
            manager=services.subroot_manager(),
            allow_none=True),
        'portal_data':
        TunableSet(
            description=
            '\n            If the object owning this part has a portal component tuned, the\n            specified portals will be created for each part of this type. The\n            root position of the part is the subroot position.\n            ',
            tunable=TunablePortalReference(pack_safe=True)),
        'can_pick':
        Tunable(
            description=
            '\n            If checked, this part can be picked (selected as target when\n            clicking on object.)  If unchecked, cannot be picked.\n            ',
            tunable_type=bool,
            default=True),
        'part_surface':
        TunableVariant(
            description=
            '\n            The rules to determine the surface type for this object.\n            ',
            part_owner=_PartOwnerSurfaceType.TunableFactory(),
            override_surface=_OverrideSurfaceType.TunableFactory(),
            default='part_owner')
    }
    _bone_name_hashes_for_part_suffices = None

    @classmethod
    def register_tuned_animation(cls, *_, **__):
        pass

    @classmethod
    def add_auto_constraint(cls, participant_type, tuned_constraint, **kwargs):
        pass

    @classmethod
    def get_bone_name_hashes_for_part_suffix(cls, part_suffix):
        if cls._bone_name_hashes_for_part_suffices is None:
            cls._bone_name_hashes_for_part_suffices = {}
        if part_suffix in cls._bone_name_hashes_for_part_suffices:
            return cls._bone_name_hashes_for_part_suffices[part_suffix]
        bone_name_hashes = set()
        if cls.subroot is not None:
            for bone_name_hash in cls.subroot.bone_names:
                if part_suffix is not None:
                    bone_name_hash = hash32(str(part_suffix),
                                            initial_hash=bone_name_hash)
                bone_name_hashes.add(bone_name_hash)
        cls._bone_name_hashes_for_part_suffices[part_suffix] = frozenset(
            bone_name_hashes)
        return cls._bone_name_hashes_for_part_suffices[part_suffix]
class AdoptionService(Service):
    PET_ADOPTION_CATALOG_LIFETIME = TunableSimMinute(description='\n        The amount of time in Sim minutes before a pet Sim is removed from the adoption catalog.\n        ', default=60, minimum=0)
    PET_ADOPTION_GENDER_OPTION_TRAITS = TunableList(description='\n        List of gender option traits from which one will be applied to generated\n        Pets based on the tuned weights.\n        ', tunable=TunableTuple(description='\n            A weighted gender option trait that might be applied to the\n            generated Pet.\n            ', weight=Tunable(description='\n                The relative weight of this trait.\n                ', tunable_type=float, default=1), trait=Trait.TunableReference(description='\n                A gender option trait that might be applied to the generated\n                Pet.\n                ', pack_safe=True)))

    def __init__(self):
        self._sim_infos = defaultdict(list)
        self._real_sim_ids = None
        self._creation_times = {}

    @classproperty
    def save_error_code(cls):
        return persistence_error_types.ErrorCodes.SERVICE_SAVE_FAILED_ADOPTION_SERVICE

    def timeout_real_sim_infos(self):
        sim_now = services.time_service().sim_now
        for sim_id in tuple(self._creation_times.keys()):
            elapsed_time = (sim_now - self._creation_times[sim_id]).in_minutes()
            if elapsed_time > self.PET_ADOPTION_CATALOG_LIFETIME:
                del self._creation_times[sim_id]

    def save(self, save_slot_data=None, **kwargs):
        self.timeout_real_sim_infos()
        adoption_service_proto = GameplaySaveData_pb2.PersistableAdoptionService()
        for (sim_id, creation_time) in self._creation_times.items():
            with ProtocolBufferRollback(adoption_service_proto.adoptable_sim_data) as msg:
                msg.adoptable_sim_id = sim_id
                msg.creation_time = creation_time.absolute_ticks()
        save_slot_data.gameplay_data.adoption_service = adoption_service_proto

    def on_all_households_and_sim_infos_loaded(self, _):
        save_slot_data = services.get_persistence_service().get_save_slot_proto_buff()
        sim_info_manager = services.sim_info_manager()
        for sim_data in save_slot_data.gameplay_data.adoption_service.adoptable_sim_data:
            sim_info = sim_info_manager.get(sim_data.adoptable_sim_id)
            if sim_info is None:
                continue
            self._creation_times[sim_data.adoptable_sim_id] = DateAndTime(sim_data.creation_time)

    def stop(self):
        self._sim_infos.clear()
        self._creation_times.clear()

    def add_sim_info(self, age, gender, species):
        key = (age, gender, species)
        sim_info = SimInfoBaseWrapper(age=age, gender=gender, species=species)
        generate_random_siminfo(sim_info._base)
        breed_tag = get_random_breed_tag(species)
        if breed_tag is not None:
            try_conform_sim_info_to_breed(sim_info, breed_tag)
        trait_manager = services.get_instance_manager(sims4.resources.Types.TRAIT)
        traits = {trait_manager.get(trait_id) for trait_id in sim_info.trait_ids}
        if sim_info.is_pet:
            gender_option_traits = [(entry.weight, entry.trait) for entry in self.PET_ADOPTION_GENDER_OPTION_TRAITS if entry.trait.is_valid_trait(sim_info)]
            selected_trait = sims4.random.weighted_random_item(gender_option_traits)
            if selected_trait is not None:
                traits.add(selected_trait)
        sim_info.set_trait_ids_on_base(trait_ids_override=list(t.guid64 for t in traits))
        sim_info.first_name = SimSpawner.get_random_first_name(gender, species)
        sim_info.manager = services.sim_info_manager()
        Distributor.instance().add_object(sim_info)
        self._sim_infos[key].append(sim_info)

    def add_real_sim_info(self, sim_info):
        self._creation_times[sim_info.sim_id] = services.time_service().sim_now

    def get_sim_info(self, sim_id):
        for sim_info in itertools.chain.from_iterable(self._sim_infos.values()):
            if sim_info.sim_id == sim_id:
                return sim_info
        for adoptable_sim_id in self._creation_times.keys():
            if sim_id == adoptable_sim_id:
                return services.sim_info_manager().get(adoptable_sim_id)

    @contextmanager
    def real_sim_info_cache(self):
        self.timeout_real_sim_infos()
        self._real_sim_ids = defaultdict(list)
        sim_info_manager = services.sim_info_manager()
        for sim_id in self._creation_times.keys():
            sim_info = sim_info_manager.get(sim_id)
            key = (sim_info.age, sim_info.gender, sim_info.species)
            self._real_sim_ids[key].append(sim_id)
        try:
            yield None
        finally:
            self._real_sim_ids.clear()
            self._real_sim_ids = None

    def get_sim_infos(self, interval, age, gender, species):
        key = (age, gender, species)
        real_sim_count = len(self._real_sim_ids[key]) if self._real_sim_ids is not None else 0
        entry_count = len(self._sim_infos[key]) + real_sim_count
        if entry_count < interval.lower_bound:
            while entry_count < interval.upper_bound:
                self.add_sim_info(age, gender, species)
                entry_count += 1
        real_sim_infos = []
        if self._real_sim_ids is not None:
            sim_info_manager = services.sim_info_manager()
            for sim_id in tuple(self._real_sim_ids[key]):
                sim_info = sim_info_manager.get(sim_id)
                if sim_info is not None:
                    real_sim_infos.append(sim_info)
        return tuple(itertools.chain(self._sim_infos[key], real_sim_infos))

    def remove_sim_info(self, sim_info):
        for sim_infos in self._sim_infos.values():
            if sim_info in sim_infos:
                sim_infos.remove(sim_info)
        if sim_info.sim_id in self._creation_times:
            del self._creation_times[sim_info.sim_id]

    def create_adoption_sim_info(self, sim_info, household=None, account=None, zone_id=None):
        sim_creator = SimCreator(age=sim_info.age, gender=sim_info.gender, species=sim_info.extended_species, first_name=sim_info.first_name, last_name=sim_info.last_name)
        (sim_info_list, new_household) = SimSpawner.create_sim_infos((sim_creator,), household=household, account=account, zone_id=0, creation_source='adoption')
        SimInfoBaseWrapper.copy_physical_attributes(sim_info_list[0], sim_info)
        sim_info_list[0].pelt_layers = sim_info.pelt_layers
        sim_info_list[0].breed_name_key = sim_info.breed_name_key
        sim_info_list[0].load_outfits(sim_info.save_outfits())
        sim_info_list[0].resend_physical_attributes()
        return (sim_info_list[0], new_household)

    def convert_base_sim_info_to_full(self, sim_id):
        current_sim_info = self.get_sim_info(sim_id)
        if current_sim_info is None:
            return
        (new_sim_info, new_household) = self.create_adoption_sim_info(current_sim_info)
        new_household.set_to_hidden()
        self.remove_sim_info(current_sim_info)
        self.add_real_sim_info(new_sim_info)
        return new_sim_info
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()
class TraitTracker(AffordanceCacheMixin, SimInfoTracker):
    GENDER_TRAITS = TunableMapping(
        description=
        '\n        A mapping from gender to trait. Any Sim with the specified gender will\n        have the corresponding gender trait.\n        ',
        key_type=TunableEnumEntry(
            description="\n            The Sim's gender.\n            ",
            tunable_type=Gender,
            default=Gender.MALE),
        value_type=Trait.TunableReference(
            description=
            '\n            The trait associated with the specified gender.\n            '
        ))
    DEFAULT_GENDER_OPTION_TRAITS = TunableMapping(
        description=
        "\n        A mapping from gender to default gender option traits. After loading the\n        sim's trait tracker, if no gender option traits are found (e.g. loading\n        a save created prior to them being added), the tuned gender option traits\n        for the sim's gender will be added.\n        ",
        key_type=TunableEnumEntry(
            description="\n            The Sim's gender.\n            ",
            tunable_type=Gender,
            default=Gender.MALE),
        value_type=TunableSet(
            description=
            '\n            The default gender option traits to be added for this gender.\n            ',
            tunable=Trait.TunableReference(pack_safe=True)))
    SPECIES_TRAITS = TunableMapping(
        description=
        '\n        A mapping from species to trait. Any Sim of the specified species will\n        have the corresponding species trait.\n        ',
        key_type=TunableEnumEntry(
            description="\n            The Sim's species.\n            ",
            tunable_type=Species,
            default=Species.HUMAN,
            invalid_enums=(Species.INVALID, )),
        value_type=Trait.TunableReference(
            description=
            '\n            The trait associated with the specified species.\n            ',
            pack_safe=True))
    SPECIES_EXTENDED_TRAITS = TunableMapping(
        description=
        '\n        A mapping from extended species to trait. Any Sim of the specified \n        extended species will have the corresponding extended species trait.\n        ',
        key_type=TunableEnumEntry(
            description=
            "\n            The Sim's extended species.\n            ",
            tunable_type=SpeciesExtended,
            default=SpeciesExtended.SMALLDOG,
            invalid_enums=(SpeciesExtended.INVALID, )),
        value_type=Trait.TunableReference(
            description=
            '\n            The trait associated with the specified extended species.\n            ',
            pack_safe=True))
    TRAIT_INHERITANCE = TunableList(
        description=
        '\n        Define how specific traits are transferred to offspring. Define keys of\n        sets of traits resulting in the assignment of another trait, weighted\n        against other likely outcomes.\n        ',
        tunable=TunableTuple(
            description=
            '\n            A set of trait requirements and outcomes. Please note that inverted\n            requirements are not necessary. The game will automatically swap\n            parents A and B to try to fulfill the constraints.\n            \n            e.g. Alien Inheritance\n                Alien inheritance follows a simple set of rules:\n                 Alien+Alien always generates aliens\n                 Alien+None always generates part aliens\n                 Alien+PartAlien generates either aliens or part aliens\n                 PartAlien+PartAlien generates either aliens, part aliens, or regular Sims\n                 PartAlien+None generates either part aliens or regular Sims\n                 \n                Given the specifications involving "None", we need to probably\n                blacklist the two traits to detect a case where only one of the\n                two parents has a meaningful trait:\n                \n                a_whitelist = Alien\n                b_whitelist = Alien\n                outcome = Alien\n                \n                a_whitelist = Alien\n                b_blacklist = Alien,PartAlien\n                outcome = PartAlien\n                \n                etc...\n            ',
            parent_a_whitelist=TunableList(
                description=
                '\n                Parent A must have ALL these traits in order to generate this\n                outcome.\n                ',
                tunable=Trait.TunableReference(pack_safe=True)),
            parent_a_blacklist=TunableList(
                description=
                '\n                Parent A must not have ANY of these traits in order to generate this\n                outcome.\n                ',
                tunable=Trait.TunableReference(pack_safe=True)),
            parent_b_whitelist=TunableList(
                description=
                '\n                Parent B must have ALL these traits in order to generate this\n                outcome.\n                ',
                tunable=Trait.TunableReference(pack_safe=True)),
            parent_b_blacklist=TunableList(
                description=
                '\n                Parent B must not have ANY of these traits in order to generate this\n                outcome.\n                ',
                tunable=Trait.TunableReference(pack_safe=True)),
            outcomes=TunableList(
                description=
                '\n                A weighted list of potential outcomes given that the\n                requirements have been satisfied.\n                ',
                tunable=TunableTuple(
                    description=
                    '\n                    A weighted outcome. The weight is relative to other entries\n                    within this outcome set.\n                    ',
                    weight=Tunable(
                        description=
                        '\n                        The relative weight of this outcome versus other\n                        outcomes in this same set.\n                        ',
                        tunable_type=float,
                        default=1),
                    trait=Trait.TunableReference(
                        description=
                        '\n                        The potential inherited trait.\n                        ',
                        allow_none=True,
                        pack_safe=True)))))

    def __init__(self, sim_info):
        super().__init__()
        self._sim_info = sim_info
        self._sim_info.on_base_characteristic_changed.append(
            self.add_auto_traits)
        self._equipped_traits = set()
        self._unlocked_equip_slot = 0
        self._buff_handles = {}
        self.trait_vfx_mask = 0
        self._hiding_relationships = False
        self._day_night_state = None
        self._load_in_progress = False

    def __iter__(self):
        return self._equipped_traits.__iter__()

    def __len__(self):
        return len(self._equipped_traits)

    def can_add_trait(self, trait):
        if not self._has_valid_lod(trait):
            return False
        if self.has_trait(trait):
            logger.info('Trying to equip an existing trait {} for Sim {}',
                        trait, self._sim_info)
            return False
        if trait.is_personality_trait and self.empty_slot_number == 0:
            logger.info('Reach max equipment slot number {} for Sim {}',
                        self.equip_slot_number, self._sim_info)
            return False
        if not trait.is_valid_trait(self._sim_info):
            logger.info(
                "Trying to equip a trait {} that conflicts with Sim {}'s age {} or gender {}",
                trait, self._sim_info, self._sim_info.age,
                self._sim_info.gender)
            return False
        elif self.is_conflicting(trait):
            logger.info('Trying to equip a conflicting trait {} for Sim {}',
                        trait, self._sim_info)
            return False
        return True

    def add_auto_traits(self):
        for trait in itertools.chain(self.GENDER_TRAITS.values(),
                                     self.SPECIES_TRAITS.values(),
                                     self.SPECIES_EXTENDED_TRAITS.values()):
            if self.has_trait(trait):
                self._remove_trait(trait)
        auto_traits = (self.GENDER_TRAITS.get(self._sim_info.gender),
                       self.SPECIES_TRAITS.get(self._sim_info.species),
                       self.SPECIES_EXTENDED_TRAITS.get(
                           self._sim_info.extended_species))
        for trait in auto_traits:
            if trait is None:
                continue
            self._add_trait(trait)

    def remove_invalid_traits(self):
        for trait in tuple(self._equipped_traits):
            if not trait.is_valid_trait(self._sim_info):
                self._sim_info.remove_trait(trait)

    def sort_and_send_commodity_list(self):
        if not self._sim_info.is_selectable:
            return
        final_list = []
        commodities = self._sim_info.get_initial_commodities()
        for trait in self._equipped_traits:
            if not trait.ui_commodity_sort_override:
                continue
            final_list = [
                override_commodity
                for override_commodity in trait.ui_commodity_sort_override
                if override_commodity in commodities
            ]
            break
        if not final_list:
            final_list = sorted(commodities,
                                key=operator.attrgetter('ui_sort_order'))
        self._send_commodity_list_msg(final_list)

    def _send_commodity_list_msg(self, commodity_list):
        list_msg = Commodities_pb2.CommodityListUpdate()
        list_msg.sim_id = self._sim_info.sim_id
        for commodity in commodity_list:
            if commodity.visible:
                stat = self._sim_info.commodity_tracker.get_statistic(
                    commodity)
                if stat:
                    if stat.is_visible_commodity():
                        with ProtocolBufferRollback(
                                list_msg.commodities) as commodity_msg:
                            stat.populate_commodity_update_msg(
                                commodity_msg, is_rate_change=False)
        send_sim_commodity_list_update_message(self._sim_info, list_msg)

    def _update_initial_commodities(self, trait, previous_initial_commodities):
        should_update_commodity_ui = False
        current_initial_commodities = self._sim_info.get_initial_commodities()
        commodities_to_remove = previous_initial_commodities - current_initial_commodities
        for commodity_to_remove in commodities_to_remove:
            commodity_inst = self._sim_info.commodity_tracker.get_statistic(
                commodity_to_remove)
            if commodity_inst is None:
                continue
            if not should_update_commodity_ui:
                if commodity_inst.is_visible_commodity():
                    should_update_commodity_ui = True
            commodity_inst.core = False
            self._sim_info.commodity_tracker.remove_statistic(
                commodity_to_remove)
        commodities_to_add = current_initial_commodities - previous_initial_commodities
        for commodity_to_add in commodities_to_add:
            commodity_inst = self._sim_info.commodity_tracker.add_statistic(
                commodity_to_add)
            if commodity_inst is None:
                continue
            commodity_inst.core = True
            if not should_update_commodity_ui:
                if commodity_inst.is_visible_commodity():
                    should_update_commodity_ui = True
        if should_update_commodity_ui:
            self.sort_and_send_commodity_list()

    def _add_trait(self, trait, from_load=False):
        if not self.can_add_trait(trait):
            return False
        initial_commodities_modified = trait.initial_commodities or trait.initial_commodities_blacklist
        if initial_commodities_modified:
            previous_initial_commodities = self._sim_info.get_initial_commodities(
            )
        self._equipped_traits.add(trait)
        if initial_commodities_modified:
            self._update_initial_commodities(trait,
                                             previous_initial_commodities)
        if not trait.buffs_add_on_spawn_only or self._sim_info.is_instanced(
                allow_hidden_flags=ALL_HIDDEN_REASONS):
            try:
                self._add_buffs(trait)
            except Exception as e:
                logger.exception(
                    'Error adding buffs while adding trait: {0}. {1}.',
                    trait.__name__,
                    e,
                    owner='asantos')
        self._add_vfx_mask(trait, send_op=not from_load)
        self._add_day_night_tracking(trait)
        self.update_trait_effects()
        if trait.is_ghost_trait:
            sims.ghost.Ghost.enable_ghost_routing(self._sim_info)
        if trait.disable_aging:
            self._sim_info.update_age_callbacks()
        if trait.is_robot_trait and not from_load:
            self._sim_info.set_days_alive_to_zero()
        sim = self._sim_info.get_sim_instance()
        provided_affordances = []
        for provided_affordance in trait.target_super_affordances:
            provided_affordance_data = ProvidedAffordanceData(
                provided_affordance.affordance,
                provided_affordance.object_filter,
                provided_affordance.allow_self)
            provided_affordances.append(provided_affordance_data)
        self.add_to_affordance_caches(trait.super_affordances,
                                      provided_affordances)
        self.add_to_actor_mixer_cache(trait.actor_mixers)
        self.add_to_provided_mixer_cache(trait.provided_mixers)
        apply_super_affordance_commodity_flags(sim, trait,
                                               trait.super_affordances)
        self._hiding_relationships |= trait.hide_relationships
        if sim is not None:
            teleport_style_interaction = trait.get_teleport_style_interaction_to_inject(
            )
            if teleport_style_interaction is not None:
                sim.add_teleport_style_interaction_to_inject(
                    teleport_style_interaction)
        if not from_load:
            if trait.is_personality_trait:
                if self._sim_info.household is not None:
                    for household_sim in self._sim_info.household:
                        if household_sim is self._sim_info:
                            continue
                        household_sim.relationship_tracker.add_known_trait(
                            trait, self._sim_info.sim_id)
                else:
                    logger.error(
                        "Attempting to add a trait to a Sim that doesn't have a household. This shouldn't happen. Sim={}, trait={}",
                        self._sim_info, trait)
            self._sim_info.resend_trait_ids()
            if trait.disable_aging is not None:
                self._sim_info.resend_age_progress_data()
            if sim is not None:
                with telemetry_helper.begin_hook(writer,
                                                 TELEMETRY_HOOK_ADD_TRAIT,
                                                 sim=sim) as hook:
                    hook.write_int(TELEMETRY_FIELD_TRAIT_ID, trait.guid64)
            if trait.always_send_test_event_on_add or sim is not None:
                services.get_event_manager().process_event(
                    test_events.TestEvent.TraitAddEvent,
                    sim_info=self._sim_info)
            if trait.loot_on_trait_add is not None:
                resolver = SingleSimResolver(self._sim_info)
                for loot_action in trait.loot_on_trait_add:
                    loot_action.apply_to_resolver(resolver)
        return True

    def _remove_trait(self, trait):
        if not self.has_trait(trait):
            return False
        initial_commodities_modified = trait.initial_commodities or trait.initial_commodities_blacklist
        if initial_commodities_modified:
            previous_initial_commodities = self._sim_info.get_initial_commodities(
            )
        self._equipped_traits.remove(trait)
        if initial_commodities_modified:
            self._update_initial_commodities(trait,
                                             previous_initial_commodities)
        self._remove_buffs(trait)
        self._remove_vfx_mask(trait)
        self._remove_day_night_tracking(trait)
        self._remove_build_buy_purchase_tracking(trait)
        self.update_trait_effects()
        self.update_affordance_caches()
        if trait.disable_aging:
            self._sim_info.update_age_callbacks()
        self._sim_info.resend_trait_ids()
        if trait.disable_aging is not None:
            self._sim_info.resend_age_progress_data()
        if not any(t.is_ghost_trait for t in self._equipped_traits):
            sims.ghost.Ghost.remove_ghost_from_sim(self._sim_info)
        sim = self._sim_info.get_sim_instance()
        if sim is not None:
            with telemetry_helper.begin_hook(writer,
                                             TELEMETRY_HOOK_REMOVE_TRAIT,
                                             sim=sim) as hook:
                hook.write_int(TELEMETRY_FIELD_TRAIT_ID, trait.guid64)
            services.get_event_manager().process_event(
                test_events.TestEvent.TraitRemoveEvent,
                sim_info=self._sim_info)
            teleport_style_interaction = trait.get_teleport_style_interaction_to_inject(
            )
            if teleport_style_interaction is not None:
                sim.try_remove_teleport_style_interaction_to_inject(
                    teleport_style_interaction)
        remove_super_affordance_commodity_flags(sim, trait)
        self._hiding_relationships = any(trait.hide_relationships
                                         for trait in self)
        return True

    def get_traits_of_type(self, trait_type):
        return [t for t in self._equipped_traits if t.trait_type == trait_type]

    def remove_traits_of_type(self, trait_type):
        for trait in list(self._equipped_traits):
            if trait.trait_type == trait_type:
                self._remove_trait(trait)

    def clear_traits(self):
        for trait in list(self._equipped_traits):
            self._remove_trait(trait)

    def has_trait(self, trait):
        return trait in self._equipped_traits

    def has_any_trait(self, traits):
        return any(t in traits for t in self._equipped_traits)

    def is_conflicting(self, trait):
        return any(t.is_conflicting(trait) for t in self._equipped_traits)

    @staticmethod
    def _get_inherited_traits_internal(traits_a, traits_b, trait_entry):
        if trait_entry.parent_a_whitelist and not all(
                t in traits_a for t in trait_entry.parent_a_whitelist):
            return False
        if any(t in traits_a for t in trait_entry.parent_a_blacklist):
            return False
        if trait_entry.parent_b_whitelist and not all(
                t in traits_b for t in trait_entry.parent_b_whitelist):
            return False
        elif any(t in traits_b for t in trait_entry.parent_b_blacklist):
            return False
        return True

    def get_inherited_traits(self, other_sim):
        traits_a = list(self)
        traits_b = list(other_sim.trait_tracker)
        inherited_entries = []
        for trait_entry in TraitTracker.TRAIT_INHERITANCE:
            if not self._get_inherited_traits_internal(traits_a, traits_b,
                                                       trait_entry):
                if self._get_inherited_traits_internal(traits_b, traits_a,
                                                       trait_entry):
                    inherited_entries.append(
                        tuple((outcome.weight, outcome.trait)
                              for outcome in trait_entry.outcomes))
            inherited_entries.append(
                tuple((outcome.weight, outcome.trait)
                      for outcome in trait_entry.outcomes))
        return inherited_entries

    def get_leave_lot_now_interactions(self, must_run=False):
        interactions = set()
        for trait in self:
            if trait.npc_leave_lot_interactions:
                if must_run:
                    interactions.update(trait.npc_leave_lot_interactions.
                                        leave_lot_now_must_run_interactions)
                else:
                    interactions.update(trait.npc_leave_lot_interactions.
                                        leave_lot_now_interactions)
        return interactions

    @property
    def personality_traits(self):
        return tuple(trait for trait in self if trait.is_personality_trait)

    @property
    def gender_option_traits(self):
        return tuple(trait for trait in self if trait.is_gender_option_trait)

    @property
    def aspiration_traits(self):
        return tuple(trait for trait in self if trait.is_aspiration_trait)

    @property
    def trait_ids(self):
        return [t.guid64 for t in self._equipped_traits]

    @property
    def equipped_traits(self):
        return self._equipped_traits

    def get_default_trait_asm_params(self, actor_name):
        asm_param_dict = {}
        for trait_asm_param in Trait.default_trait_params:
            asm_param_dict[(trait_asm_param, actor_name)] = False
        return asm_param_dict

    @property
    def equip_slot_number(self):
        age = self._sim_info.age
        slot_number = self._unlocked_equip_slot
        slot_number += self._sim_info.get_aging_data(
        ).get_personality_trait_count(age)
        return slot_number

    @property
    def empty_slot_number(self):
        equipped_personality_traits = sum(1 for trait in self
                                          if trait.is_personality_trait)
        empty_slot_number = self.equip_slot_number - equipped_personality_traits
        return max(empty_slot_number, 0)

    def _add_buffs(self, trait):
        if trait.guid64 in self._buff_handles:
            return
        buff_handles = []
        for buff in trait.buffs:
            buff_handle = self._sim_info.add_buff(
                buff.buff_type,
                buff_reason=buff.buff_reason,
                remove_on_zone_unload=trait.buffs_add_on_spawn_only)
            if buff_handle is not None:
                buff_handles.append(buff_handle)
        if buff_handles:
            self._buff_handles[trait.guid64] = buff_handles

    def _remove_buffs(self, trait):
        if trait.guid64 in self._buff_handles:
            for buff_handle in self._buff_handles[trait.guid64]:
                self._sim_info.remove_buff(buff_handle)
            del self._buff_handles[trait.guid64]

    def _add_vfx_mask(self, trait, send_op=False):
        if trait.vfx_mask is None:
            return
        for mask in trait.vfx_mask:
            self.trait_vfx_mask |= mask
        if send_op and self._sim_info is services.active_sim_info():
            generate_mask_message(self.trait_vfx_mask, self._sim_info)

    def _remove_vfx_mask(self, trait):
        if trait.vfx_mask is None:
            return
        for mask in trait.vfx_mask:
            self.trait_vfx_mask ^= mask
        if self._sim_info is services.active_sim_info():
            generate_mask_message(self.trait_vfx_mask, self._sim_info)

    def update_trait_effects(self):
        if self._load_in_progress:
            return
        self._update_voice_effect()
        self._update_plumbbob_override()

    def _update_voice_effect(self):
        try:
            voice_effect_request = max(
                (trait.voice_effect
                 for trait in self if trait.voice_effect is not None),
                key=operator.attrgetter('priority'))
            self._sim_info.voice_effect = voice_effect_request.voice_effect
        except ValueError:
            self._sim_info.voice_effect = None

    def _update_plumbbob_override(self):
        try:
            plumbbob_override_request = max(
                (trait.plumbbob_override
                 for trait in self if trait.plumbbob_override is not None),
                key=operator.attrgetter('priority'))
            self._sim_info.plumbbob_override = (
                plumbbob_override_request.active_sim_plumbbob,
                plumbbob_override_request.active_sim_club_leader_plumbbob)
        except ValueError:
            self._sim_info.plumbbob_override = None

    def _add_default_gender_option_traits(self):
        gender_option_traits = self.DEFAULT_GENDER_OPTION_TRAITS.get(
            self._sim_info.gender)
        for gender_option_trait in gender_option_traits:
            if not self.has_trait(gender_option_trait):
                self._add_trait(gender_option_trait)

    def on_sim_startup(self):
        sim = self._sim_info.get_sim_instance()
        for trait in tuple(self):
            if trait in self:
                if trait.buffs_add_on_spawn_only:
                    self._add_buffs(trait)
                apply_super_affordance_commodity_flags(sim, trait,
                                                       trait.super_affordances)
                teleport_style_interaction = trait.get_teleport_style_interaction_to_inject(
                )
                if teleport_style_interaction is not None:
                    sim.add_teleport_style_interaction_to_inject(
                        teleport_style_interaction)
                    logger.error('Trait:{} was removed during startup', trait)
            else:
                logger.error('Trait:{} was removed during startup', trait)
        if any(trait.is_ghost_trait for trait in self):
            sims.ghost.Ghost.enable_ghost_routing(self._sim_info)

    def on_zone_unload(self):
        if game_services.service_manager.is_traveling:
            for trait in tuple(self):
                if trait in self:
                    if not trait.buffs_add_on_spawn_only:
                        self._remove_buffs(trait)
                    if not trait.persistable:
                        self._remove_trait(trait)

    def on_zone_load(self):
        if game_services.service_manager.is_traveling:
            for trait in tuple(self):
                if trait in self:
                    if not trait.buffs_add_on_spawn_only:
                        self._add_buffs(trait)

    def on_sim_removed(self):
        for trait in tuple(self):
            if trait.buffs_add_on_spawn_only:
                self._remove_buffs(trait)
            if not trait.persistable:
                self._remove_trait(trait)

    def save(self):
        data = protocols.PersistableTraitTracker()
        trait_ids = [
            trait.guid64 for trait in self._equipped_traits
            if trait.persistable
        ]
        data.trait_ids.extend(trait_ids)
        return data

    def load(self, data, skip_load):
        trait_manager = services.get_instance_manager(
            sims4.resources.Types.TRAIT)
        try:
            self._load_in_progress = True
            self._sim_info._update_age_trait(self._sim_info.age)
            for trait_instance_id in data.trait_ids:
                trait = trait_manager.get(trait_instance_id)
                if trait is not None:
                    if not self._has_valid_lod(trait):
                        continue
                    if skip_load and not trait.allow_from_gallery:
                        continue
                    self._sim_info.add_trait(trait, from_load=True)
            if not self.personality_traits and not self._sim_info.is_baby:
                possible_traits = [
                    trait for trait in trait_manager.types.values()
                    if trait.is_personality_trait if self.can_add_trait(trait)
                ]
                if possible_traits:
                    chosen_trait = random.choice(possible_traits)
                    self._add_trait(chosen_trait, from_load=True)
            if not any(trait.is_gender_option_trait for trait in self):
                self._add_default_gender_option_traits()
            add_quirks(self._sim_info)
            self._sim_info.on_all_traits_loaded()
        finally:
            self._load_in_progress = False

    def _has_any_trait_with_day_night_tracking(self):
        return any(trait for trait in self
                   if trait.day_night_tracking is not None)

    def _add_day_night_tracking(self, trait):
        sim = self._sim_info.get_sim_instance(
            allow_hidden_flags=ALL_HIDDEN_REASONS)
        if sim is None:
            return
        if trait.day_night_tracking is not None and not sim.is_on_location_changed_callback_registered(
                self._day_night_tracking_callback):
            sim.register_on_location_changed(self._day_night_tracking_callback)
        self.update_day_night_tracking_state(force_update=True)

    def _remove_day_night_tracking(self, trait):
        self._day_night_state = None
        sim = self._sim_info.get_sim_instance(
            allow_hidden_flags=ALL_HIDDEN_REASONS)
        if sim is None:
            return
        if trait.day_night_tracking is None or self._has_any_trait_with_day_night_tracking(
        ):
            return
        sim.unregister_on_location_changed(self._day_night_tracking_callback)

    def _day_night_tracking_callback(self, *_, **__):
        self.update_day_night_tracking_state()

    def update_day_night_tracking_state(self,
                                        force_update=False,
                                        full_reset=False):
        if not self._has_any_trait_with_day_night_tracking():
            return
        sim = self._sim_info.get_sim_instance(
            allow_hidden_flags=ALL_HIDDEN_REASONS)
        if sim is None:
            return
        if full_reset:
            self._clear_all_day_night_buffs()
        time_service = services.time_service()
        is_day = time_service.is_day_time()
        in_sunlight = time_service.is_in_sunlight(sim)
        new_state = self._day_night_state is None
        if new_state:
            self._day_night_state = DayNightTrackingState(is_day, in_sunlight)
        update_day_night = new_state or self._day_night_state.is_day != is_day
        update_sunlight = new_state or self._day_night_state.in_sunlight != in_sunlight
        if not force_update and not (not update_day_night
                                     and not update_sunlight):
            return
        self._day_night_state.is_day = is_day
        self._day_night_state.in_sunlight = in_sunlight
        for trait in self:
            if not trait.day_night_tracking:
                continue
            day_night_tracking = trait.day_night_tracking
            if update_day_night or force_update:
                self._add_remove_day_night_buffs(day_night_tracking.day_buffs,
                                                 add=is_day)
                self._add_remove_day_night_buffs(
                    day_night_tracking.night_buffs, add=not is_day)
            if not update_sunlight:
                if force_update:
                    self._add_remove_day_night_buffs(
                        day_night_tracking.sunlight_buffs, add=in_sunlight)
                    self._add_remove_day_night_buffs(
                        day_night_tracking.shade_buffs, add=not in_sunlight)
            self._add_remove_day_night_buffs(day_night_tracking.sunlight_buffs,
                                             add=in_sunlight)
            self._add_remove_day_night_buffs(day_night_tracking.shade_buffs,
                                             add=not in_sunlight)

    def update_day_night_buffs_on_buff_removal(self, buff_to_remove):
        if not self._has_any_trait_with_day_night_tracking():
            return
        for trait in self:
            if trait.day_night_tracking:
                if not trait.day_night_tracking.force_refresh_buffs:
                    continue
                force_refresh_buffs = trait.day_night_tracking.force_refresh_buffs
                if any(buff.buff_type is buff_to_remove.buff_type
                       for buff in force_refresh_buffs):
                    self.update_day_night_tracking_state(full_reset=True,
                                                         force_update=True)
                    return

    def _clear_all_day_night_buffs(self):
        for trait in self:
            if not trait.day_night_tracking:
                continue
            day_night_tracking = trait.day_night_tracking
            self._add_remove_day_night_buffs(day_night_tracking.day_buffs,
                                             add=False)
            self._add_remove_day_night_buffs(day_night_tracking.night_buffs,
                                             add=False)
            self._add_remove_day_night_buffs(day_night_tracking.sunlight_buffs,
                                             add=False)
            self._add_remove_day_night_buffs(day_night_tracking.shade_buffs,
                                             add=False)

    def _add_remove_day_night_buffs(self, buffs, add=True):
        for buff in buffs:
            if add:
                self._sim_info.add_buff(buff.buff_type,
                                        buff_reason=buff.buff_reason)
            else:
                self._sim_info.remove_buff_by_type(buff.buff_type)

    def _has_any_trait_with_build_buy_purchase_tracking(self):
        return any(trait for trait in self
                   if trait.build_buy_purchase_tracking is not None)

    def _add_build_buy_purchase_tracking(self, trait):
        if trait.build_buy_purchase_tracking is not None and not services.get_event_manager(
        ).is_registered_for_event(self, test_events.TestEvent.ObjectAdd):
            services.get_event_manager().register(
                self, (test_events.TestEvent.ObjectAdd, ))

    def _remove_build_buy_purchase_tracking(self, trait):
        if trait.build_buy_purchase_tracking is None or self._has_any_trait_with_build_buy_purchase_tracking(
        ):
            return
        services.get_event_manager().unregister(
            self, (test_events.TestEvent.ObjectAdd, ))

    def _handle_build_buy_purchase_event(self, trait, resolver):
        if not trait.build_buy_purchase_tracking:
            return
        for loot_action in trait.build_buy_purchase_tracking:
            loot_action.apply_to_resolver(resolver)

    def handle_event(self, sim_info, event_type, resolver):
        if event_type == test_events.TestEvent.ObjectAdd:
            for trait in self:
                self._handle_build_buy_purchase_event(trait, resolver)

    def on_sim_ready_to_simulate(self):
        for trait in self:
            self._add_day_night_tracking(trait)
            self._add_build_buy_purchase_tracking(trait)

    def get_provided_super_affordances(self):
        affordances = set()
        target_affordances = list()
        for trait in self._equipped_traits:
            affordances.update(trait.super_affordances)
            for provided_affordance in trait.target_super_affordances:
                provided_affordance_data = ProvidedAffordanceData(
                    provided_affordance.affordance,
                    provided_affordance.object_filter,
                    provided_affordance.allow_self)
                target_affordances.append(provided_affordance_data)
        return (affordances, target_affordances)

    def get_actor_and_provided_mixers_list(self):
        actor_mixers = [trait.actor_mixers for trait in self._equipped_traits]
        provided_mixers = [
            trait.provided_mixers for trait in self._equipped_traits
        ]
        return (actor_mixers, provided_mixers)

    def get_sim_info_from_provider(self):
        return self._sim_info

    @classproperty
    def _tracker_lod_threshold(cls):
        return SimInfoLODLevel.MINIMUM

    def on_lod_update(self, old_lod, new_lod):
        if new_lod == old_lod:
            return
        increase_lod = old_lod < new_lod
        for trait in tuple(self._equipped_traits):
            if self._has_valid_lod(trait):
                if increase_lod:
                    initial_commodities = trait.initial_commodities - trait.initial_commodities_blacklist
                    initial_commodities = initial_commodities - frozenset(
                        self._sim_info.get_blacklisted_statistics())
                    for commodity in initial_commodities:
                        commodity_inst = self._sim_info.commodity_tracker.get_statistic(
                            commodity, add=True)
                        if commodity_inst is not None:
                            commodity_inst.core = True
                    else:
                        self._sim_info.remove_trait(trait)
            else:
                self._sim_info.remove_trait(trait)

    def _has_valid_lod(self, trait):
        if self._sim_info.lod < trait.min_lod_value:
            return False
        return True

    @property
    def hide_relationships(self):
        return self._hiding_relationships
Exemple #17
0
class EventVisualization(HasTraits):
    background = (0, 0, 0)
    normal_barcolor = (153.0 / 255, 204.0 / 255, 255.0 / 255)
    textcolor = normal_barcolor

    scene = Instance(MlabSceneModel, ())

    def default_traits_view(self):
        view = View(
            Item('scene',
                 editor=SceneEditor(scene_class=MayaviScene),
                 height=600,
                 width=600,
                 show_label=False),
            HGroup(
                Item("current_time", label="Date"), Item(" "),
                Item("num_of_shown_days", label="Show"),
                Item("_home_button", show_label=False),
                Item("_selected_source_name", show_label=False),
                Item("_selected_event_name",
                     editor=CheckListEditor(name='_selected_events_list'),
                     show_label=False), Item("_back1", show_label=False),
                Item(
                    "Relative_Start_Day",
                    show_label=False,
                    editor=RangeEditor(mode="slider",
                                       low_name="_low_start_day_number",
                                       high_name="_high_start_day_number"),
                    tooltip=
                    "Shows total number of days in data set and the currently selected day",
                    springy=True,
                    full_size=True), Item("_forward1", show_label=False),
                Item("move_step", show_label=False),
                Item("play_button", label='Play')),
            title="Visualization of Events",
            resizable=True)
        view.resizable = True
        return view

    # Index to the currently selected source. None if no source is selected.
    selected_source = Trait(None, None, Int)
    _selected_source_name = Enum(None, [None])

    # index to a selected event. None if not selected.
    selected_event = None
    _selected_event_name = List
    _selected_events_list = List(['None', 'None'])

    # Current (most recent) time, can also be set usinh start day, but then relative to the total number of days.
    current_time = Date
    start_day = Trait(None, int, tuple, String, Date)
    _low_start_day_number = Int(0)  # minimum start day = 0
    _high_start_day_number = Int(
        100)  # maximum start day = total number of days

    # Configures the shown number of days in the visualization.
    num_of_shown_days = Trait(
        "30 days",
        Enum([
            "7 days", "14 days", "30 days", "60 days", "90 days", "120 days",
            "180 days", "365 days"
        ]))

    ## Buttons for controlling the visualization

    _home_button = Button("Unselect>>", width_padding=0, height_padding=0)
    _back1 = Button("<", width_padding=0, height_padding=0)
    _forward1 = Button(">", width_padding=0, height_padding=0)

    # Play button sets the visualization on stepping in the last selected direction, back or forth.
    play_button = Bool

    # Configure the number of days the visualization can step.
    move_step = Trait("1 day", Enum(["1 day", "2 days", "3 days", "7 days"]))

    # Home button, unselects any selection

    def __home_button_changed(self):
        self.selected_source = None

    _last_clicked_direction = None

    def move_backward(self):
        '''
        Moves the shown area backward 1 step.
        :return:
        '''
        if self.Relative_Start_Day >= (self._low_start_day_number +
                                       int(self.move_step[0])):
            self.Relative_Start_Day -= int(self.move_step[0])

    def move_forward(self):
        '''
        Moves the shown area forward 1 step.
        :return:
        '''
        if self.Relative_Start_Day <= (self._high_start_day_number -
                                       int(self.move_step[0])):
            self.Relative_Start_Day += int(self.move_step[0])

    def __back1_changed(self):
        '''
        Triggered when back button is pressed
        :return:
        '''
        self.move_backward()
        self._last_clicked_direction = self.move_backward

    def __forward1_changed(self):
        '''
        Triggered when forward button is pressed
        :return:
        '''
        self.move_forward()
        self._last_clicked_direction = self.move_forward

    _play_thread = False

    def _play_button_changed(self, play_pressed):
        '''
        Triggered when play button is selected
        :param play_pressed:
        :return:
        '''
        if play_pressed:
            if not self._play_thread:
                self._play_thread = True
                GUI.invoke_after(1, self._play_func)
        else:
            self._play_thread = False

    def _play_func(self):
        '''
        Called while play button is selected
        :return:
        '''
        if self._play_thread:
            if self._last_clicked_direction is not None:
                self._last_clicked_direction()
            else:
                self.move_forward()

            GUI.invoke_after(1000, self._play_func)

    @on_trait_change("Relative_Start_Day")
    def _relative_start_day_changed(self, new_value):
        self.start_day = new_value

    def _start_day_changed(self, new_time):
        if isinstance(new_time, int):
            self.current_time = datetools.to_datetime(self._data_times.min() +
                                                      datetools.Day(new_time))
        elif isinstance(new_time, str):
            self.current_time = datetools.to_datetime(new_time)
        elif isinstance(new_time, datetime.date):
            self.current_time = datetools.to_datetime(new_time)
        else:
            print "Unsupported start day ", new_time
            return

    def _selected_source_changed(self, old_source, new_source):
        #print "Source changed", old_source, new_source

        if old_source is not new_source:

            if new_source is not None:
                self._selected_source_name = self._get_source_name(new_source)
                self._update_selected_event(None)
                self._update_selected_event(
                    self._vis_model.get_num_of_selected_events() - 1)
            else:
                self._selected_source_name = None
                self._update_selected_event(None)

    def __selected_source_name_changed(self, old_source, new_source):
        if new_source in self.source_names:
            source_index = self.source_names.index(new_source)
        else:
            source_index = -1
        if self.selected_source != source_index:
            if source_index == -1:
                self.selected_source = None
            else:
                self.selected_source = source_index

    def _current_time_changed(self, old_time, new_time):
        num_of_days = int(
            (datetools.to_datetime(new_time) - self._data_times.min()).days)
        if self.Relative_Start_Day != num_of_days:
            self.Relative_Start_Day = num_of_days
        elif old_time != new_time:
            self.current_time = datetools.to_datetime(self.current_time)
            self.update()

    _old_event_name = -1

    def _update_selected_event(self, event_index):

        if event_index > -1:
            event = self._vis_model.get_selected_event(event_index)
        else:
            event = None

        # if self._old_event_name == event or event is not None and self._old_event_name == event.name:
        #     return # Do nothing

        self._old_event_name = None if event is None else event.name

        if event_index > -1:
            selected_event = self._vis_model.expand_events(
                event.parent if self.selected_event > -1
                and event_index >= self.selected_event else self._vis_model.
                get_selected_event(event_index))
        else:
            selected_event = self._vis_model.expand_events(None)

        self.selected_event = selected_event

        if event is not None:
            sevents = self._vis_model.get_selected_events_name()

            names = list(
                sorted([
                    pyisc._get_string_value(sevents, i) for i in range(
                        self._vis_model.get_num_of_selected_events())
                ]))

            if event.name in names:
                self._selected_events_list = names
                self._selected_event_name = [event.name]
        else:
            self._selected_events_list = ['None']
            self._selected_event_name = ['None']

        self.update()

    def __selected_event_name_changed(self, oldvalue, newvalue):
        if self._old_event_name is None or len(
                self._selected_event_name
        ) == 1 and self._old_event_name != self._selected_event_name[0]:
            if len(oldvalue) != len(newvalue) or len(
                    newvalue) == 1 and oldvalue[0] != newvalue[0]:
                if len(self._selected_event_name
                       ) == 1 and self._selected_event_name[0] != 'None':
                    event_index = self._vis_model.get_event_index(
                        self._selected_event_name[0])
                    self._update_selected_event(event_index)
                else:
                    self._update_selected_event(None)

    def _num_of_shown_days_changed(self):
        self.used_cache_size = self._num_of_shown_days_to_int(
        ) * self._num_of_sources
        self.update()

    # Used for caching anomaly calculations
    _cache = dict()
    # Used for scaling visualizatuion in the z direction
    _scale_z = Trait(0.1, Range(0.0, 1.0))
    # Used for setting a good default view in 3D
    _last_view = None

    def __init__(self,
                 visualisation_model,
                 decision_threshold,
                 start_day=3,
                 num_of_shown_days="30 days",
                 precompute_cache=False):
        '''

        :param visualisation_model: an instance of EventDataModel
        :param decision_threshold: a float larger or equal to 0.0 that is used for deciding when an anomaly score is significantly anomalous
        :param start_day: an integer >= or an instance of datetime.date or an string, like "2014-10-11" or a tuple, like (2014, 10, 11)
        :param num_of_shown_days: an integer > 1 that specifies the number of days back in time from start_day that will be shown.
        :param precompute_cache: boolean that indates whether all anomaly scores should be computed at once or when asked for.
        :return:
        '''
        assert isinstance(visualisation_model, EventDataModel)
        assert isinstance(
            start_day, int) or isinstance(start_day, str) or isinstance(
                start_day, datetime.date) or (isinstance(start_day, tuple)
                                              and len(start_day) == 3)
        HasTraits.__init__(self)

        self.used_cache_size = 0  # must be initialized
        self._data = visualisation_model._event_data_object

        self.num_of_shown_days = num_of_shown_days  # Updates self.used_cache_size

        self._vis_model = visualisation_model
        self._anomaly_detector = visualisation_model._anomaly_detector
        self.anomaly_detection_threshold = decision_threshold

        dates = visualisation_model._event_data_object.dates_
        self._data_times = array([datetools.to_datetime(d) for d in dates])

        self.source_names = list(
            unique(visualisation_model._event_data_object.sources_))
        self._data_sources = array([
            self.source_names.index(source)
            for source in visualisation_model._event_data_object.sources_
        ])
        self._num_of_sources = len(unique(
            self.source_names))  # number of sources

        self.barcharts = []
        self.barchart_actors = []
        self.time_text3ds = []
        self.source_text3ds = []
        self.xy_positions = []

        self._high_start_day_number = int(
            (self._data_times.max() - self._data_times.min()).days)

        self.scene.anti_aliasing_frames = 8

        # add traits dynamically
        self.add_trait("Relative_Start_Day",
                       Range(0, self._high_start_day_number))

        self.add_trait("_selected_source_name",
                       Enum(None, [None] + self.source_names))

        self.configure_traits()

        self.scene.background = self.background

        # add the mouse pick handler
        self.picker = self.scene.mayavi_scene.on_mouse_pick(
            self.vis_picker, 'cell')

        self.picker.tolerance = 0.01

        #cmap = matplotlib.cm.get_cmap('Reds')
        #self.severity_color = [cmap(x)[:-1] for x in linspace(0.75, 0.95, self._vis_model.num_of_severity_levels_)]

        if self._vis_model.num_of_severity_levels_ > 1:
            self.severity_color = self.severity_color = [
                (1, x / 100.0, x / 100.0)
                for x in range(70, 30, -40 /
                               self._vis_model.num_of_severity_levels_)
            ]
        else:
            self.severity_color = [(255.0 / 255, 51 / 255.0, 51 / 255.0)]

        # This used for a fix to manage a bug in Mayavi library, an invisible default object
        self._obj = self.scene.mlab.points3d(0, 0, 0, opacity=0.0)

        # Cache all anomaly calculations for all data values
        if precompute_cache:
            self.used_cache_size = len(self._data)
            for data_index in xrange(len(self._data)):
                self._populate_cache(data_index)

        self.start_day = start_day

        self.update()

    def _create_barcharts(self, severities, x, y, z):
        '''
        Creates and shows the 3D bars
        :param severities:
        :param x:
        :param y:
        :param z:
        :return:
        '''
        #self.scene.disable_render = True

        x = array(x)
        y = array(y)
        z = array(z)
        severities = array(severities)

        for s in set(severities):
            s_index = (severities == s)
            color = self.normal_barcolor if s == -1 else self.background if s == -2 else self.severity_color[
                s]
            x0 = x[s_index]
            y0 = y[s_index]
            z0 = z[s_index]

            barchart = self.scene.mlab.barchart(x0,
                                                y0,
                                                z0,
                                                color=color,
                                                auto_scale=False,
                                                reset_zoom=False)
            self.barcharts.append(barchart)
            self.barchart_actors.append(barchart.actor.actors[0])
            self.xy_positions.append((x0, y0, z0))

        for actor in self.barchart_actors:
            actor.scale = array([1.0, 1.0, self._scale_z])

            #self.scene.disable_render = False

    def clear_figure(self):
        '''
        Removes the objects from the scene.
        :return:
        '''
        self.scene.remove_actors(self.barchart_actors)

        # A bug fix, when there are no objects left in the scene it stops working unless you set a default current_object
        # It is an invisibale point outside the 3D bar plot region.
        self.scene.mlab.get_engine().current_object = self._obj

        self.barchart_actors = []
        self.xy_positions = []

    def _trim_cache(self, data_index, used_cache_size):
        '''
        Keeps the cache to the defined size.
        :param data_index:
        :param used_cache_size:
        :return:
        '''
        if len(self._cache) > used_cache_size:
            max_index = max(self._cache.keys())
            min_index = min(self._cache.keys())
            if data_index > max_index:
                del self._cache[min_index]
            elif data_index < min_index:
                del self._cache[max_index]
            else:  # Remove the one farest away
                median_index = median(self._cache.keys())
                triple_indexes = array([min_index, median_index, max_index])
                diffs = abs(data_index - triple_indexes)
                diff_max_index = diffs.argmax()

                del self._cache[triple_indexes[diff_max_index]]

    def update(self):
        '''
        Plots the 3D bars and axis.
        :return:
        '''
        is_first_update = False

        if self._last_view is None:
            self._last_view = (38, 8, 205, array([8, 17.5, 49.25]))
            self.scene.mlab.view(*self._last_view)
            is_first_update = True
        else:
            self._last_view = self.scene.mlab.view()

        self.scene.disable_render = True
        self.clear_figure()
        #print "Day: %s" % time.ctime(self.current_time)

        max_z = 0

        time_index = (
            (self._data_times <= datetools.to_datetime(self.current_time)) &
            (self._data_times >= (datetools.to_datetime(self.current_time) -
                                  self._num_of_shown_days_to_timedelta())))

        if self.selected_source is None:  # Plot all sources
            x = []
            y = []
            z = []
            severities = []

            for source in range(self._num_of_sources):
                for data_index in array(range(len(self._data)))[time_index][
                        self._data_sources[time_index] == source]:
                    if self.used_cache_size > 0 and self._cache.has_key(
                            data_index):
                        devsptr, sevs, expectptr, min2, max2, count = self._cache[
                            data_index]
                    else:
                        devs, sevs, expect, min2, max2 = self._vis_model.calc_one(
                            data_index)

                        devsptr = pyisc._to_cpp_array(devs)
                        expectptr = pyisc._to_cpp_array(expect)

                        count = None

                    if count is None:
                        self._trim_cache(data_index, self.used_cache_size)
                        vec = self._data._get_intfloat(data_index)
                        count = sum([
                            pyisc._get_intfloat_value(
                                vec,
                                self._vis_model.get_event_hierarchy().
                                get_index_value(l)) for l in range(
                                    self._vis_model.num_of_severity_levels_)
                            if self._vis_model.get_event_hierarchy().
                            get_index_value(l) != -1
                        ])
                        self._cache[data_index] = (devsptr, sevs, expectptr,
                                                   min2, max2, count)

                    ztime = self._num_of_shown_days_to_int() - (
                        self.current_time - self._data_times[data_index]).days

                    x.append(source)
                    y.append(ztime)

                    z.append(count)

                    sev_max = argmax(sevs)
                    sev = (-1
                           if sevs[sev_max] < self.anomaly_detection_threshold
                           else sev_max)

                    severities.append(sev)
            self._create_barcharts(severities, x, y, z)

            max_z = max([max_z] + z)
        else:  # Plot for selected source
            source_index = self._data_sources[
                time_index] == self.selected_source

            data_indexes = array(range(len(
                self._data)))[time_index][source_index]
            x = []
            y = []
            z = []
            severities = []

            # Plot selected events
            for data_index in data_indexes:
                if self.used_cache_size > 0 and self._cache.has_key(
                        data_index):
                    devsptr, sevs, expectptr, min2, max2, _ = self._cache[
                        data_index]
                else:
                    devs, sevs, expect, min2, max2 = self._vis_model.calc_one(
                        data_index)
                    devsptr = pyisc._to_cpp_array(devs)
                    expectptr = pyisc._to_cpp_array(expect)

                    self._trim_cache(data_index, self.used_cache_size)

                    self._cache[data_index] = (devsptr, sevs, expectptr, min2,
                                               max2, None)

                ztime = self._num_of_shown_days_to_int() - (
                    datetools.to_datetime(self.current_time) -
                    self._data_times[data_index]).days

                if self._vis_model.get_num_of_selected_events() > 0:
                    for element in range(
                            self._vis_model.get_num_of_selected_events()):
                        x.append(element)
                        y.append(ztime)

                        (dev, sev, count, mexp,
                         maxind) = self._vis_model.summarize_event_children(
                             self._vis_model.get_selected_event(element),
                             devsptr, expectptr,
                             self._data._get_intfloat(data_index),
                             1 if element >= self.selected_event else 0)
                        z.append(count)

                        if dev < self.anomaly_detection_threshold:
                            severities.append(-1)
                        else:
                            severities.append(sev)

            self._create_barcharts(severities, x, y, z)
            max_z = max([max_z] + z)

        self.scene.disable_render = True

        datetime.date

        curr_t = self.current_time
        time_strs = [
            str(t.date()) for t in date_range(
                curr_t - self._num_of_shown_days_to_timedelta(), curr_t)
        ]
        time_max_len = min([len(t) for t in time_strs])

        max_x = (self._num_of_sources if self.selected_source is None else
                 self._vis_model.get_num_of_selected_events())
        max_y = self._num_of_shown_days_to_int()

        if len(self.time_text3ds) != len(time_strs):
            self.scene.remove_actors(
                [t.actor.actors[0] for t in self.time_text3ds])
            self.time_text3ds = []

            for slot in range(len(time_strs)):
                name = time_strs[slot]
                pos = (max_x + time_max_len / 2 - 1, slot, 0)
                self.time_text3ds.append(
                    self.scene.mlab.text3d(*pos,
                                           text=name,
                                           scale=0.5,
                                           color=self.textcolor,
                                           orient_to_camera=False,
                                           orientation=(180, 180, 0)))
        else:
            for slot in range(len(time_strs)):
                name = time_strs[slot]
                pos = (max_x + time_max_len / 2 - 1, slot, 0)

                self.time_text3ds[slot].position = pos
                self.time_text3ds[slot].text = name

        if self.selected_source is None:
            source_strs = [
                self._get_source_name(source)
                for source in range(self._num_of_sources)
            ]
            num_of_sources = self._num_of_sources
        else:
            source_strs = [
                self._get_event_name(element) for element in range(
                    self._vis_model.get_num_of_selected_events())
            ]
            num_of_sources = self._vis_model.get_num_of_selected_events()

        if len(self.source_text3ds
               ) != num_of_sources or self.selected_source is None:
            self.scene.remove_actors(
                [t.actor.actors[0] for t in self.source_text3ds])
            self.source_text3ds = []
            for source in range(num_of_sources):
                name = source_strs[source]
                if self.selected_source is None:
                    self.source_text3ds.append(
                        self.scene.mlab.text3d(source,
                                               max_y + 0.5,
                                               0,
                                               name,
                                               scale=0.6,
                                               color=self.textcolor,
                                               orient_to_camera=False,
                                               orientation=(0, 0, 90)))
                else:
                    self.source_text3ds.append(
                        self.scene.mlab.text3d(
                            source,
                            max_y + 0.5,
                            0,
                            name,
                            color=self.textcolor
                            if source < self.selected_event else
                            (192.0 / 255, 192.0 / 255,
                             192.0 / 255) if source > self.selected_event else
                            (1.0, 1.0, 1.0),
                            scale=0.5,
                            orient_to_camera=False,
                            orientation=(0, 0, 90)))
        else:
            for source in range(num_of_sources):
                name = source_strs[source]
                self.source_text3ds[source].text = name
                self.source_text3ds[source].position = (source, max_y + 0.5, 0)

        if is_first_update:
            self.scene.reset_zoom()

        self.scene.disable_render = False

        return

    last_picker = None

    def vis_picker(self, picker):
        '''
        Called when the user clicks in the scene in order to find the object that was selected.
        :param picker:
        :return:
        '''
        self.last_picker = picker
        _source = None

        if picker.actor is not None:

            if picker.actor in self.barchart_actors:
                actor_index = self.barchart_actors.index(picker.actor)
                _sources, _time_slots, _ = self.xy_positions[actor_index]

                _source = _sources[picker.point_id / 24]
                _time_slot = _time_slots[picker.point_id / 24]
            else:
                actors = [t.actor.actors[0] for t in self.source_text3ds]

                if picker.actor in actors:
                    actor_index = actors.index(picker.actor)
                    _source = actor_index

            if _source is not None:
                if self.selected_source is None:
                    if _source >= 0 and _source < self._num_of_sources:
                        self.selected_source = _source
                elif _source >= 0 and _source < self._vis_model.get_num_of_selected_events(
                ):
                    self._update_selected_event(_source)

    def _num_of_shown_days_to_timedelta(self):
        return datetools.to_offset(
            str(self.num_of_shown_days).split(' ')[0] + "d")

    def _num_of_shown_days_to_int(self):
        return int(str(self.num_of_shown_days).split(' ')[0])

    def _get_source_name(self, source):
        return self.source_names[source]

    def _get_event_name(self, element):
        return self._vis_model.get_selected_event(element).name

    def _populate_cache(self, data_index):
        devs, sevs, expect, min2, max2 = self._vis_model.calc_one(data_index)

        devsptr = pyisc._to_cpp_array(devs)
        expectptr = pyisc._to_cpp_array(expect)

        vec = self._data._get_intfloat(data_index)
        count = sum([
            pyisc._get_intfloat_value(
                vec,
                self._vis_model.get_event_hierarchy().get_index_value(l))
            for l in xrange(self._vis_model.num_of_severity_levels_)
            if self._vis_model.get_event_hierarchy().get_index_value(l) != -1
        ])

        self._cache[data_index] = (devsptr, sevs, expectptr, min2, max2, count)
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