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) }
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
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
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)
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)
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
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)
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
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