class LaundryTuning: GENERATE_CLOTHING_PILE = TunableTuple(description='\n The tunable to generate clothing pile on the lot. This will be called\n when we find laundry hero objects on the lot and there is no hamper\n available.\n ', loot_to_apply=TunableReference(description='\n Loot to apply for generating clothing pile.\n ', manager=services.get_instance_manager(sims4.resources.Types.ACTION), class_restrictions=('LootActions',), pack_safe=True), naked_outfit_category=TunableSet(description="\n Set of outfits categories which is considered naked.\n When Sim switches FROM these outfits, it won't generate the pile.\n When Sim switches TO these outfits, it won't apply laundry reward\n or punishment.\n ", tunable=TunableEnumEntry(tunable_type=OutfitCategory, default=OutfitCategory.EVERYDAY, invalid_enums=(OutfitCategory.CURRENT_OUTFIT,))), no_pile_outfit_category=TunableSet(description="\n Set of outfits categories which will never generate the pile.\n When Sim switches FROM or TO these outfits, it won't generate the\n pile.\n \n Laundry reward or punishment will still be applied to the Sim when \n switching FROM or TO these outfits.\n ", tunable=TunableEnumEntry(tunable_type=OutfitCategory, default=OutfitCategory.EVERYDAY, invalid_enums=(OutfitCategory.CURRENT_OUTFIT,))), no_pile_interaction_tag=TunableEnumWithFilter(description='\n If interaction does spin clothing change and has this tag, it will\n generate no clothing pile.\n ', tunable_type=Tag, default=Tag.INVALID, filter_prefixes=('interaction',))) HAMPER_OBJECT_TAGS = TunableTags(description='\n Tags that considered hamper objects.\n ', filter_prefixes=('func',)) LAUNDRY_HERO_OBJECT_TAGS = TunableTags(description='\n Tags of laundry hero objects. Placing any of these objects on the lot\n will cause the service to generate clothing pile for each Sims on the\n household after spin clothing change.\n ', filter_prefixes=('func',)) NOT_DOING_LAUNDRY_PUNISHMENT = TunableTuple(description='\n If no Sim in the household unload completed laundry in specific\n amount of time, the negative loot will be applied to Sim household \n on spin clothing change to engage them doing laundry.\n ', timeout=TunableSimMinute(description="\n The amount of time in Sim minutes, since the last time they're \n finishing laundry, before applying the loot.\n ", default=2880, minimum=1), loot_to_apply=TunableReference(description='\n Loot defined here will be applied to the Sim in the household\n on spin clothing change if they are not doing laundry for \n a while.\n ', manager=services.get_instance_manager(sims4.resources.Types.ACTION), class_restrictions=('LootActions',), pack_safe=True)) PUT_AWAY_FINISHED_LAUNDRY = TunableTuple(description='\n The tunable to update laundry service on Put Away finished laundry\n interaction.\n ', interaction_tag=TunableEnumWithFilter(description='\n Tag that represent the put away finished laundry interaction which \n will update Laundry Service data.\n ', tunable_type=Tag, default=Tag.INVALID, filter_prefixes=('interaction',)), laundry_condition_states=TunableTuple(description='\n This is the state type of completed laundry object condition \n which will aggregate the data to the laundry service.\n ', condition_states=TunableList(description='\n A list of state types to be stored on laundry service.\n ', tunable=TunableStateTypeReference(pack_safe=True), unique_entries=True), excluded_states=TunableList(description='\n A list of state values of Condition States which will not \n be added to the laundry service.\n ', tunable=TunableStateValueReference(pack_safe=True), unique_entries=True)), laundry_condition_timeout=TunableSimMinute(description='\n The amount of time in Sim minutes that the individual laundry\n finished conditions will be kept in the laundry conditions \n aggregate data.\n ', default=1440, minimum=0), conditions_and_rewards_map=TunableMapping(description='\n Mapping of laundry conditions and loot rewards.\n ', key_type=TunableReference(manager=services.get_instance_manager(sims4.resources.Types.OBJECT_STATE), pack_safe=True), value_type=TunableReference(manager=services.get_instance_manager(sims4.resources.Types.ACTION), class_restrictions=('LootActions',), pack_safe=True))) PUT_CLOTHING_PILE_ON_HAMPER = TunableTuple(description='\n The Tunable to directly put generated clothing pile in the hamper.\n ', chance=TunablePercent(description='\n The chance that a clothing pile will be put directly in the hamper. \n Tune the value in case putting clothing pile in hamper every \n spin-outfit-change feeling excessive.\n ', default=100), clothing_pile=TunableTuple(description="\n Clothing pile object that will be created and put into the hamper \n automatically. \n \n You won't see the object on the lot since it will go directly to \n the hamper. We create it because we need to transfer all of the \n commodities data and average the values into the hamper precisely.\n ", definition=TunablePackSafeReference(description='\n Reference to clothing pile object definition.\n ', manager=services.definition_manager()), initial_states=TunableList(description='\n A list of states to apply to the clothing pile as soon as it \n is created.\n ', tunable=TunableTuple(description='\n The state to apply and optional to decide if the state \n should be applied.\n ', state=TunableStateValueReference(pack_safe=True), tests=TunableTestSet()))), full_hamper_state=TunableStateValueReference(description='\n The state of full hamper which make the hamper is unavailable to \n add new clothing pile in it.\n ', pack_safe=True), loots_to_apply=TunableList(description='\n Loots to apply to the hamper when clothing pile is being put.\n ', tunable=TunableReference(manager=services.get_instance_manager(sims4.resources.Types.ACTION), class_restrictions=('LootActions',), pack_safe=True)), tests=TunableTestSet(description='\n The test to run on the Sim that must pass in order for putting\n clothing pile automatically to the hamper. These tests will only \n be run when we have available hamper on the lot.\n '))
class AffordanceTagFactory(HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = { 'interaction_tags': TunableTags( description= '\n Affordances with any of these tags to affect.\n ', filter_prefixes=('Interaction', )), 'exceptions': TunableList( description= '\n Affordances that are not affected even if they have the specified\n tags.\n ', tunable=TunableAffordanceListReference(pack_safe=True)) } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) affordance_exceptions = frozenset(affordance for exception_list in self.exceptions for affordance in exception_list) self.affordance_exceptions = affordance_exceptions or None def __call__(self, affordance): if affordance.interaction_category_tags & self.interaction_tags and ( self.affordance_exceptions is None or affordance not in self.affordance_exceptions): return False return True
class ObjectStateHelper(AutoFactoryInit, HasTunableSingletonFactory): FACTORY_TUNABLES = { 'object_target': TunableObjectTargetVariant( description= '\n Define the set of objects that this interaction is applied to.\n ' ), 'object_tags': TunableTags( description= '\n Find all of the objects based on these tags.\n ', filter_prefixes=('func', )), 'desired_state': TunableStateValueReference( description= '\n State that will be set to the objects.\n ', pack_safe=True), 'tests': TunableTestSet( description= "\n If pass these tests, the object's state will be changed to\n Desired State.\n " ) } def execute_helper(self, interaction): if self.desired_state is not None: objects = list(services.object_manager().get_objects_with_tags_gen( *self.object_tags)) for obj in self.object_target.get_object_target_gen( interaction, objects): resolver = SingleObjectResolver(obj) if self.tests.run_tests(resolver): obj.set_state(self.desired_state.state, self.desired_state)
class OutfitChangeSituation(HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = {'situation_tags': TunableTags(description='\n Tags for situations that will be considered for the outfit\n interactions.\n ', filter_prefixes=['situation'])} def outfit_affordances_gen(self, sim, target, affordance, **kwargs): resolver = SingleSimResolver(sim.sim_info) for situation in services.get_zone_situation_manager().get_situations_by_tags(self.situation_tags): situation_job = situation.get_current_job_for_sim(sim) if situation_job is not None: if situation_job.job_uniform is not None: outfit_generators = situation_job.job_uniform.situation_outfit_generators if outfit_generators is None: continue for entry in outfit_generators: if entry.tests.run_tests(resolver): yield AffordanceObjectPair(affordance, target, affordance, None, pie_menu_cateogory=affordance.category, outfit_tags=entry.generator.tags, **kwargs) def get_outfit_tags(self): outfit_tags = set() situation_manager = services.get_instance_manager(sims4.resources.Types.SITUATION) for situation in situation_manager.types.values(): if self.situation_tags: if any(tag in situation.tags for tag in self.situation_tags): for situation_job in situation.get_tuned_jobs(): if situation_job.job_uniform is None: continue outfit_generators = situation_job.job_uniform.situation_outfit_generators if outfit_generators is None: continue for entry in outfit_generators: for tag in entry.generator.tags: outfit_tags.add(tag) return outfit_tags def get_outfit_for_clothing_change(self, sim_info, outfit_change_category): return sim_info.get_outfit_for_clothing_change(None, OutfitChangeReason.DefaultOutfit, resolver=SingleSimResolver(sim_info))
class ApplyTagsToObject(BaseLootOperation): FACTORY_TUNABLES = { 'apply_unpersisted_tags': TunableTags( description= '\n A set of unpersisted category tags to apply to the finished product.\n ' ), 'apply_persisted_tags': TunableTags( description= '\n A set of persisted category tags to apply to the finished product.\n ' ) } def __init__(self, apply_unpersisted_tags, apply_persisted_tags, *args, **kwargs): super().__init__(*args, **kwargs) self._apply_unpersisted_tags = apply_unpersisted_tags self._apply_persisted_tags = apply_persisted_tags def _apply_to_subject_and_target(self, subject, target, resolver): if subject is None: return if hasattr(subject, 'append_tags'): subject.append_tags(self._apply_unpersisted_tags, persist=False) subject.append_tags(self._apply_persisted_tags, persist=True) else: logger.error( "ApplyTagsToObject Tuning: Subject {} does not have attribute 'append_tags'", subject) @TunableFactory.factory_option def subject_participant_type_options(description=singletons.DEFAULT, **kwargs): if description is singletons.DEFAULT: description = 'The object the tags are applied to.' return BaseLootOperation.get_participant_tunable( *('subject', ), description=description, default_participant=interactions.ParticipantType.Object, **kwargs)
class _ObjectsFromTags(HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = { 'tags': TunableTags( description= '\n For each tag, in order, gather objects that match that have that\n tag. If the placement fails, consider another object, then consider\n objects for the next tag.\n ' ) } def get_objects_gen(self, resolver): for tag in self.tags: yield from services.object_manager().get_objects_with_tag_gen(tag)
class SituationByTags(_SituationMatchBase, HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = { 'situation_tags': TunableTags( description= '\n Situation tags to match.\n \n A situation that matches ANY of these tags will match.\n ', filter_prefixes=('situation', ), minlength=1) } def match(self, situation): return self.situation_tags & situation.tags
class _RouteTargetTypeObject(_RouteTargetType): FACTORY_TUNABLES = { 'tags': TunableTags( description= '\n Tags used to pre-filter the list of potential targets.\n If any of the tags match the object will be considered.\n ', filter_prefixes=('Func', )) } def get_objects(self): if self.tags: return services.object_manager().get_objects_matching_tags( self.tags, match_any=True) else: return services.object_manager().get_valid_objects_gen()
class _SicknessTagTest(HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = { 'tags': TunableTags( description= '\n Only sickness that share any of the tags specified pass. \n ', filter_prefixes=('Sickness', )) } def test_item(self, item): if item is None: return False return self.tags & item.sickness_tags def test_collection(self, collection): return any(self.test_item(item) for item in collection)
class RandomizeCASPart(BaseAppearanceModification): FACTORY_TUNABLES = { 'body_type': TunableEnumEntry( description= '\n The body type that will have its part randomized.\n ', tunable_type=BodyType, default=BodyType.NONE, invalid_enums=(BodyType.NONE, )), 'tag_categories_to_keep': TunableSet( description= '\n Match tags from the existing CAS part of the specified body \n type that belong to these tag categories when searching\n for a new random part.\n ', tunable=TunableEnumEntry( description= '\n Tags that belong to this category that are on the existing\n CAS part of the specified body type will be used to find\n a new random part.\n ', tunable_type=TagCategory, default=TagCategory.INVALID, invalid_enums=(TagCategory.INVALID, ))), 'tags': TunableTags( description= '\n List of tags to use when randomizing a CAS part for the tuned\n body type.\n ' ) } def modify_sim_info(self, source_sim_info, modified_sim_info, random_seed): if randomize_caspart(source_sim_info._base, modified_sim_info._base, self.body_type, list(self.tag_categories_to_keep), random_seed, list(self.tags)): return BodyTypeFlag.make_body_type_flag(self.body_type) return BodyTypeFlag.NONE @property def modifier_type(self): return AppearanceModifierType.RANDOMIZE_CAS_PART @property def combinable_sorting_key(self): return self.body_type def __repr__(self): return standard_repr(self, body_type=self.body_type)
class _PortalTypeDataTeleport(_PortalTypeDataLocomotion): FACTORY_TUNABLES = { 'destination_object_tags': TunableTags( description= '\n A list of tags used to find objects that this object connects with\n to form two sides of a portal. \n When the portals are created all of the objects on the lot with at \n least one of the tags found in this list are found and a portal is \n created between the originating object and the described object.\n ' ) } @property def requires_los_between_points(self): return False @property def portal_type(self): return PortalType.PortalType_Wormhole @cached def get_portal_locations(self, obj): object_manager = services.object_manager() locations = [] for connected_object in object_manager.get_objects_with_tags_gen( *self.destination_object_tags): if connected_object is obj: continue for portal_entry in self.object_portals: entry_location = portal_entry.location_entry(obj) exit_location = portal_entry.location_exit(connected_object) if portal_entry.is_bidirectional: locations.append((entry_location, exit_location, exit_location, entry_location, 0)) else: locations.append( (entry_location, exit_location, None, None, 0)) return locations @cached def get_destination_objects(self): object_manager = services.object_manager() destination_objects = tuple( object_manager.get_objects_with_tags_gen( *self.destination_object_tags)) return destination_objects
class _DestroyObjectSelectionRuleTags(_DestroyObjectSelectionRule): FACTORY_TUNABLES = { 'tags': TunableTags( description= '\n Only objects with these tags are considered.\n ', filter_prefixes=('Func', )), 'radius': TunableDistanceSquared( description= '\n Only objects within this distance are considered.\n ', default=1) } def get_objects(self, obj, target): objects = tuple( o for o in services.object_manager().get_objects_matching_tags( self.tags, match_any=True) if (o.position - obj.position).magnitude_squared() <= self.radius) return objects
class _RandomFromTags(CreationDataBase, HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = { 'filter_tags': TunableTags( description= '\n Define tags to try and create the object. Picks randomly from\n objects with these tags.\n ', minlength=1) } def get_definition(self, resolver): definition_manager = services.definition_manager() filtered_defs = list( definition_manager.get_definitions_for_tags_gen(self.filter_tags)) if len(filtered_defs) > 0: return random.choice(filtered_defs) logger.error( '{} create object basic extra tries to find object definitions tagged as {} , but no object definitions were found.', resolver, self.filter_tags, owner='jgiordano')
class SicknessTuning: SICKNESS_TIME_OF_DAY = TunableTimeOfDay( description= '\n Hour of day in which sicknesses will be distributed to Sims.\n ', default_hour=3) SICKNESS_TESTS = TunableTestSet( description= '\n Test sets determining whether or not a given Sim may become sick at all.\n These tests run before we attempt to roll on whether or not \n the Sim can avoid becoming sick. \n (ORs of ANDs)\n ' ) SICKNESS_CHANCE = TestedSum.TunableFactory( description= '\n Chance of any given Sim to become sick. \n \n Chance is out of 100.\n \n When the sum of the base value and values from passed tests are\n greater than 100, the Sim is guaranteed to become sick during a \n sickness distribution pass.\n \n When 0 or below, the Sim will not get sick.\n ' ) PREVIOUS_SICKNESSES_TO_TRACK = TunableRange( description= '\n Number of previous sicknesses to track. Can use this to help promote\n variation of sicknesses a Sim receives over time.', tunable_type=int, minimum=0, default=1) EXAM_TYPES_TAGS = TunableTags( description= '\n Tags that represent the different types of objects that are used\n to run exams.\n ', filter_prefixes=('interaction', ))
class _SicknessMatchingCritera(HasTunableSingletonFactory, AutoFactoryInit): @staticmethod def _verify_tunable_callback(instance_class, tunable_name, source, value): if value.tags is None and value.difficulty_range is None: logger.error('_SicknessMatchingCritera: {} has a sickness criteria {} that sets no criteria.', source, tunable_name) FACTORY_TUNABLES = {'tags': OptionalTunable(description='\n Optionally, only sicknesses that share any of the tags specified are considered. \n ', tunable=TunableTags(filter_prefixes=('Sickness',))), 'difficulty_range': OptionalTunable(description="\n Optionally define the difficulty rating range that is required\n for the Sim's sickness.\n ", tunable=TunableInterval(description="\n The difficulty rating range, this maps to 'difficulty_rating'\n values in Sickness tuning.\n ", tunable_type=float, default_lower=0, default_upper=10, minimum=0, maximum=10)), 'verify_tunable_callback': _verify_tunable_callback} def give_sickness(self, subject): sickness_criteria = CallableTestList() if self.tags is not None: sickness_criteria.append(lambda s: self.tags & s.sickness_tags) if self.difficulty_range is not None: sickness_criteria.append(lambda s: s.difficulty_rating in self.difficulty_range) services.get_sickness_service().make_sick(subject, criteria_func=sickness_criteria, only_auto_distributable=False)
class Sickness(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(sims4.resources.Types.SICKNESS)): INSTANCE_TUNABLES = {'diagnosis_stat': TunableReference(description='\n Statistic we are using to track diagnostic progress for this sickness.\n This is used for the threshold actions checks.\n ', manager=services.get_instance_manager(Types.STATISTIC)), 'threshold_actions': TunableMapping(description='\n After passing specific values of the diagnosis stat, perform\n the appropriate actions.\n ', key_type=int, value_type=TunableList(description='\n List of actions to process when this threshold is reached \n or passed.', tunable=_DiagnosticThresholdActions())), 'display_name': TunableLocalizedStringFactory(description="\n The sickness's display name. This string is provided with the owning\n Sim as its only token.\n ", tuning_group=GroupNames.UI), 'difficulty_rating': TunableRange(description='\n The difficulty rating for treating this sickness.\n ', tunable_type=float, tuning_group=GroupNames.UI, default=5, minimum=0, maximum=10), 'symptoms': TunableSet(description='\n Symptoms associated with this sickness. When the sickness\n is applied to a Sim, all symptoms are applied.', tunable=TunableReference(manager=services.get_instance_manager(Types.SICKNESS), class_restrictions=(Symptom,), pack_safe=True)), 'associated_buffs': TunableSet(description='\n The associated buffs that will be added to the Sim when the sickness\n is applied, and removed when the sickness is removed.\n ', tunable=TunableReference(manager=services.get_instance_manager(Types.BUFF), pack_safe=True)), 'associated_statistics': TunableSet(description="\n The associated stats that will be added to the Sim when the sickness\n is applied, and removed when the sickness is removed.\n \n These are added at the statistic's default value.\n ", tunable=TunableReference(manager=services.get_instance_manager(Types.STATISTIC), pack_safe=True)), 'restrictions': TunableTestSet(description='\n Test set specifying whether or not this sickness can be applied.\n One set of tests must pass in order for the sickness to be valid.\n (This is an OR of ANDS.)\n '), 'weight': TestedSum.TunableFactory(description='\n Weighted value of this sickness versus other valid sicknesses that\n are possible for the Sim to apply a sickness to.\n \n Tests, if defined here, may adjust the weight in addition \n to the tuned base value.\n '), 'difficulty_rating': TunableRange(description='\n The difficulty rating for treating this sickness.\n ', tunable_type=float, tuning_group=GroupNames.UI, default=5, minimum=0, maximum=10), 'examination_loots': TunableMapping(description='\n Mapping of examination result types to loots to apply\n as a result of the interaction.\n ', key_type=TunableEnumEntry(tunable_type=DiagnosticActionResultType, default=DiagnosticActionResultType.DEFAULT), value_type=_DiagnosticActionLoots.TunableFactory()), 'treatment_loots': TunableMapping(description='\n Mapping of treatment result types to loots to apply\n as a result of the interaction.\n ', key_type=TunableEnumEntry(tunable_type=DiagnosticActionResultType, default=DiagnosticActionResultType.DEFAULT), value_type=_DiagnosticActionLoots.TunableFactory()), 'available_treatments': TunableSet(description='\n Treatments that are available for this sickness.\n ', tunable=TunableReference(manager=services.get_instance_manager(Types.INTERACTION), pack_safe=True)), 'available_treatment_lists': TunableSet(description='\n Treatments that are available for this sickness.\n ', tunable=snippets.TunableAffordanceListReference()), 'correct_treatments': TunableSet(description='\n Treatments that can cure this sickness. These sicknesses\n will never be ruled out as exams are performed.\n ', tunable=TunableReference(manager=services.get_instance_manager(Types.INTERACTION), pack_safe=True)), 'sickness_tags': TunableTags(description='\n Tags that help categorize this sickness.\n ', filter_prefixes=('Sickness',)), 'track_in_history': Tunable(description='\n If checked, this is tracked in sickness history.\n ', tunable_type=bool, default=True), 'considered_sick': Tunable(description='\n Considered as sickness. Most sickness should have this tuned.\n Examinations, which are pseudo-sicknesses will have this tuned false.\n \n If this is checked, the sickness will pass is_sick tests.\n ', tunable_type=bool, default=True), 'distribute_manually': Tunable(description='\n If checked, this is not distributed by the sickness service,\n and must be done by a game system or loot.\n ', tunable_type=bool, default=False)} @classmethod def _can_be_applied(cls, resolver=None, sim_info=None): if not resolver and not sim_info: raise ValueError('Must specify a Sim info or a resolver') if not resolver: resolver = SingleSimResolver(sim_info) return cls.restrictions.run_tests(resolver) @classmethod def get_sickness_weight(cls, resolver=None, sim_info=None): if not resolver and not sim_info: raise ValueError('Must specify a Sim info or a resolver') if not cls._can_be_applied(resolver=resolver): return 0 return max(cls.weight.get_modified_value(resolver), 0) @classmethod def apply_to_sim_info(cls, sim_info, from_load=False): if not cls._can_be_applied(resolver=SingleSimResolver(sim_info)): return for symptom in cls.symptoms: symptom.apply_to_sim_info(sim_info) for buff in cls.associated_buffs: if buff.can_add(sim_info): if not sim_info.has_buff(buff): sim_info.add_buff(buff, buff_reason=cls.display_name) for stat in cls.associated_statistics: if not sim_info.get_tracker(stat).has_statistic(stat): sim_info.add_statistic(stat, stat.default_value) if not from_load: sim_info.sickness_tracker.add_sickness(cls) @classmethod def remove_from_sim_info(cls, sim_info): if sim_info is None: return for symptom in cls.symptoms: symptom.remove_from_sim_info(sim_info) for buff in cls.associated_buffs: sim_info.remove_buff_by_type(buff) for stat in cls.associated_statistics: sim_info.remove_statistic(stat) sim_info.remove_statistic(cls.diagnosis_stat) if sim_info.has_sickness(cls): sim_info.sickness_tracker.remove_sickness() @classmethod def is_available_treatment(cls, affordance): return affordance in itertools.chain(cls.available_treatments, *cls.available_treatment_lists) @classmethod def is_correct_treatment(cls, affordance): return affordance in cls.correct_treatments @classmethod def apply_loots_for_action(cls, action_type, result_type, interaction): loots_to_apply = None if action_type == SicknessDiagnosticActionType.EXAM: if result_type in cls.examination_loots: loots_to_apply = cls.examination_loots[result_type] elif action_type == SicknessDiagnosticActionType.TREATMENT: if result_type in cls.treatment_loots: loots_to_apply = cls.treatment_loots[result_type] if loots_to_apply is not None: loots_to_apply.apply_loots(interaction.get_resolver()) cls._handle_threshold_actions(interaction.get_resolver()) @classmethod def _handle_threshold_actions(cls, resolver): cls.update_diagnosis(resolver.target.sim_info, interaction=resolver.interaction) @classmethod def update_diagnosis(cls, sim_info, interaction=None): diagnostic_progress = sim_info.get_statistic(cls.diagnosis_stat).get_value() last_progress = sim_info.sickness_tracker.last_progress if diagnostic_progress == last_progress: return for (threshold, actions) in cls._get_sorted_threshold_actions(): if threshold <= last_progress: continue if diagnostic_progress < threshold: break for action in actions: action.perform(sim_info, interaction=interaction) sim_info.sickness_record_last_progress(diagnostic_progress) @classmethod def _get_sorted_threshold_actions(cls): return sorted(cls.threshold_actions.items(), key=operator.itemgetter(0)) @classmethod def on_zone_load(cls, sim_info): cls.on_sim_info_loaded(sim_info) @classmethod def on_sim_info_loaded(cls, sim_info): if not sim_info.has_sickness_tracking(): return sim_info.sickness_record_last_progress(sim_info.get_statistic(cls.diagnosis_stat).get_value()) cls.apply_to_sim_info(sim_info, from_load=True) @classmethod def get_ordered_symptoms(cls): ordered_symptoms = [] for (_, action_list) in cls._get_sorted_threshold_actions(): for action in action_list: if isinstance(action, _DiscoverSymptomThresholdAction): ordered_symptoms.append(action.symptom) return ordered_symptoms
class ActingEmployeeSituation(SituationComplexCommon): INSTANCE_TUNABLES = { '_pre_performance_state': _ActingEmployeePrePerformanceState.TunableFactory( description= '\n The initial state for npc co star sims.\n ', tuning_group=GroupNames.STATE, display_name='01_pre_performance_state'), '_go_to_marks_state': _ActingEmployeeGoToMarksState.TunableFactory( description= '\n The employee sim will go to this state once the player says their\n ready to perform.\n ', tuning_group=GroupNames.STATE, display_name='02_go_to_marks_state'), '_performance_state': _ActingEmployeePerformanceState.TunableFactory( description= '\n Once the employee gets to their marks, they will end up in this\n state. The only interactions that should be valid at this point is\n some idle interaction and the performance interactions.\n ', tuning_group=GroupNames.STATE, display_name='03_performance_state'), '_post_performance_state': _ActingEmployeePostPerformanceState.TunableFactory( description= '\n When the main situation goal is completed by the player, employees will be pushed into\n this state.\n ', tuning_group=GroupNames.STATE, display_name='04_post_performance_state'), '_actor_career_event_situation_tags': TunableTags( description= '\n A set of tags that can identify an actor career event situation.\n \n Used to track when the actor completes the performance.\n ', tuning_group=GroupNames.SITUATION, filter_prefixes=('Situation', )) } @classmethod def _states(cls): return (SituationStateData(1, _ActingEmployeePrePerformanceState, factory=cls._pre_performance_state), SituationStateData(2, _ActingEmployeeGoToMarksState, factory=cls._go_to_marks_state), SituationStateData(3, _ActingEmployeePerformanceState, factory=cls._performance_state), SituationStateData(4, _ActingEmployeePostPerformanceState, factory=cls._post_performance_state)) @classmethod def default_job(cls): pass @classmethod def _get_tuned_job_and_default_role_state_tuples(cls): return list(cls._pre_performance_state._tuned_values. job_and_role_changes.items()) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._register_test_event_for_keys( TestEvent.MainSituationGoalComplete, self._actor_career_event_situation_tags) self._register_test_event_for_keys( TestEvent.SituationEnded, self._actor_career_event_situation_tags) def start_situation(self): super().start_situation() self._change_state(self._pre_performance_state()) def handle_event(self, sim_info, event, resolver): super().handle_event(sim_info, event, resolver) if event == TestEvent.MainSituationGoalComplete: self._change_state(self._post_performance_state()) elif event == TestEvent.SituationEnded: self._change_state(self._post_performance_state())
class SituationIdentityTest(HasTunableSingletonFactory, AutoFactoryInit, BaseTest): FACTORY_TUNABLES = {'situation_list': TunableSet(description='\n Test will pass if the specified reference is in the given list.\n ', tunable=TunableReference(manager=services.get_instance_manager(Types.SITUATION), pack_safe=True)), 'situation_tags': TunableTags(description='\n Test will pass if the tested reference is tagged\n with one of the tuned tags.\n ', filter_prefixes=('situation',))} @cached_test def __call__(self, situation): match = situation in self.situation_list or self.situation_tags & situation.tags if not match: return TestResult(False, 'Failed {}. Items Tested: {}. Tags Tested: {}', situation, self.situation_list, self.situation_tags, tooltip=self.tooltip) return TestResult.TRUE
class HolidayTradition(HasTunableReference, HolidayTraditionDisplayMixin, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager( sims4.resources.Types.HOLIDAY_TRADITION)): INSTANCE_TUNABLES = { 'situation_goal': TunableReference( description= '\n This is the situation goal that will be offered when this tradition\n is active.\n ', manager=services.get_instance_manager( sims4.resources.Types.SITUATION_GOAL)), 'pre_holiday_buffs': TunableList( description= '\n A list of buffs that will be given out to all of the player Sims\n during the pre-holiday period of each holiday.\n ', tunable=TunableReference( description= '\n A buff that is given to all of the player Sims when it is the\n pre-holiday period.\n ', manager=services.get_instance_manager( sims4.resources.Types.BUFF)), unique_entries=True), 'pre_holiday_buff_reason': OptionalTunable( description= '\n If set, specify a reason why the buff was added.\n ', tunable=TunableLocalizedString( description= '\n The reason the buff was added. This will be displayed in the\n buff tooltip.\n ' )), 'holiday_buffs': TunableList( description= '\n A list of buffs that will be given out to all Sims during each\n holiday.\n ', tunable=TunableReference( description= '\n A buff that is given to all Sims during the holiday.\n ', manager=services.get_instance_manager( sims4.resources.Types.BUFF)), unique_entries=True), 'holiday_buff_reason': OptionalTunable( description= '\n If set, specify a reason why the buff was added.\n ', tunable=TunableLocalizedString( description= '\n The reason the buff was added. This will be displayed in the\n buff tooltip.\n ' )), 'drama_nodes_to_score': TunableList( description= '\n Drama nodes that we will attempt to schedule and score when this\n tradition becomes active.\n ', tunable=TunableReference( description= '\n A drama node that we will put in the scoring pass when this\n tradition becomes active.\n ', manager=services.get_instance_manager( sims4.resources.Types.DRAMA_NODE)), unique_entries=True), 'drama_nodes_to_run': TunableList( description= '\n Drama nodes that will be run when the tradition is activated.\n ', tunable=TunableReference( description= '\n A drama node that we will run when the holiday becomes active.\n ', manager=services.get_instance_manager( sims4.resources.Types.DRAMA_NODE)), unique_entries=True), 'additional_walkbys': SituationCurve.TunableFactory( description= '\n An additional walkby schedule that will be added onto the walkby\n schedule when the tradition is active.\n ', get_create_params={'user_facing': False}), 'preference': TunableList( description= '\n A list of pairs of preference categories and tests. To determine\n what a Sim feels about a tradition each set of tests in this list\n will be run in order. When one of the test sets passes then we\n will set that as the preference. If none of them pass we will\n default to LIKES.\n ', tunable=TunableTuple( description= '\n A pair of preference and test set.\n ', preference=TunableEnumEntry( description= '\n The preference that the Sim will have to this tradition if\n the test set passes.\n ', tunable_type=TraditionPreference, default=TraditionPreference.LIKES), tests=TunablePreferenceTestList( description= '\n A set of tests that need to pass for the Sim to have the\n tuned preference.\n ' ), reason=OptionalTunable( description= '\n If enabled then we will also give this reason as to why the\n preference is the way it is.\n ', tunable=TunableLocalizedString( description= '\n The reason that the Sim has this preference.\n ' )))), 'preference_reward_buff': OptionalTunable( description= '\n If enabled then if the Sim loves this tradition when the holiday is\n completed they will get a special buff if they completed the\n tradition.\n ', tunable=TunableBuffReference( description= '\n The buff given if this Sim loves the tradition and has completed\n it at the end of the holiday.\n ' )), 'selectable': Tunable( description= '\n If checked then this tradition will appear in the tradition\n selection.\n ', tunable_type=bool, default=True, tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'lifecycle_actions': TunableList( description= '\n Actions that occur as a result of the tradition activation/de-activation.\n ', tunable=TraditionActions.TunableFactory()), 'events': TunableList( description= '\n A list of times and things we want to happen at that time.\n ', tunable=TunableTuple( description= '\n A pair of a time of day and event of something that we want\n to occur.\n ', time=TunableTimeOfDay( description= '\n The time of day this event will occur.\n ' ), event=TunableVariant( description= '\n What we want to occur at this time.\n ', modify_items=ModifyAllItems(), start_situation=StartSituation(), default='start_situation'))), 'core_object_tags': TunableTags( description= '\n Tags of all the core objects used in this tradition.\n ', filter_prefixes=('func', ), tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'deco_object_tags': TunableTags( description= '\n Tags of all the deco objects used in this tradition.\n ', filter_prefixes=('func', ), tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'business_cost_multiplier': TunableMapping( description= '\n A mapping between the business type and the cost multiplier that\n we want to use if this tradition is active.\n ', key_type=TunableEnumEntry( description= '\n The type of business that we want to apply this price modifier\n on.\n ', tunable_type=BusinessType, default=BusinessType.INVALID, invalid_enums=(BusinessType.INVALID, )), value_type=TunableRange( description= '\n The value of the multiplier to use.\n ', tunable_type=float, default=1.0, minimum=0.0)) } @classmethod def _verify_tuning_callback(cls): if cls._display_data.instance_display_description is None: logger.error('Tradition {} missing display description', cls) if cls._display_data.instance_display_icon is None: logger.error('Tradition {} missing display icon', cls) if cls._display_data.instance_display_name is None: logger.error('Tradition {} missing display name', cls) def __init__(self): self._state = HolidayState.INITIALIZED self._buffs_added = defaultdict(list) self._event_alarm_handles = {} self._drama_node_processor = None @property def state(self): return self._state @classmethod def get_buiness_multiplier(cls, business_type): return cls.business_cost_multiplier.get(business_type, 1.0) @classmethod def get_sim_preference(cls, sim_info): resolver = SingleSimResolver(sim_info) for possible_preference in cls.preference: if possible_preference.tests.run_tests(resolver): return (possible_preference.preference, possible_preference.reason) return (TraditionPreference.LIKES, None) def on_sim_spawned(self, sim): if self._state == HolidayState.PRE_DAY: if sim.is_npc: return for buff in self.pre_holiday_buffs: buff_handle = sim.add_buff( buff, buff_reason=self.pre_holiday_buff_reason) if buff_handle is not None: self._buffs_added[sim.sim_id].append(buff_handle) elif self._state == HolidayState.RUNNING: for buff in self.holiday_buffs: buff_handle = sim.add_buff( buff, buff_reason=self.holiday_buff_reason) if buff_handle is not None: self._buffs_added[sim.sim_id].append(buff_handle) def activate_pre_holiday(self): if self._state >= HolidayState.PRE_DAY: logger.error( 'Tradition {} is trying to be put into the pre_holiday, but is already in {} which is farther along.', self, self._state) return self._state = HolidayState.PRE_DAY if self.pre_holiday_buffs: services.sim_spawner_service().register_sim_spawned_callback( self.on_sim_spawned) for sim_info in services.active_household().instanced_sims_gen(): for buff in self.pre_holiday_buffs: buff_handle = sim_info.add_buff( buff, buff_reason=self.pre_holiday_buff_reason) if buff_handle is not None: self._buffs_added[sim_info.sim_id].append(buff_handle) def _remove_all_buffs(self): sim_info_manager = services.sim_info_manager() for (sim_id, buff_handles) in self._buffs_added.items(): sim_info = sim_info_manager.get(sim_id) if sim_info is None: continue if sim_info.Buffs is None: continue for buff_handle in buff_handles: sim_info.remove_buff(buff_handle) self._buffs_added.clear() def _deactivate_pre_holiday(self): if self.pre_holiday_buffs: services.sim_spawner_service().unregister_sim_spawned_callback( self.on_sim_spawned) self._remove_all_buffs() def deactivate_pre_holiday(self): if self._state != HolidayState.PRE_DAY: logger.error( 'Tradition {} is trying to deactivate the preday, but it is in the {} state, not that one.', self, self._state) self._state = HolidayState.SHUTDOWN self._deactivate_pre_holiday() def _create_event_alarm(self, key, event): def callback(_): event.event.perform(GlobalResolver()) del self._event_alarm_handles[key] now = services.time_service().sim_now time_to_event = now.time_till_next_day_time(event.time) if key in self._event_alarm_handles: alarms.cancel_alarm(self._event_alarm_handles[key]) self._event_alarm_handles[key] = alarms.add_alarm( self, time_to_event, callback) def _process_scoring_gen(self, timeline): try: yield from services.drama_scheduler_service( ).score_and_schedule_nodes_gen(self.drama_nodes_to_score, 1, timeline=timeline) except GeneratorExit: raise except Exception as exception: logger.exception('Exception while scoring DramaNodes: ', exc=exception, level=sims4.log.LEVEL_ERROR) finally: self._drama_node_processor = None def activate_holiday(self, from_load=False, from_customization=False): if self._state >= HolidayState.RUNNING: logger.error( 'Tradition {} is trying to be put into the Running state, but is already in {} which is farther along.', self, self._state) return self._deactivate_pre_holiday() self._state = HolidayState.RUNNING if self.holiday_buffs: services.sim_spawner_service().register_sim_spawned_callback( self.on_sim_spawned) for sim_info in services.sim_info_manager().instanced_sims_gen(): for buff in self.holiday_buffs: buff_handle = sim_info.add_buff( buff, buff_reason=self.holiday_buff_reason) if buff_handle is not None: self._buffs_added[sim_info.sim_id].append(buff_handle) for (key, event) in enumerate(self.events): self._create_event_alarm(key, event) if not from_load: resolver = GlobalResolver() for actions in self.lifecycle_actions: actions.try_perform( resolver, TraditionActivationEvent.TRADITION_ADD if from_customization else TraditionActivationEvent.HOLIDAY_ACTIVATE) if self.drama_nodes_to_score: sim_timeline = services.time_service().sim_timeline self._drama_node_processor = sim_timeline.schedule( elements.GeneratorElement(self._process_scoring_gen)) drama_scheduler = services.drama_scheduler_service() for drama_node in self.drama_nodes_to_run: drama_scheduler.run_node(drama_node, resolver) def deactivate_holiday(self, from_customization=False): if self._state != HolidayState.RUNNING: logger.error( 'Tradition {} is trying to deactivate the tradition, but it is in the {} state, not that one.', self, self._state) self._state = HolidayState.SHUTDOWN if self.holiday_buffs: services.sim_spawner_service().unregister_sim_spawned_callback( self.on_sim_spawned) self._remove_all_buffs() for alarm in self._event_alarm_handles.values(): alarms.cancel_alarm(alarm) self._event_alarm_handles.clear() resolver = GlobalResolver() for actions in self.lifecycle_actions: actions.try_perform( resolver, TraditionActivationEvent.TRADITION_REMOVE if from_customization else TraditionActivationEvent.HOLIDAY_DEACTIVATE) def get_additional_walkbys(self, predicate=lambda _: True): weighted_situations = self.additional_walkbys.get_weighted_situations( predicate=predicate) if weighted_situations is None: return () return weighted_situations
class BowlingVenueMixin: BOWLING_LANE_OBJECT_TAGS = TunableTags( description= '\n Tags that considered bowling lane objects.\n ', filter_prefixes=('func', )) MAX_SIMS_ON_VENUE = TunableRange( description= '\n The maximum number of the Sims can be spawned in all bowling situations\n combined in the venue.\n ', tunable_type=int, default=4, minimum=1) MOONLIGHT_AFFORDANCES = TunableTuple( description= '\n Super affordances to set moonlight bowling lane when they are turned\n on and turned off.\n ', moonlight_on=TunableReference( description= '\n A super affordance when the moonlight is turned on.\n ', manager=services.affordance_manager(), class_restrictions=('SwitchLightAndStateImmediateInteraction', ), pack_safe=True), moonlight_off=TunableReference( description= '\n A super affordance when the moonlight is turned off.\n ', manager=services.affordance_manager(), class_restrictions=('SwitchLightAndStateImmediateInteraction', ), pack_safe=True)) INSTANCE_TUNABLES = { 'bowling_venue': TunableTuple( description= '\n Holds data associated with bowling venue situations.\n ', situation_alarm_interval=TunableRange( description= '\n Interval in sim minutes to trigger bowling situation.\n ', tunable_type=int, minimum=1, default=60), bowling_situations_schedule=SituationCurve.TunableFactory( description= '\n Bowling situations that are created depending on what time of \n the day it is.\n ', get_create_params={'user_facing': False}), moonlight_on_off_schedule=TunableList( description= '\n A list of tuples declaring a schedule to turn on/off moonlight bowling lane.\n ', tunable=TunableTuple( description= '\n The first value is the day of the week that maps to a desired\n on/off time for moonlight bowling by the time of the day.\n \n days_of_the_week on_off_moonlight_by_time_of_day\n M,Th,F schedule_1\n W,Sa schedule_2\n ', days_of_the_week=TunableDayAvailability(), moonlight_on_off_desire_by_time_of_day=TunableMapping( description= '\n Each entry in the map has two columns.\n The first column is the hour of the day (0-24)\n that maps to is_moonlight_on.\n \n The entry with starting hour that is closest to, but before\n the current hour will be chosen.\n \n Given this tuning\n beginning_hour is_moonlight_on\n 6 true\n 10 false\n 14 true\n 20 false\n \n if the current hour is 11, hour_of_day will be 10 and moonlight_on is false, so moonlight is disabled.\n if the current hour is 19, hour_of_day will be 14 and moonlight_on is true, so moonlight is enabled.\n if the current hour is 23, hour_of_day will be 20 and moonlight_on is false, so moonlight is disabled.\n if the current hour is 2, hour_of_day will be 20 and moonlight_on is false. (uses 20 tuning because it is not 6 yet)\n \n The entries will be automatically sorted by time.\n ', key_type=Tunable(tunable_type=int, default=0), value_type=Tunable(tunable_type=bool, default=False), key_name='beginning_hour', value_name='is_moonlight_on')))) } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._bowling_lanes = self._get_bowling_lanes() self._bowling_lanes_dict = {} self._bowling_situation_alarm_handle = None self._moonlight_alarm_handle = None def create_situations_during_zone_spin_up(self): super().create_situations_during_zone_spin_up() self._initialize_alarms() def on_shutdown(self): super().on_shutdown() if self._bowling_situation_alarm_handle is not None: alarms.cancel_alarm(self._bowling_situation_alarm_handle) self._bowling_situation_alarm_handle = None if self._moonlight_alarm_handle is not None: alarms.cancel_alarm(self._moonlight_alarm_handle) self._moonlight_alarm_handle = None def on_exit_buildbuy(self): super().on_exit_buildbuy() self._bowling_lanes = self._get_bowling_lanes() def _initialize_alarms(self): repeating_time_span = date_and_time.create_time_span( minutes=self.bowling_venue.situation_alarm_interval) self._bowling_situation_alarm_handle = alarms.add_alarm( self, date_and_time.create_time_span(minutes=5), self._find_available_lane_and_start_situations, repeating=True, repeating_time_span=repeating_time_span) if self.bowling_venue.moonlight_on_off_schedule: self._moonlight_change() time_of_day = services.time_service().sim_now self._moonlight_alarm_handle = alarms.add_alarm( self, self._get_time_span_to_next_moonlight_schedule(time_of_day), self._moonlight_change) def get_total_bowling_lanes(self): if self._bowling_lanes is None: return 0 return len(self._bowling_lanes) def set_situation_bowling_lane(self, situation_id, bowling_lane_obj): self._bowling_lanes_dict[situation_id] = bowling_lane_obj def get_situation_bowling_lane(self, situation_id): return self._bowling_lanes_dict.get(situation_id) def remove_situation_bowling_lane(self, situation_id): if situation_id in self._bowling_lanes_dict: del self._bowling_lanes_dict[situation_id] def _get_bowling_lanes(self): if self.BOWLING_LANE_OBJECT_TAGS is None: return else: bowling_lanes = list( services.object_manager().get_objects_with_tags_gen( *self.BOWLING_LANE_OBJECT_TAGS)) if len(bowling_lanes) == 0: return return bowling_lanes def _get_number_of_sims_in_all_situations(self): situation_manager = services.get_zone_situation_manager() if situation_manager is None: return 0 total_sims = 0 for (situation_id, _) in self._bowling_lanes_dict.items(): situation = situation_manager.get(situation_id) if situation is not None: total_sims += len(situation._situation_sims) return total_sims def _find_available_lane_and_start_situations(self, *_): if self._bowling_lanes is None: return if self.get_total_bowling_lanes() - len(self._bowling_lanes_dict) <= 1: return if self._get_number_of_sims_in_all_situations( ) >= self.MAX_SIMS_ON_VENUE: return for bowling_lane_obj in self._bowling_lanes: if bowling_lane_obj.in_use: continue if bowling_lane_obj in self._bowling_lanes_dict.values(): continue self._start_bowling_situation(bowling_lane_obj) break def _start_bowling_situation(self, bowling_lane_obj): situation_manager = services.get_zone_situation_manager() if not self.bowling_venue.bowling_situations_schedule.entries: return total_bowling_lanes = self.get_total_bowling_lanes() all_weighted_situations = self.bowling_venue.bowling_situations_schedule.get_weighted_situations( ) weighted_situations = [ (weight, situation[0]) for (weight, situation) in all_weighted_situations if situation[0].situation_meets_starting_requirements( total_bowling_lanes) ] if not weighted_situations: return situation_to_create = weighted_random_item(weighted_situations) guest_list = situation_to_create.get_predefined_guest_list() if guest_list is None: guest_list = SituationGuestList(invite_only=True) situation_id = situation_manager.create_situation( situation_to_create, guest_list=guest_list, spawn_sims_during_zone_spin_up=True, user_facing=False) self.set_situation_bowling_lane(situation_id, bowling_lane_obj) return situation_id def _get_sorted_moonlight_schedule(self, day): moonlight_schedule = [] for item in self.bowling_venue.moonlight_on_off_schedule: enabled = item.days_of_the_week.get(day, None) if enabled: for (beginning_hour, is_on ) in item.moonlight_on_off_desire_by_time_of_day.items(): moonlight_schedule.append((beginning_hour, is_on)) moonlight_schedule.sort(key=operator.itemgetter(0)) return moonlight_schedule def _moonlight_change(self, alarm_handle=None): if not self.bowling_venue.moonlight_on_off_schedule: return if self._get_desired_is_moonlight_on(): self._activate_moonlight_affordance( self.MOONLIGHT_AFFORDANCES.moonlight_on) else: self._activate_moonlight_affordance( self.MOONLIGHT_AFFORDANCES.moonlight_off) if alarm_handle is not None: time_of_day = services.time_service().sim_now self._moonlight_alarm_handle = alarms.add_alarm( self, self._get_time_span_to_next_moonlight_schedule(time_of_day), self._moonlight_change) def _get_desired_is_moonlight_on(self): if not self.bowling_venue.moonlight_on_off_schedule: return time_of_day = services.time_service().sim_now hour_of_day = time_of_day.hour() day = time_of_day.day() moonlight_schedule = self._get_sorted_moonlight_schedule(day) if not moonlight_schedule: return entry = moonlight_schedule[-1] desire = entry[1] for entry in moonlight_schedule: if entry[0] <= hour_of_day: desire = entry[1] else: break return desire def _get_time_span_to_next_moonlight_schedule(self, time_of_day): if not self.bowling_venue.moonlight_on_off_schedule: return days_to_schedule_ahead = 1 current_day = time_of_day.day() next_day = (current_day + days_to_schedule_ahead) % 7 next_day_sorted_times = self._get_sorted_moonlight_schedule(next_day) if next_day_sorted_times: next_moonlight_hour = next_day_sorted_times[0][0] else: next_moonlight_hour = 0 now = services.time_service().sim_now sorted_times = self._get_sorted_moonlight_schedule(current_day) scheduled_day = int(now.absolute_days()) now_hour = now.hour() for (moonlight_hour, _) in sorted_times: if moonlight_hour > now_hour: next_moonlight_hour = moonlight_hour break else: scheduled_day += 1 future = date_and_time.create_date_and_time(days=scheduled_day, hours=next_moonlight_hour) time_span_until = future - now return time_span_until def _activate_moonlight_affordance(self, affordance): if self._bowling_lanes is None: return moonlight_state = affordance.state_settings.desired_state for bowling_lane in self._bowling_lanes: if moonlight_state == bowling_lane.get_state( moonlight_state.state): continue affordance.target = bowling_lane affordance.state_settings.execute_helper(affordance) affordance.lighting_setting_operation.execute(affordance)
class LightningTuning: ACTIVE_LIGHTNING = TunableTuple( description='\n Active Lightning Tuning\n ', weights=TunableTuple( description= '\n Weights for striking various objects.\n ', weight_terrain=TunableRange( description= '\n Weighted chance of striking terrain versus other locations.\n ', tunable_type=float, default=1.0, minimum=0.0), weight_object=TunableRange( description= '\n Weighted chance of striking non-Sim objects versus other locations.\n ', tunable_type=float, default=1.0, minimum=0.0), weight_sim=TunableRange( description= '\n Weighted chance of striking Sims versus other locations.\n ', tunable_type=float, default=1.0, minimum=0.0))) STRIKE_TERRAIN_TUNING = TunableTuple( description= '\n Tuning for when we want a lightning bolt to strike the ground.\n ', effect_off_lot=PlayEffect.TunableFactory( description= '\n The effect we want to spawn at the terrain location if it is off\n lot.\n ' ), effect_on_lot=PlayEffect.TunableFactory( description= "\n The effect we want to spawn at the object's location if it is on\n lot. This will also have a scorch mark associated with it.\n " ), scorch_mark_delay=TunableRealSecond( description= '\n The delay, in real seconds, before we place a scorch mark for on-\n lot lightning strikes.\n ', default=0), create_object_tuning=TunableTuple( description= '\n Tuning related to creating objects when lightning strikes the\n ground.\n ', chance=TunablePercent( description= '\n Chance to spawn one of the objects tuned here when lightning\n strikes the terrain.\n ', default=10), definition_weights=TunableList( description= '\n List of definitions and their weighted chance of being created\n at the location of the lightning strike.\n ', tunable=TunableTuple( description= '\n The object definition and weighted chance of it being\n created.\n ', weight=TunableRange( description= '\n The weighted chance of creating this object.\n ', tunable_type=float, default=1.0, minimum=0.0), definition=TunableReference( description= '\n The object we want to create at the strike location.\n ', manager=services.definition_manager())))), broadcaster=BroadcasterRequest.TunableFactory( description= '\n The broadcaster we want to fire when a lightning bolt strikes the\n terrain.\n ' )) STRIKE_OBJECT_TUNING = TunableTuple( description= "\n Tuning for when we want a lightning bolt to strike an object.\n \n For an object to be considered for a lightning strike, it must have one\n of the tags tuned here. We will increase its chance based on lightning\n multiplier tuning on it's Weather Aware Component if it has one, and\n apply both the generic loot tuned here, as well as any loot that is\n registered for Struck By Lightning.\n ", effect=PlayEffect.TunableFactory( description= "\n The effect we want to spawn at the object's location.\n " ), scorch_mark_delay=TunableRealSecond( description= '\n The delay, in real seconds, before we place a scorch mark for on-\n lot lightning strikes.\n ', default=0), generic_loot_on_strike=TunableList( description= '\n Loot to apply to all objects when struck by lightning.\n \n Objects that have a weather aware component can tune loot when\n listening for Struck By Lightning.\n ', tunable=TunableReference( description= '\n A loot action to apply to the object struck by lightning.\n ', manager=services.get_instance_manager( sims4.resources.Types.ACTION))), tags=TunableTags( description= '\n A set of tags that determine if an object can be struck by\n lightning. Each object has a weight of 1 to be struck by lightning,\n but can be multiplied in the weather aware component to give\n preference to electronics, etc.\n ' ), broadcaster=BroadcasterRequest.TunableFactory( description= '\n The broadcaster we want to fire when a lightning bolt strikes an\n object.\n ' )) STRIKE_SIM_TUNING = TunableTuple( description= '\n Tuning for when we want a lightning bolt to strike a Sim.\n ', affordance=TunablePackSafeReference( description= '\n The interaction to push on a Sim that is struck by lightning.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION)))
class UniversityCourseData(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(Types.UNIVERSITY_COURSE_DATA)): INSTANCE_TUNABLES = {'spawn_point_tag': TunableMapping(description='\n University specific spawn point tags.\n Used by course related interactions to determine which spawn\n point to use for the constraint. (i.e. the one in front of the\n appropriate building)\n ', key_type=TunableReference(manager=services.get_instance_manager(Types.UNIVERSITY)), value_type=TunableSet(tunable=TunableEnumWithFilter(tunable_type=Tag, default=Tag.INVALID, filter_prefixes=('Spawn',)), minlength=1)), 'classroom_tag': TunableMapping(description='\n University specific classroom tags.\n Used by university interactions on shells to determine which building\n shell should have the interaction(s) available.\n ', key_type=TunableReference(manager=services.get_instance_manager(Types.UNIVERSITY)), value_type=TunableSet(tunable=TunableEnumEntry(tunable_type=Tag, default=Tag.INVALID), minlength=1)), 'university_course_mapping': TunableMapping(description='\n University specific course name and description.\n Each university can have its own course name and description\n defined.\n ', key_type=TunableReference(manager=services.get_instance_manager(Types.UNIVERSITY)), value_type=TunableTuple(course_name=TunableLocalizedStringFactory(description='\n The name of this course.\n '), course_description=TunableLocalizedString(description='\n A description for this course.\n ', allow_none=True), export_class_name='UniversityCourseDisplayData'), tuple_name='UniversityCourseDataMapping', export_modes=ExportModes.All), 'course_skill_data': TunableTuple(description='\n The related skill data for this specific course. Whenever a Sim \n does something that increases their course grade performance (like\n attending lecture or studying), this skill will also increase by\n the tunable amount. Likewise, whenever this related skill \n increases, the course grade will also increase.\n ', related_skill=OptionalTunable(description='\n The related skill associated with this course.\n ', tunable=TunablePackSafeReference(manager=services.get_instance_manager(Types.STATISTIC), class_restrictions=('Skill',)))), 'icon': TunableIcon(description='\n Icon for this university course.\n ', export_modes=ExportModes.All, allow_none=True), 'cost': TunableRange(description='\n The cost of this course.\n ', tunable_type=int, default=200, minimum=0, export_modes=ExportModes.All), 'course_tags': TunableTags(description='\n The tag for this course. Used for objects that may be shared \n between courses.\n ', filter_prefixes=['course']), 'final_requirement_type': TunableEnumEntry(description='\n The final requirement for this course. This requirement must be \n completed before the course can be considered complete.\n ', tunable_type=FinalCourseRequirement, default=FinalCourseRequirement.NONE), 'final_requirement_aspiration': TunableReference(description='\n An aspiration to use for tracking the final course requirement. \n ', manager=services.get_instance_manager(sims4.resources.Types.ASPIRATION), class_restrictions='AspirationAssignment', allow_none=True), 'professor_assignment_trait': TunableMapping(description='\n A mapping of University -> professor assignment trait.\n \n This is needed because each of the universities shipped with EP08\n use the exact same classes but we want different teachers for each\n university.\n ', key_type=TunableReference(description='\n A reference to the University that the professor will belong to.\n ', manager=services.get_instance_manager(sims4.resources.Types.UNIVERSITY)), value_type=TunableReference(description='\n The trait used to identify the professor for this course.\n ', manager=services.get_instance_manager(sims4.resources.Types.TRAIT)))} @classproperty def is_elective(cls): return any(cls is e.elective for e in University.COURSE_ELECTIVES.electives)
class ObstacleCourseSituation(SituationComplexCommon): INSTANCE_TUNABLES = { 'coach_job_and_role_state': TunableSituationJobAndRoleState( description= '\n Job and Role State for the coach Sim. Pre-populated as\n the actor of the Situation.\n ', tuning_group=GroupNames.ROLES), 'athlete_job_and_role_state': TunableSituationJobAndRoleState( description= '\n Job and Role State for the athlete. Pre-populated as the\n target of the Situation.\n ', tuning_group=GroupNames.ROLES), 'run_course_state': RunCourseState.TunableFactory(tuning_group=GroupNames.STATE), 'obstacle_tags': TunableTags( description= '\n Tags to use when searching for obstacle course objects.\n ', filter_prefixes=('Func_PetObstacleCourse', ), minlength=1), 'setup_obstacle_state_value': ObjectStateValue.TunableReference( description= '\n The state to setup obstacles before we run the course.\n ' ), 'teardown_obstacle_state_value': ObjectStateValue.TunableReference( description= '\n The state to teardown obstacles after we run the course or when the\n situation ends.\n ' ), 'failure_commodity': Commodity.TunableReference( description= '\n The commodity we use to track how many times the athlete has failed\n to overcome an obstacle.\n ' ), 'obstacles_required': TunableRange( description= '\n The number of obstacles required for the situation to be available. \n If the obstacles that the pet can route to drops below this number,\n the situation is destroyed.\n ', tunable_type=int, default=4, minimum=1), 'unfinished_notification': UiDialogNotification.TunableFactory( description= '\n The dialog for when the situation ends prematurely or the dog never\n finishes the course.\n Token 0: Athlete\n Token 1: Coach\n Token 2: Time\n ', tuning_group=GroupNames.UI), 'finish_notifications': TunableList( description= '\n A list of thresholds and notifications to play given the outcome of\n the course. We run through the thresholds until one passes, and\n play the corresponding notification.\n ', tuning_group=GroupNames.UI, tunable=TunableTuple( description= '\n A threshold and notification to play if the threshold passes.\n ', threshold=TunableThreshold( description= '\n A threshold to compare the number of failures from the\n failure commodity when the course is finished.\n ' ), notification=UiDialogNotification.TunableFactory( description= '\n Notification to play when the situation ends.\n Token 0: Athlete\n Token 1: Coach\n Token 2: Failure Count\n Token 3: Time\n ' ))) } @classmethod def _states(cls): return (SituationStateData(0, WaitForSimJobsState), SituationStateData(1, RunCourseState, factory=cls.run_course_state)) @classmethod def _get_tuned_job_and_default_role_state_tuples(cls): return [(cls.coach_job_and_role_state.job, cls.coach_job_and_role_state.role_state), (cls.athlete_job_and_role_state.job, cls.athlete_job_and_role_state.role_state)] @classmethod def default_job(cls): pass @classmethod def get_prepopulated_job_for_sims(cls, sim, target_sim_id=None): prepopulate = [(sim.id, cls.coach_job_and_role_state.job.guid64)] if target_sim_id is not None: prepopulate.append( (target_sim_id, cls.athlete_job_and_role_state.job.guid64)) return prepopulate @classmethod def get_obstacles(cls): object_manager = services.object_manager() found_objects = set() for tag in cls.obstacle_tags: found_objects.update( object_manager.get_objects_matching_tags({tag})) return found_objects @classmethod def is_situation_available(cls, *args, **kwargs): obstacles = cls.get_obstacles() if len(obstacles) < cls.obstacles_required: return TestResult(False, 'Not enough obstacles.') return super().is_situation_available(*args, **kwargs) @classproperty def situation_serialization_option(cls): return situations.situation_types.SituationSerializationOption.LOT def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) reader = self._seed.custom_init_params_reader if reader is not None: obstacles = self.get_obstacles() if not obstacles: self._self_destruct() self._obstacle_ids = {obstacle.id for obstacle in obstacles} self._course_start_time = DateAndTime( reader.read_uint64(OBSTACLE_COURSE_START_TIME_TOKEN, services.time_service().sim_now)) self._course_end_time = DateAndTime( reader.read_uint64(OBSTACLE_COURSE_END_TIME_TOKEN, services.time_service().sim_now)) else: self._obstacle_ids = set() self._course_start_time = None self._course_end_time = None self._course_progress = ObstacleCourseProgress.NOT_STARTED @property def course_progress(self): return self._course_progress @property def obstacle_ids(self): return self._obstacle_ids def _save_custom_situation(self, writer): super()._save_custom_situation(writer) if self._course_start_time is not None: writer.write_uint64(OBSTACLE_COURSE_START_TIME_TOKEN, int(self._course_start_time)) if self._course_end_time is not None: writer.write_uint64(OBSTACLE_COURSE_END_TIME_TOKEN, int(self._course_end_time)) def start_situation(self): super().start_situation() self._register_obstacle_course_events() self._change_state(WaitForSimJobsState()) def _on_remove_sim_from_situation(self, sim): super()._on_remove_sim_from_situation(sim) self._self_destruct() def _on_add_sim_to_situation(self, sim, job_type, *args, **kwargs): super()._on_add_sim_to_situation(sim, job_type, *args, **kwargs) if self.get_coach() is not None and self.get_athlete() is not None: object_manager = services.object_manager() obstacles = { object_manager.get(obstacle_id) for obstacle_id in self._obstacle_ids } sim_info_manager = services.sim_info_manager() users = sim_info_manager.instanced_sims_gen() for user in users: if user in self._situation_sims: continue for interaction in user.get_all_running_and_queued_interactions( ): target = interaction.target target = target.part_owner if target is not None and target.is_part else target if target is not None and target in obstacles: interaction.cancel( FinishingType.SITUATIONS, cancel_reason_msg='Obstacle Course Starting') self._change_state(self.run_course_state()) def _register_obstacle_course_events(self): services.get_event_manager().register_single_event( self, TestEvent.ObjectDestroyed) services.get_event_manager().register_single_event( self, TestEvent.OnExitBuildBuy) def _unregister_obstacle_course_events(self): services.get_event_manager().unregister_single_event( self, TestEvent.ObjectDestroyed) services.get_event_manager().unregister_single_event( self, TestEvent.OnExitBuildBuy) def handle_event(self, sim_info, event, resolver): super().handle_event(sim_info, event, resolver) if event == TestEvent.ObjectDestroyed: destroyed_object = resolver.get_resolved_arg('obj') if destroyed_object.id in self._obstacle_ids: self._obstacle_ids.remove(destroyed_object.id) if len(self._obstacle_ids) < self.obstacles_required: self._self_destruct() elif event == TestEvent.OnExitBuildBuy: self.validate_obstacle_course() def on_remove(self): coach = self.get_coach() athlete = self.get_athlete() if coach is not None and athlete is not None: if self.course_progress > ObstacleCourseProgress.NOT_STARTED and self.course_progress < ObstacleCourseProgress.FINISHED: course_end_time = services.time_service().sim_now course_time_span = course_end_time - self._course_start_time unfinished_dialog = self.unfinished_notification(coach) unfinished_dialog.show_dialog( additional_tokens=(athlete, coach, course_time_span)) athlete.commodity_tracker.remove_statistic(self.failure_commodity) self.teardown_obstacle_course() self._unregister_obstacle_course_events() super().on_remove() def start_course(self): self._course_progress = ObstacleCourseProgress.RUNNING self._course_start_time = services.time_service( ).sim_now if self._course_start_time is None else self._course_start_time def continue_course(self): self._change_state(self.run_course_state()) def finish_course(self): self._course_end_time = services.time_service().sim_now self._course_progress = ObstacleCourseProgress.FINISHED self._change_state(self.run_course_state()) def finish_situation(self): course_time_span = self._course_end_time - self._course_start_time athlete = self.get_athlete() coach = self.get_coach() failures = athlete.commodity_tracker.get_value(self.failure_commodity) for threshold_notification in self.finish_notifications: if threshold_notification.threshold.compare(failures): dialog = threshold_notification.notification(coach) dialog.show_dialog(additional_tokens=(athlete, coach, failures, course_time_span)) break else: logger.error( "Obstacle Course Situation doesn't have a threshold, notification for failure count of {}", failures) self._self_destruct() def setup_obstacle_course(self): obstacles = self.get_obstacles() if len(obstacles) < self.obstacles_required: self._self_destruct() self._obstacle_ids = {obstacle.id for obstacle in obstacles} def validate_obstacle_course(self): athlete = self.get_athlete() if athlete is None: self._self_destruct() return all_obstacles = self.get_obstacles() if len(all_obstacles) < self.obstacles_required: self._self_destruct() return valid_obstacles = set() for obstacle in all_obstacles: currentState = obstacle.get_state( self.setup_obstacle_state_value.state) if obstacle.is_connected(athlete): valid_obstacles.add(obstacle) if currentState == self.teardown_obstacle_state_value: obstacle.set_state(self.setup_obstacle_state_value.state, self.setup_obstacle_state_value, immediate=True) if currentState == self.setup_obstacle_state_value: obstacle.set_state( self.setup_obstacle_state_value.state, self.teardown_obstacle_state_value, immediate=True) elif currentState == self.setup_obstacle_state_value: obstacle.set_state(self.setup_obstacle_state_value.state, self.teardown_obstacle_state_value, immediate=True) if len(valid_obstacles) < self.obstacles_required: self._self_destruct() else: self._obstacle_ids = {obstacle.id for obstacle in valid_obstacles} def teardown_obstacle_course(self): obstacles = self.get_obstacles() for obstacle in obstacles: obstacle.set_state(self.teardown_obstacle_state_value.state, self.teardown_obstacle_state_value, immediate=True) def get_coach(self): return next( iter(self.all_sims_in_job_gen(self.coach_job_and_role_state.job)), None) def get_athlete(self): return next( iter(self.all_sims_in_job_gen( self.athlete_job_and_role_state.job)), None)
class _RuleOutTreatmentThresholdAction(HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = {'rule_out_reason': OptionalTunable(description='\n The reason based on which treatments are ruled out.\n \n By default, it will rule out treatments that contain any of the\n interaction category tags of the exam that was performed. This can\n be overridden to rule out treatments with specific tags.\n ', tunable=TunableTags(description='\n Only rule out treatments with one of the specified tags.\n '), disabled_name='interaction_tags', enabled_name='specified_tags')} def perform(self, patient_sim, interaction=None): sickness = patient_sim.current_sickness if sickness is None: return applicable = set(itertools.chain(sickness.available_treatments, *sickness.available_treatment_lists)) ruled_out = set(itertools.chain(patient_sim.sickness_tracker.treatments_performed, patient_sim.sickness_tracker.ruled_out_treatments)) correct = set(sickness.correct_treatments) available_for_ruling_out = tuple(applicable - ruled_out - correct) if self.rule_out_reason is None and interaction is not None: interaction_tags = SicknessTuning.EXAM_TYPES_TAGS & interaction.interaction_category_tags available_for_ruling_out = tuple(treatment for treatment in available_for_ruling_out if interaction_tags & treatment.interaction_category_tags) elif self.rule_out_reason is not None: available_for_ruling_out = tuple(treatment for treatment in available_for_ruling_out if self.rule_out_reason & treatment.interaction_category_tags) if not available_for_ruling_out: return to_rule_out = random.choice(available_for_ruling_out) patient_sim.rule_out_treatment(to_rule_out) services.get_sickness_service().add_sickness_event(patient_sim.sim_info, patient_sim.current_sickness, 'Ruled out {}'.format(to_rule_out.__name__))
class WalkDogSituation(SituationComplexCommon): INSTANCE_TUNABLES = { 'walker_job_and_role_state': TunableSituationJobAndRoleState( description= '\n Job and Role State for the Sim walking the dog. Pre-populated as\n the actor of the Situation.\n ' ), 'dog_job_and_role_state': TunableSituationJobAndRoleState( description= '\n Job and Role State for the dog being walked. Pre-populated as the\n target of the Situation.\n ' ), 'walk_nodes': TunableInterval( description= '\n How many nodes in the world we want to traverse for our walk.\n Currently this will only affect fallback attractor points. We will\n try to use ALL of the attractor points returned by search tags.\n ', tunable_type=int, default_lower=5, default_upper=6, minimum=1), 'finish_walk_state': FinishWalkState.TunableFactory(tuning_group=GroupNames.STATE), 'walk_state': WalkState.TunableFactory(tuning_group=GroupNames.STATE), 'wait_around_state': WaitAroundState.TunableFactory(tuning_group=GroupNames.STATE), 'attractor_point_tags': TunableTuple( description= '\n Tags that are used to select objects and attractor points for our\n path.\n ', fallback_tags=TunableTags( description= "\n Tags to use if we don't find any objects with the search tags.\n This is primarily so we can have a separate list for pre-\n patched worlds where there are no hand-placed attractor points.\n ", filter_prefixes=('AtPo', ), minlength=1), search_tags=TunableList( description= "\n A list of path tags to look for in order. This will search for\n objects with each tag, find the closest object, and use it's\n matching tag to find others for a full path. \n \n Example: Short_1, Short_2 are in the list. We would search for \n all objects with either of those tags, and grab the closest \n one. If the object has Short_1 tag on it, we find all objects \n with Short_1 to create our path.\n ", tunable=TunableEnumWithFilter( description= '\n A set of attractor point tags we use to pull objects from when\n searching for attractor points to create a walking path from.\n ', tunable_type=Tag, default=Tag.INVALID, invalid_enums=(Tag.INVALID, ), filter_prefixes=('AtPo', )), unique_entries=True)) } REMOVE_INSTANCE_TUNABLES = Situation.NON_USER_FACING_REMOVE_INSTANCE_TUNABLES @classmethod def _verify_tuning_callback(cls): super()._verify_tuning_callback() if cls.attractor_point_tags.fallback_tags.issubset( cls.attractor_point_tags.search_tags): logger.error( 'Walk Dog Situation {} fallback tags are a subset of search tags. You need at least one tag to be different in fallback tags.', cls) @classmethod def _states(cls): return (SituationStateData(0, WalkState), SituationStateData(1, WaitAroundState), SituationStateData(2, FinishWalkState)) @classmethod def _get_tuned_job_and_default_role_state_tuples(cls): return [(cls.walker_job_and_role_state.job, cls.walker_job_and_role_state.role_state), (cls.dog_job_and_role_state.job, cls.dog_job_and_role_state.role_state)] @classmethod def default_job(cls): pass @classmethod def get_prepopulated_job_for_sims(cls, sim, target_sim_id=None): prepopulate = [(sim.id, cls.walker_job_and_role_state.job.guid64)] if target_sim_id is not None: prepopulate.append( (target_sim_id, cls.dog_job_and_role_state.job.guid64)) return prepopulate @classmethod def has_walk_nodes(cls): object_manager = services.object_manager() found_objects = object_manager.get_objects_matching_tags( set(cls.attractor_point_tags.search_tags) | cls.attractor_point_tags.fallback_tags, match_any=True) if found_objects: return True return False @classmethod def get_walk_nodes(cls): object_manager = services.object_manager() def get_objects(tag_set): found_objects = set() for tag in tag_set: found_objects.update( object_manager.get_objects_matching_tags({tag})) return found_objects attractor_objects = get_objects(cls.attractor_point_tags.search_tags) if not attractor_objects: return (get_objects(cls.attractor_point_tags.fallback_tags), True) return (attractor_objects, False) @classmethod def is_situation_available(cls, *args, **kwargs): result = cls.has_walk_nodes() if not result: return TestResult(False, 'Not enough attractor points to walk the dog.') return super().is_situation_available(*args, **kwargs) @classproperty def situation_serialization_option(cls): return situations.situation_types.SituationSerializationOption.DONT def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._walker = None self._dog = None self._path_index = 0 self._path_obj_ids = [] self.walk_dog_progress = WalkDogProgress.WALK_DOG_NOT_STARTED def _on_remove_sim_from_situation(self, sim): if sim is self.get_walker() or sim is self.get_pet(): self._self_destruct() super()._on_remove_sim_from_situation def _on_add_sim_to_situation(self, *args, **kwargs): super()._on_add_sim_to_situation(*args, **kwargs) if self.get_walker() is not None and self.get_pet() is not None: self._build_walking_path() if not self._path_obj_ids: self._self_destruct() return self.walk_onward() def get_walker(self): if self._walker is None: self._walker = next( iter( self.all_sims_in_job_gen( self.walker_job_and_role_state.job)), None) return self._walker def get_pet(self): if self._dog is None: self._dog = next( iter(self.all_sims_in_job_gen( self.dog_job_and_role_state.job)), None) return self._dog def _build_walking_path(self): (attractor_objects, is_fallback) = self.get_walk_nodes() if not attractor_objects: logger.warn('Could not build a path for {}', self) return sim = self.get_walker() or self.get_pet() sim_position = sim.position all_obj_and_pos_list = [(obj, obj.position) for obj in attractor_objects] min_dist_obj = min(all_obj_and_pos_list, key=lambda k: (k[1] - sim_position).magnitude_2d_squared())[0] obj_and_pos_list = [] if not is_fallback: tags = min_dist_obj.get_tags() matching_tags = { tag for tag in self.attractor_point_tags.search_tags if tag in tags } for obj_pos in all_obj_and_pos_list: if obj_pos[0].has_any_tag(matching_tags): obj_and_pos_list.append(obj_pos) else: obj_and_pos_list = all_obj_and_pos_list positions = [item[1] for item in obj_and_pos_list] center = sum(positions, sims4.math.Vector3.ZERO()) / len(positions) obj_and_pos_list.sort(key=lambda k: sims4.math.atan2( k[1].x - center.x, k[1].z - center.z), reverse=True) start_index = 0 for (obj, _) in obj_and_pos_list: if obj is min_dist_obj: break start_index += 1 if not is_fallback: num_nodes = len(obj_and_pos_list) elif self.walk_nodes.lower_bound == self.walk_nodes.upper_bound: num_nodes = self.walk_nodes.lower_bound else: num_nodes = random.randrange(self.walk_nodes.lower_bound, self.walk_nodes.upper_bound) clockwise = 1 if random.randint(2, 4) % 2 else -1 index = start_index for _ in range(num_nodes): if index >= len(obj_and_pos_list): index = 0 elif index < 0: index = len(obj_and_pos_list) - 1 (node, _) = obj_and_pos_list[index] self._path_obj_ids.append(node.id) index += clockwise if self._path_obj_ids[-1] != min_dist_obj.id: self._path_obj_ids.append(min_dist_obj.id) def walk_onward(self): if self._path_index < len(self._path_obj_ids): self.walk_dog_progress = WalkDogProgress.WALK_DOG_WALKING self._change_state( self.walk_state(self._path_obj_ids[self._path_index])) self._path_index += 1 return if self.walk_dog_progress == WalkDogProgress.WALK_DOG_WALKING: self.walk_dog_progress = WalkDogProgress.WALK_DOG_FINISHING self._change_state(self.finish_walk_state()) return elif self.walk_dog_progress >= WalkDogProgress.WALK_DOG_FINISHING: self.walk_dog_progress = WalkDogProgress.WALK_DOG_DONE self._self_destruct() return def wait_around(self, attractor_point): self._change_state(self.wait_around_state())
class HolidayVisitorNPCSituation(WalkbyLimitingTagsMixin, SituationComplexCommon): INSTANCE_TUNABLES = { 'holiday_visitor_npc_job': sims4.tuning.tunable.TunableTuple( situation_job=SituationJob.TunableReference( description= '\n A reference to the SituationJob used for the Sim performing the\n holiday visitor situation.\n ' ), arrival_state=_ArrivalState.TunableFactory( description= '\n The state for pushing the NPC onto the lot.\n ' ), hang_out_state=_HangOutState.TunableFactory( description= '\n State where they hang out using role autonomy (if we want\n them to eat cookies). The interaction of interest should be them\n leaving at the fireplace.\n ' ), push_interaction_state=_PushInteractionState.TunableFactory( description= '\n The state for pushing the NPC to do an interaction on\n one of the primary targets\n ' ), leave_state=_LeaveState.TunableFactory( description= '\n The state for pushing the NPC to leave.\n ' ), tuning_group=GroupNames.SITUATION), 'target_filter_tags': OptionalTunable( description= '\n Choose what kind of targets to grab. If\n turned on, use tags. Otherwise, use \n household sims.\n ', tunable=TunableTags( description= '\n Define tags we want to filter by.\n ', minlength=1), disabled_name='use_household_sims', enabled_name='use_tags') } REMOVE_INSTANCE_TUNABLES = Situation.NON_USER_FACING_REMOVE_INSTANCE_TUNABLES def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.selected_target = None reader = self._seed.custom_init_params_reader if reader is not None: selected_target_id = reader.read_uint64(INTERACTION_TARGET_TOKEN, 0) object_manager = services.object_manager() self.selected_target = object_manager.get(selected_target_id) @classmethod def _states(cls): return ( SituationStateData( 1, _ArrivalState, factory=cls.holiday_visitor_npc_job.arrival_state), SituationStateData( 2, _PushInteractionState, factory=cls.holiday_visitor_npc_job.push_interaction_state), SituationStateData( 3, _HangOutState, factory=cls.holiday_visitor_npc_job.hang_out_state), SituationStateData( 4, _LeaveState, factory=cls.holiday_visitor_npc_job.leave_state)) @classmethod def _get_tuned_job_and_default_role_state_tuples(cls): return [(cls.holiday_visitor_npc_job.situation_job, cls.holiday_visitor_npc_job.arrival_state)] @classmethod def default_job(cls): pass @classmethod def get_sims_expected_to_be_in_situation(cls): return 1 def _save_custom_situation(self, writer): super()._save_custom_situation(writer) if self.selected_target is not None: writer.write_uint64(INTERACTION_TARGET_TOKEN, int(self.selected_target.id)) def holiday_visitor_npc(self): sim = next( self.all_sims_in_job_gen( self.holiday_visitor_npc_job.situation_job), None) return sim def get_random_target(self): object_manager = services.object_manager() if self.target_filter_tags is not None: found_objects = object_manager.get_objects_matching_tags( self.target_filter_tags, match_any=True) if len(found_objects) > 0: random_object = random.choice(list(found_objects)) return random_object return else: household_sims = services.active_household().instanced_sims_gen() random_sim = random.choice(list(household_sims)) return random_sim def start_situation(self): super().start_situation() if self.selected_target is None: self.selected_target = self.get_random_target() self._change_state(self.holiday_visitor_npc_job.arrival_state())
class PortalComponent(Component, HasTunableFactory, AutoFactoryInit, component_name=types.PORTAL_COMPONENT): PORTAL_DIRECTION_THERE = 0 PORTAL_DIRECTION_BACK = 1 PORTAL_LOCATION_ENTRY = 0 PORTAL_LOCATION_EXIT = 1 FACTORY_TUNABLES = { '_portal_data': TunableList( description= '\n The portals that are to be created for this object.\n ', tunable=TunablePortalReference(pack_safe=True)), 'state_values_which_disable_portals': TunableMapping( description= '\n A mapping between object state values and portals which should be\n disabled when those state values are active. Disabling a portal\n requires a full refresh of the owning objects portals.\n ', key_type=TunableStateValueReference(pack_safe=True), value_type=TunableList(tunable=TunablePortalReference( pack_safe=True))), '_portal_animation_component': OptionalTunable( description= '\n If enabled, this portal animates in response to agents traversing\n it. Use Enter/Exit events to control when and for how long an\n animation plays.\n ', tunable=PortalAnimationComponent.TunableFactory()), '_portal_locking_component': OptionalTunable( description= '\n If enabled then this object will be capable of being locked using\n the same system as Portal Objects.\n \n If not enabled then it will not have a portal locking component\n and will therefore not be lockable.\n ', tunable=PortalLockingComponent.TunableFactory()), '_portal_disallowed_tags': TunableTags( description= '\n A set of tags used to prevent Sims in particular role states from\n using this portal.\n ', filter_prefixes=tag.PORTAL_DISALLOWANCE_PREFIX) } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._portals = {} self._custom_portals = None self._enable_refresh = True def get_subcomponents_gen(self): yield from super().get_subcomponents_gen() if self._portal_locking_component is not None: portal_locking_component = self._portal_locking_component( self.owner) yield from portal_locking_component.get_subcomponents_gen() if self._portal_animation_component is not None: portal_animation_component = self._portal_animation_component( self.owner) yield from portal_animation_component.get_subcomponents_gen() @property def refresh_enabled(self): return self._enable_refresh @refresh_enabled.setter def refresh_enabled(self, value): self._enable_refresh = bool(value) def on_buildbuy_exit(self, *_, **__): self._refresh_portals() def on_location_changed(self, *_, **__): zone = services.current_zone() if zone.is_in_build_buy or zone.is_zone_loading: return self._refresh_portals() def finalize_portals(self): self._refresh_portals() def _refresh_portals(self): if self.refresh_enabled: self._remove_portals() self._add_portals() self.owner.refresh_locks() def on_add(self, *_, **__): services.object_manager().add_portal_to_cache(self.owner) if len(self.state_values_which_disable_portals) > 0: self.owner.add_state_changed_callback( self._on_state_changed_callback) def on_remove(self, *_, **__): self._remove_portals() services.object_manager().remove_portal_from_cache(self.owner) @componentmethod @sims4.utils.exception_protected(default_return=0) def c_api_get_portal_duration(self, portal_id, walkstyle, age, gender, species): portal = self._portals.get(portal_id) if portal is not None: return portal.get_portal_duration(portal_id, walkstyle, age, gender, species) return 0 @componentmethod def add_portal_data(self, portal_id, actor, walkstyle): portal = self._portals.get(portal_id) if portal is not None: return portal.add_portal_data(portal_id, actor, walkstyle) @componentmethod def split_path_on_portal(self, portal_id): portal = self._portals.get(portal_id) if portal is not None: return portal.split_path_on_portal() return PathSplitType.PathSplitType_DontSplit @componentmethod def get_posture_change(self, portal_id, initial_posture): portal = self._portals.get(portal_id) if portal is not None: return portal.get_posture_change(portal_id, initial_posture) return (initial_posture, initial_posture) @componentmethod def provide_route_events(self, portal_id, route_event_context, sim, path, **kwargs): if portal_id in self._portals: portal = self._portals.get(portal_id) return portal.provide_route_events(portal_id, route_event_context, sim, path, **kwargs) @componentmethod def add_portal_events(self, portal_id, actor, time, route_pb): portal = self._portals.get(portal_id) if portal is not None: portal.traversal_type.add_portal_events(portal_id, actor, self.owner, time, route_pb) portal.traversal_type.notify_in_use(actor, portal, self.owner) @componentmethod def get_portal_asm_params(self, portal_id, sim): portal = self._portals.get(portal_id) if portal is not None: return portal.get_portal_asm_params(portal_id, sim) return {} @componentmethod def get_portal_owner(self, portal_id): portal = self._portals.get(portal_id) if portal is not None: return portal.obj return self.owner @componentmethod def get_target_surface(self, portal_id): portal = self._portals.get(portal_id) if portal is not None: return portal.get_target_surface(portal_id) return self.owner.routing_surface def _add_portals(self): disabled_portals = set() if self.state_values_which_disable_portals: for (state_value, portals) in self.state_values_which_disable_portals.items(): if self.owner.state_value_active(state_value): disabled_portals.update(portals) for portal_data in self._portal_data: if portal_data not in disabled_portals: self._add_portal_internal(self.owner, portal_data) if self.owner.parts is not None: for part in self.owner.parts: part_definition = part.part_definition for portal_data in part_definition.portal_data: if portal_data not in disabled_portals: self._add_portal_internal(part, portal_data) if self._custom_portals is not None: for (location_point, portal_data, mask, _) in self._custom_portals: self._add_portal_internal(location_point, portal_data, mask) def _add_portal_internal(self, obj, portal_data, portal_creation_mask=None): portal_instance_ids = [] for portal in portal_data.get_portal_instances(obj, portal_creation_mask): if portal.there is not None: self._portals[portal.there] = portal portal_instance_ids.append(portal.there) if portal.back is not None: self._portals[portal.back] = portal portal_instance_ids.append(portal.back) return portal_instance_ids def _remove_portal_internal(self, portal_id): if portal_id in self._portals: remove_portal(portal_id) portal = self._portals[portal_id] if portal.there is not None and portal.there == portal_id: portal.there = None elif portal.back is not None: if portal.back == portal_id: portal.back = None del self._portals[portal_id] def _remove_portals(self): for portal_id in self._portals: remove_portal(portal_id) self._portals.clear() if self._custom_portals is not None: self._custom_portals.clear() self._custom_portals = None @componentmethod_with_fallback(lambda *_, **__: False) def has_portals(self, check_parts=True): if self._portal_data or self._custom_portals: return True elif check_parts and self.owner.parts is not None: return any(part.part_definition is not None and part.part_definition.portal_data is not None for part in self.owner.parts) return False @componentmethod_with_fallback(lambda *_, **__: []) def get_portal_pairs(self): return set( _PortalPair(portal.there, portal.back) for portal in self._portals.values()) @componentmethod_with_fallback(lambda *_, **__: None) def get_portal_data(self): return self._portal_data @componentmethod def get_portal_instances(self): return frozenset(self._portals.values()) @componentmethod def get_portal_type(self, portal_id): portal = self._portals.get(portal_id) if portal is not None: return portal.portal_type return PortalType.PortalType_Animate @componentmethod def update_portal_cache(self, portal, portal_id): self._portals[portal_id] = portal @componentmethod_with_fallback(lambda *_, **__: None) def get_portal_by_id(self, portal_id): return self._portals.get(portal_id, None) @componentmethod_with_fallback(lambda *_, **__: ()) def get_dynamic_portal_locations_gen(self): for portal_data in self._portal_data: yield from portal_data.get_dynamic_portal_locations_gen(self.owner) @componentmethod def get_single_portal_locations(self): portal_pair = next(iter(self._portals.values()), None) if portal_pair is not None: portal_there = self.get_portal_by_id(portal_pair.there) portal_back = self.get_portal_by_id(portal_pair.back) front_location = None if portal_there is not None: front_location = portal_there.there_entry back_location = None if portal_back is not None: back_location = portal_back.back_entry return (front_location, back_location) return (None, None) @componentmethod def set_portal_cost_override(self, portal_id, cost, sim=None): portal = self._portals.get(portal_id) if portal is not None: portal.set_portal_cost_override(cost, sim=sim) @componentmethod def get_portal_cost(self, portal_id): portal = self._portals.get(portal_id) if portal is not None: return portal.get_portal_cost(portal_id) @componentmethod def get_portal_cost_override(self, portal_id): portal = self._portals.get(portal_id) if portal is not None: return portal.get_portal_cost_override() @componentmethod_with_fallback(lambda *_, **__: True) def lock_portal_on_use(self, portal_id): portal = self._portals.get(portal_id) if portal is not None: return portal.lock_portal_on_use return True @componentmethod def clear_portal_cost_override(self, portal_id, sim=None): portal = self._portals.get(portal_id) if portal is not None: portal.clear_portal_cost_override(sim=sim) @componentmethod def is_ungreeted_sim_disallowed(self): return any(p.is_ungreeted_sim_disallowed() for p in self._portals.values()) @componentmethod def get_portal_disallowed_tags(self): return self._portal_disallowed_tags @componentmethod def get_entry_clothing_change(self, interaction, portal_id, **kwargs): portal = self._portals.get(portal_id) if portal is not None: return portal.get_entry_clothing_change(interaction, portal_id, **kwargs) @componentmethod def get_exit_clothing_change(self, interaction, portal_id, **kwargs): portal = self._portals.get(portal_id) if portal is not None: return portal.get_exit_clothing_change(interaction, portal_id, **kwargs) @componentmethod def get_on_entry_outfit(self, interaction, portal_id, **kwargs): portal = self._portals.get(portal_id) if portal is not None: return portal.get_on_entry_outfit(interaction, portal_id, **kwargs) @componentmethod def get_on_exit_outfit(self, interaction, portal_id, **kwargs): portal = self._portals.get(portal_id) if portal is not None: return portal.get_on_exit_outfit(interaction, portal_id, **kwargs) @componentmethod def get_gsi_portal_items_list(self, key_name, value_name): gsi_portal_items = self.owner.get_gsi_portal_items( key_name, value_name) return gsi_portal_items @componentmethod def get_nearest_posture_change(self, sim): shortest_dist = sims4.math.MAX_FLOAT nearest_portal_id = None nearest_portal = None sim_position = sim.position for (portal_id, portal_instance) in self._portals.items(): (posture_entry, posture_exit) = portal_instance.get_posture_change( portal_id, None) if posture_entry is posture_exit: continue (entry_loc, _) = portal_instance.get_portal_locations(portal_id) dist = (entry_loc.position - sim_position).magnitude_squared() if not nearest_portal is None: if shortest_dist > dist: shortest_dist = dist nearest_portal = portal_instance nearest_portal_id = portal_id shortest_dist = dist nearest_portal = portal_instance nearest_portal_id = portal_id if nearest_portal is None: return (None, None) return nearest_portal.get_posture_change(nearest_portal_id, None) @componentmethod_with_fallback(lambda *_, **__: False) def has_posture_portals(self): for (portal_id, portal_instance) in self._portals.items(): (posture_entry, _) = portal_instance.get_posture_change(portal_id, None) if posture_entry is not None: return True def add_custom_portal(self, location_point, portal_data, portal_creation_mask=None): portal_ids = self._add_portal_internal(location_point, portal_data, portal_creation_mask) if portal_ids: if self._custom_portals is None: self._custom_portals = [] self._custom_portals.append((location_point, portal_data, portal_creation_mask, portal_ids)) return portal_ids def remove_custom_portals(self, portal_ids): if self._custom_portals is None: return for custom_portal in list(self._custom_portals): (location_point, portal_data, mask, custom_portal_ids) = custom_portal portal_ids_to_remove = [] if all(custom_portal_id in portal_ids for custom_portal_id in custom_portal_ids): self._custom_portals.remove(custom_portal) portal_ids_to_remove = custom_portal_ids else: portal_ids_to_remove = [ custom_portal_id for custom_portal_id in custom_portal_ids if custom_portal_id in portal_ids ] if portal_ids_to_remove: portal_ids_to_keep = [ custom_portal_id for custom_portal_id in custom_portal_ids if custom_portal_id not in portal_ids_to_remove ] self._custom_portals.remove(custom_portal) self._custom_portals.append((location_point, portal_data, mask, portal_ids_to_keep)) for portal_id in portal_ids_to_remove: self._remove_portal_internal(portal_id) if not self._custom_portals: self._custom_portals = None def clear_custom_portals(self): if self._custom_portals is not None: portal_ids_to_remove = [ portal_id for custom_portal in self._custom_portals for portal_id in custom_portal[3] ] self.remove_custom_portals(portal_ids_to_remove) self._custom_portals.clear() self._custom_portals = None def get_vehicles_nearby_portal_id(self, portal_id): object_manager = services.object_manager() owner_position = Vector3Immutable(self.owner.position.x, 0, self.owner.position.z) portal_inst = self.get_portal_by_id(portal_id) if portal_inst is None: return [] if portal_inst.portal_template.use_vehicle_after_traversal is None: return [] target_surface = portal_inst.get_target_surface(portal_id) results = [] portal_vehicle_tuning = portal_inst.portal_template.use_vehicle_after_traversal for vehicle in object_manager.get_objects_with_tags_gen( *portal_vehicle_tuning.vehicle_tags): if vehicle.routing_surface.type != target_surface.type: continue vehicle_position = Vector3Immutable(vehicle.position.x, 0, vehicle.position.z) distance = (owner_position - vehicle_position).magnitude_squared() if distance > portal_inst.portal_template.use_vehicle_after_traversal.max_distance: continue results.append(vehicle) return results def get_portal_location_by_type(self, portal_type, portal_direction, portal_location): portal_pairs = self.get_portal_pairs() for (portal_there, portal_back) in portal_pairs: if portal_there is None and portal_back is None: continue there_instance = self.get_portal_by_id(portal_there) if there_instance.portal_template is portal_type.value: location = self._get_desired_location(portal_there, portal_back, portal_direction, portal_location) if location is None: continue return location def _on_state_changed_callback(self, owner, state, old_value, new_value): if old_value == new_value: return if old_value in self.state_values_which_disable_portals or new_value in self.state_values_which_disable_portals: self._refresh_portals() def _get_desired_location(self, portal_there_id, portal_back_id, portal_direction, portal_location): if portal_direction == PortalComponent.PORTAL_DIRECTION_THERE: portal_instance = self.get_portal_by_id(portal_there_id) else: if portal_back_id is None: return portal_instance = self.get_portal_by_id(portal_back_id) if portal_instance is None: return location = portal_instance.there_entry if portal_location == PortalComponent.PORTAL_LOCATION_ENTRY else portal_instance.there_exit return location
class ObjectManager(DistributableObjectManager, GameObjectManagerMixin, AttractorManagerMixin): FIREMETER_DISPOSABLE_OBJECT_CAP = Tunable( int, 5, description= 'Number of disposable objects a lot can have at any given moment.') BED_TAGS = TunableTuple( description= '\n Tags to check on an object to determine what type of bed an object is.\n ', beds=TunableSet( description= '\n Tags that consider an object as a bed other than double beds.\n ', tunable=TunableEnumWithFilter(tunable_type=tag.Tag, default=tag.Tag.INVALID, filter_prefixes=BED_PREFIX_FILTER)), double_beds=TunableSet( description= '\n Tags that consider an object as a double bed\n ', tunable=TunableEnumWithFilter(tunable_type=tag.Tag, default=tag.Tag.INVALID, filter_prefixes=BED_PREFIX_FILTER)), kid_beds=TunableSet( description= '\n Tags that consider an object as a kid bed\n ', tunable=TunableEnumWithFilter(tunable_type=tag.Tag, default=tag.Tag.INVALID, filter_prefixes=BED_PREFIX_FILTER)), other_sleeping_spots=TunableSet( description= '\n Tags that considered sleeping spots.\n ', tunable=TunableEnumWithFilter(tunable_type=tag.Tag, default=tag.Tag.INVALID, filter_prefixes=BED_PREFIX_FILTER))) HOUSEHOLD_INVENTORY_OBJECT_TAGS = TunableTags( description= '\n List of tags to apply to every household inventory proxy object.\n ' ) INVALID_UNPARENTED_OBJECT_TAGS = TunableTags( description= '\n Objects with these tags should not exist without a parent. An obvious\n case is for transient objects. They should only exist as a carried object,\n thus parented to a sim, when loading into a save game.\n ' ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._crafting_cache = CraftingObjectCache() self._sim_spawn_conditions = collections.defaultdict(set) self._water_terrain_object_cache = WaterTerrainObjectCache() self._client_connect_callbacks = CallableList() self._portal_cache = WeakSet() self._portal_added_callbacks = CallableList() self._portal_removed_callbacks = CallableList() self._front_door_candidates_changed_callback = CallableList() self._all_bed_tags = self.BED_TAGS.beds | self.BED_TAGS.double_beds | self.BED_TAGS.kid_beds | self.BED_TAGS.other_sleeping_spots self._tag_to_object_list = defaultdict(set) self._whim_set_cache = Counter() self._posture_providing_object_cache = None self._objects_to_ignore_portal_validation_cache = [] @classproperty def save_error_code(cls): return persistence_error_types.ErrorCodes.SERVICE_SAVE_FAILED_OBJECT_MANAGER @property def crafting_cache(self): return self._crafting_cache @property def water_terrain_object_cache(self): return self._water_terrain_object_cache def portal_cache_gen(self): yield from self._portal_cache def on_client_connect(self, client): all_objects = list(self._objects.values()) for game_object in all_objects: game_object.on_client_connect(client) def move_to_inventory(self, obj, inventory_manager): logger.assert_raise( isinstance(inventory_manager, InventoryManager), 'Trying to move object to a non-inventory manager: {}', inventory_manager, owner='tingyul') logger.assert_raise( obj.id, 'Attempting to move an object that was never added or has already been removed', owner='tingyul') logger.assert_raise( self._objects.get(obj.id) is obj, 'Attempting to move an object {} that is not in this manager or not the same object {} in manager', obj, self._objects.get(obj.id), owner='tingyul') del self._objects[obj.id] obj.manager = inventory_manager inventory_manager._objects[obj.id] = obj self.remove_object_from_object_tags_cache(obj) self.remove_object_from_posture_providing_cache(obj) def add(self, obj, *args, **kwargs): super().add(obj, *args, **kwargs) self.add_object_to_object_tags_cache(obj) self.add_object_to_posture_providing_cache(obj) def remove(self, obj, *args, **kwargs): super().remove(obj, *args, **kwargs) current_zone = services.current_zone() if not current_zone.is_zone_shutting_down: self.remove_object_from_object_tags_cache(obj) self.remove_object_from_posture_providing_cache(obj) def add_object_to_object_tags_cache(self, obj): self.add_tags_and_object_to_cache(obj.get_tags(), obj) def add_tags_and_object_to_cache(self, tags, obj): if obj.id not in self: logger.error( "Trying to add object to tag cache when the object isn't in the manager: {}", obj, owner='tingyul') return for tag in tags: object_list = self._tag_to_object_list[tag] object_list.add(obj) def remove_object_from_object_tags_cache(self, obj): for tag in obj.get_tags(): if tag not in self._tag_to_object_list: continue object_list = self._tag_to_object_list[tag] if obj not in object_list: continue object_list.remove(obj) if not object_list: del self._tag_to_object_list[tag] def _should_save_object_on_lot(self, obj): parent = obj.parent if parent is not None and parent.is_sim: inventory = parent.inventory_component if inventory.should_save_parented_item_to_inventory(obj): return False else: vehicle_component = obj.vehicle_component if vehicle_component is not None: driver = vehicle_component.driver if driver is not None and driver.is_sim: inventory = driver.inventory_component if inventory.should_save_parented_item_to_inventory( obj): return False else: vehicle_component = obj.vehicle_component if vehicle_component is not None: driver = vehicle_component.driver if driver is not None and driver.is_sim: inventory = driver.inventory_component if inventory.should_save_parented_item_to_inventory(obj): return False return True def add_object_to_posture_providing_cache(self, obj): if not obj.provided_mobile_posture_affordances: return if self._posture_providing_object_cache is None: self._posture_providing_object_cache = set() self._posture_providing_object_cache.add(obj) posture_graph_service = services.posture_graph_service() if not posture_graph_service.has_built_for_zone_spin_up: posture_graph_service.on_mobile_posture_object_added_during_zone_spinup( obj) def remove_object_from_posture_providing_cache(self, obj): if not obj.provided_mobile_posture_affordances: return self._posture_providing_object_cache.remove(obj) if not self._posture_providing_object_cache: self._posture_providing_object_cache = None def get_posture_providing_objects(self): return self._posture_providing_object_cache or () def rebuild_objects_to_ignore_portal_validation_cache(self): self._objects_to_ignore_portal_validation_cache.clear() for obj in self._objects.values(): if not obj.routing_component is not None: if not obj.inventoryitem_component is not None: if obj.live_drag_component is not None: self._objects_to_ignore_portal_validation_cache.append( obj.id) self._objects_to_ignore_portal_validation_cache.append(obj.id) def clear_objects_to_ignore_portal_validation_cache(self): self._objects_to_ignore_portal_validation_cache.clear() def get_objects_to_ignore_portal_validation_cache(self): return self._objects_to_ignore_portal_validation_cache def clear_caches_on_teardown(self): self._tag_to_object_list.clear() self._water_terrain_object_cache.clear() if self._posture_providing_object_cache is not None: self._posture_providing_object_cache.clear() self.clear_objects_to_ignore_portal_validation_cache() build_buy.unregister_build_buy_exit_callback( self._water_terrain_object_cache.refresh) def pre_save(self): all_objects = list(self._objects.values()) lot = services.current_zone().lot for (_, inventory) in lot.get_all_object_inventories_gen( shared_only=True): for game_object in inventory: all_objects.append(game_object) for game_object in all_objects: game_object.update_all_commodities() @staticmethod def save_game_object(game_object, object_list, open_street_objects): save_result = None if game_object.persistence_group == objects.persistence_groups.PersistenceGroups.OBJECT: save_result = game_object.save_object(object_list.objects, ItemLocation.ON_LOT, 0) else: if game_object.item_location == ItemLocation.ON_LOT or game_object.item_location == ItemLocation.INVALID_LOCATION: item_location = ItemLocation.FROM_OPEN_STREET else: item_location = game_object.item_location save_result = game_object.save_object(open_street_objects.objects, item_location, 0) return save_result def save(self, object_list=None, zone_data=None, open_street_data=None, store_travel_group_placed_objects=False, **kwargs): if object_list is None: return open_street_objects = file_serialization.ObjectList() total_beds = 0 double_bed_exist = False kid_bed_exist = False alternative_sleeping_spots = 0 university_roommate_beds = 0 if store_travel_group_placed_objects: objects_to_save_for_clean_up = [] roommate_bed_tags = set() roommate_service = services.get_roommate_service() if roommate_service is not None: roommate_bed_tags = roommate_service.BED_TAGS for game_object in self._objects.values(): if self._should_save_object_on_lot(game_object): save_result = ObjectManager.save_game_object( game_object, object_list, open_street_objects) if not save_result: continue if zone_data is None: continue if store_travel_group_placed_objects and save_result.owner_id != 0: placement_flags = build_buy.get_object_placement_flags( game_object.definition.id) if build_buy.PlacementFlags.NON_INVENTORYABLE not in placement_flags: objects_to_save_for_clean_up.append(save_result) if not game_object.definition.has_build_buy_tag( *self._all_bed_tags): continue if game_object.definition.has_build_buy_tag( *self.BED_TAGS.double_beds): double_bed_exist = True total_beds += 1 elif game_object.definition.has_build_buy_tag( *self.BED_TAGS.kid_beds): total_beds += 1 kid_bed_exist = True elif game_object.definition.has_build_buy_tag( *self.BED_TAGS.other_sleeping_spots): alternative_sleeping_spots += 1 elif game_object.definition.has_build_buy_tag( *self.BED_TAGS.beds): total_beds += 1 if len(roommate_bed_tags) > 0: if game_object.definition.has_build_buy_tag( *roommate_bed_tags): university_roommate_beds += 1 if open_street_data is not None: open_street_data.objects = open_street_objects if zone_data is not None: bed_info_data = gameplay_serialization.ZoneBedInfoData() bed_info_data.num_beds = total_beds bed_info_data.double_bed_exist = double_bed_exist bed_info_data.kid_bed_exist = kid_bed_exist bed_info_data.alternative_sleeping_spots = alternative_sleeping_spots if roommate_service is not None: household_and_roommate_cap = roommate_service.HOUSEHOLD_AND_ROOMMATE_CAP bed_info_data.university_roommate_beds = min( household_and_roommate_cap, university_roommate_beds) zone_data.gameplay_zone_data.bed_info_data = bed_info_data if store_travel_group_placed_objects: current_zone = services.current_zone() save_game_protocol_buffer = services.get_persistence_service( ).get_save_game_data_proto() self._clear_clean_up_data_for_zone(current_zone, save_game_protocol_buffer) self._save_clean_up_destination_data( current_zone, objects_to_save_for_clean_up, save_game_protocol_buffer) lot = services.current_zone().lot for (inventory_type, inventory) in lot.get_all_object_inventories_gen( shared_only=True): for game_object in inventory: game_object.save_object(object_list.objects, ItemLocation.OBJECT_INVENTORY, inventory_type) def _clear_clean_up_data_for_zone(self, current_zone, save_game_protocol_buffer): current_zone_id = current_zone.id current_open_street_id = current_zone.open_street_id destination_clean_up_data = save_game_protocol_buffer.destination_clean_up_data for clean_up_save_data in destination_clean_up_data: indexes_to_clean_up = [] for (index, old_object_clean_up_data) in enumerate( clean_up_save_data.object_clean_up_data_list): if not old_object_clean_up_data.zone_id == current_zone_id: if old_object_clean_up_data.world_id == current_open_street_id: indexes_to_clean_up.append(index) indexes_to_clean_up.append(index) if len(indexes_to_clean_up) == len( clean_up_save_data.object_clean_up_data_list): clean_up_save_data.ClearField('object_clean_up_data_list') else: for index in reversed(indexes_to_clean_up): del clean_up_save_data.object_clean_up_data_list[index] def _save_clean_up_destination_data(self, current_zone, objects_to_save_for_clean_up, save_game_protocol_buffer): household_manager = services.household_manager() travel_group_manager = services.travel_group_manager() clean_up_save_data = None for object_data in sorted(objects_to_save_for_clean_up, key=lambda x: x.owner_id): owner_id = object_data.owner_id if clean_up_save_data is None or clean_up_save_data.household_id != owner_id: household = household_manager.get(owner_id) travel_group = None if household is not None: travel_group = household.get_travel_group() for clean_up_save_data in save_game_protocol_buffer.destination_clean_up_data: if clean_up_save_data.household_id != owner_id: continue if travel_group is not None: if travel_group.id == clean_up_save_data.travel_group_id: break if clean_up_save_data.travel_group_id in travel_group_manager: continue break with ProtocolBufferRollback( clean_up_save_data.object_clean_up_data_list ) as object_clean_up_data: if object_data.loc_type == ItemLocation.ON_LOT: object_clean_up_data.zone_id = current_zone.id else: object_clean_up_data.world_id = current_zone.open_street_id object_clean_up_data.object_data = object_data def add_sim_spawn_condition(self, sim_id, callback): for sim in services.sim_info_manager().instanced_sims_gen(): if sim.id == sim_id: logger.error( 'Sim {} is already in the world, cannot add the spawn condition', sim) return self._sim_spawn_conditions[sim_id].add(callback) def remove_sim_spawn_condition(self, sim_id, callback): if callback not in self._sim_spawn_conditions.get(sim_id, ()): logger.error( 'Trying to remove sim spawn condition with invalid id-callback pair ({}-{}).', sim_id, callback) return self._sim_spawn_conditions[sim_id].remove(callback) def trigger_sim_spawn_condition(self, sim_id): if sim_id in self._sim_spawn_conditions: for callback in self._sim_spawn_conditions[sim_id]: callback() del self._sim_spawn_conditions[sim_id] def add_portal_lock(self, sim, callback): self.register_portal_added_callback(callback) for portal in self.portal_cache_gen(): portal.lock_sim(sim) def register_portal_added_callback(self, callback): if callback not in self._portal_added_callbacks: self._portal_added_callbacks.append(callback) def unregister_portal_added_callback(self, callback): if callback in self._portal_added_callbacks: self._portal_added_callbacks.remove(callback) def register_portal_removed_callback(self, callback): if callback not in self._portal_removed_callbacks: self._portal_removed_callbacks.append(callback) def unregister_portal_removed_callback(self, callback): if callback in self._portal_removed_callbacks: self._portal_removed_callbacks.remove(callback) def _is_valid_portal_object(self, portal): portal_component = portal.get_component(PORTAL_COMPONENT) if portal_component is None: return False return portal.has_portals() def add_portal_to_cache(self, portal): if portal not in self._portal_cache and self._is_valid_portal_object( portal): self._portal_cache.add(portal) self._portal_added_callbacks(portal) def remove_portal_from_cache(self, portal): if portal in self._portal_cache: self._portal_cache.remove(portal) self._portal_removed_callbacks(portal) def register_front_door_candidates_changed_callback(self, callback): if callback not in self._front_door_candidates_changed_callback: self._front_door_candidates_changed_callback.append(callback) def unregister_front_door_candidates_changed_callback(self, callback): if callback in self._front_door_candidates_changed_callback: self._front_door_candidates_changed_callback.remove(callback) def on_front_door_candidates_changed(self): self._front_door_candidates_changed_callback() def cleanup_build_buy_transient_objects(self): household_inventory_proxy_objects = self.get_objects_matching_tags( self.HOUSEHOLD_INVENTORY_OBJECT_TAGS) for obj in household_inventory_proxy_objects: self.remove(obj) def get_objects_matching_tags(self, tags: set, match_any=False): matching_objects = None for tag in tags: objs = self._tag_to_object_list[ tag] if tag in self._tag_to_object_list else set() if matching_objects is None: matching_objects = objs elif match_any: matching_objects |= objs else: matching_objects &= objs if not matching_objects: break if matching_objects: return frozenset(matching_objects) return EMPTY_SET def get_num_objects_matching_tags(self, tags: set, match_any=False): matching_objects = self.get_objects_matching_tags(tags, match_any) return len(matching_objects) @contextmanager def batch_commodity_flags_update(self): default_fn = self.clear_commodity_flags_for_objs_with_affordance try: affordances = set() self.clear_commodity_flags_for_objs_with_affordance = affordances.update yield None finally: self.clear_commodity_flags_for_objs_with_affordance = default_fn self.clear_commodity_flags_for_objs_with_affordance(affordances) def clear_commodity_flags_for_objs_with_affordance(self, affordances): for obj in self.valid_objects(): if not obj.has_updated_commodity_flags(): continue if any(affordance in affordances for affordance in obj.super_affordances()): obj.clear_commodity_flags() def get_all_objects_with_component_gen(self, component_definition): if component_definition is None: return for obj in self.valid_objects(): if obj.has_component(component_definition): yield obj def get_objects_with_tag_gen(self, tag): yield from self.get_objects_matching_tags((tag, )) def get_objects_with_tags_gen(self, *tags): yield from self.get_objects_matching_tags(tags, match_any=True) def on_location_changed(self, obj): self._registered_callbacks[CallbackTypes.ON_OBJECT_LOCATION_CHANGED]( obj) def process_invalid_unparented_objects(self): invalid_objects = self.get_objects_matching_tags( self.INVALID_UNPARENTED_OBJECT_TAGS, match_any=True) for invalid_object in invalid_objects: if invalid_object.parent is None: logger.error( 'Invalid unparented object {} existed in game. Cleaning up.', invalid_object) invalid_object.destroy( source=invalid_object, cause='Invalid unparented object found on zone spin up.') @classproperty def supports_parenting(self): return True def add_active_whim_set(self, whim_set): self._whim_set_cache[whim_set] += 1 def remove_active_whim_set(self, whim_set): self._whim_set_cache[whim_set] -= 1 if self._whim_set_cache[whim_set] <= 0: del self._whim_set_cache[whim_set] @property def active_whim_sets(self): return set(self._whim_set_cache.keys())
class MovingObjectsSituation(SituationComplexCommon): INSTANCE_TUNABLES = {'_preparation_state': _PreparationState.TunableFactory(tuning_group=GroupNames.STATE), '_waiting_to_move_state': _WaitingToMoveState.TunableFactory(tuning_group=GroupNames.STATE), '_tests_to_continue': TunableTestSet(description='\n A list of tests that must pass in order to continue the situation\n after the tuned duration for the waiting state has elapsed.\n ', tuning_group=GroupNames.STATE), 'starting_requirements': TunableTestSet(description='\n A list of tests that must pass in order for the situation\n to start.\n ', tuning_group=GroupNames.SITUATION), 'object_tags': TunableTags(description='\n Tags used to find objects which will move about.\n ', tuning_group=GroupNames.SITUATION), 'placement_strategy_locations': TunableList(description='\n A list of weighted location strategies.\n ', tunable=TunableTuple(weight=TunableMultiplier.TunableFactory(description='\n The weight of this strategy relative to other locations.\n '), placement_strategy=_PlacementStrategyLocation.TunableFactory(description='\n The placement strategy for the object.\n ')), minlength=1, tuning_group=GroupNames.SITUATION), 'fade': OptionalTunable(description='\n If enabled, the objects will fade-in/fade-out as opposed to\n immediately moving to their location.\n ', tunable=TunableTuple(out_time=TunableSimMinute(description='\n Time over which the time will fade out.\n ', default=1), in_time=TunableSimMinute(description='\n Time over which the time will fade in.\n ', default=1)), enabled_by_default=True, tuning_group=GroupNames.SITUATION), 'vfx_on_move': OptionalTunable(description='\n If tuned, apply this one-shot vfx on the moving object when it\n is about to move.\n ', tunable=PlayEffect.TunableFactory(), tuning_group=GroupNames.SITUATION), 'situation_end_loots_to_apply_on_objects': TunableSet(description='\n The loots to apply on the tagged objects when the situation ends \n or is destroyed.\n \n E.g. use this to reset objects to a specific state after \n the situation is over.\n \n The loot will be processed with the active sim as the actor,\n and the object as the target.\n ', tunable=TunableReference(manager=services.get_instance_manager(Types.ACTION), pack_safe=True), tuning_group=GroupNames.SITUATION)} REMOVE_INSTANCE_TUNABLES = Situation.NON_USER_FACING_REMOVE_INSTANCE_TUNABLES def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) reader = self._seed.custom_init_params_reader if reader is None: self._target_id = self._seed.extra_kwargs.get('default_target_id', None) else: self._target_id = reader.read_uint64(OBJECT_TOKEN, None) @classmethod def _states(cls): return (SituationStateData(0, _PreparationState, factory=cls._preparation_state), SituationStateData(1, _WaitingToMoveState, factory=cls._waiting_to_move_state)) @classmethod def default_job(cls): pass @classmethod def _get_tuned_job_and_default_role_state_tuples(cls): return [] @classmethod def situation_meets_starting_requirements(cls, **kwargs): if not cls.starting_requirements: return True else: resolver = SingleSimResolver(services.active_sim_info()) if not cls.starting_requirements.run_tests(resolver): return False return True def _save_custom_situation(self, writer): super()._save_custom_situation(writer) if self._target_id is not None: writer.write_uint64(OBJECT_TOKEN, self._target_id) def start_situation(self): super().start_situation() self._change_state(self._preparation_state()) def load_situation(self): if not self.situation_meets_starting_requirements(): return False return super().load_situation() def on_objects_ready(self): self._change_state(self._waiting_to_move_state()) def on_ready_to_move(self): if self._tests_to_continue.run_tests(GlobalResolver()): self._move_objects() self._change_state(self._waiting_to_move_state()) else: self._self_destruct() def _get_placement_resolver(self): additional_participants = {} if self._target_id is not None: target = services.object_manager().get(self._target_id) additional_participants[ParticipantType.Object] = (target,) if target is not None: if target.is_sim: additional_participants[ParticipantType.TargetSim] = (target.sim_info,) return SingleSimResolver(services.active_sim_info(), additional_participants=additional_participants) def _destroy(self): objects_of_interest = services.object_manager().get_objects_matching_tags(self.object_tags, match_any=True) if not objects_of_interest: return active_sim_info = services.active_sim_info() for obj in objects_of_interest: resolver = SingleActorAndObjectResolver(active_sim_info, obj, self) for loot in self.situation_end_loots_to_apply_on_objects: loot.apply_to_resolver(resolver) super()._destroy() def _move_objects(self): objects_to_move = services.object_manager().get_objects_matching_tags(self.object_tags, match_any=True) if not objects_to_move: return resolver = self._get_placement_resolver() choices = [(location.weight.get_multiplier(resolver), location.placement_strategy) for location in self.placement_strategy_locations] chosen_strategy = random.weighted_random_item(choices) do_fade = self.fade is not None out_sequence = [] moves = [] in_sequence = [] for object_to_move in objects_to_move: object_to_move.cancel_interactions_running_on_object(FinishingType.OBJECT_CHANGED, cancel_reason_msg='Object changing location.') if self.vfx_on_move is not None: out_sequence.append(lambda _, object_to_move=object_to_move: self.vfx_on_move(object_to_move).start_one_shot()) if do_fade: out_sequence.append(lambda _, object_to_move=object_to_move: object_to_move.fade_out(self.fade.out_time)) moves.append(lambda _, object_to_move=object_to_move: chosen_strategy.try_place_object(object_to_move, resolver)) if do_fade: in_sequence.append(lambda _, object_to_move=object_to_move: object_to_move.fade_in(self.fade.in_time)) sequence = [] if out_sequence: sequence.append(out_sequence) sequence.append(SoftSleepElement(clock.interval_in_sim_minutes(self.fade.out_time))) sequence.append(moves) if in_sequence: sequence.append(in_sequence) element = build_element(sequence, critical=CleanupType.RunAll) services.time_service().sim_timeline.schedule(element)
class BuffRemovalOp(BaseLootOperation): FACTORY_TUNABLES = { 'remove_all_visible_buffs': Tunable( description= "\n If checked, all visible buffs on the Sim, excluding those specified in\n the 'buffs_to_ignore' list will be removed. If unchecked, buff removal\n will be handled by the 'buffs_to_remove' list.\n ", tunable_type=bool, default=False), 'buffs_to_remove': TunableList( description= "\n If 'remove_all_buffs' is not checked, this is the list of buffs that\n will be removed from the subject. If 'remove_all_buffs' is checked,\n this list will be ignored.\n ", tunable=TunableReference( description= '\n Buff to be removed.\n ', manager=services.buff_manager(), pack_safe=True)), 'buff_tags_to_remove': TunableTags( description= "\n If 'remove_all_buffs' is not checked, buffs with any tag in this list\n will be removed from the subject. If 'remove_all_buffs' is checked, this\n list will be ignored. You can also specify how many buffs you want to remove\n by tags in count_to_remove_by_tags\n ", filter_prefixes=('buff', )), 'count_to_remove_by_tags': OptionalTunable(tunable=TunableRange( description= '\n If enabled, randomly remove x number of buffs specified in buff_tags_to_remove.\n If disabled, all buffs specified in buff_tags_to_remove will be removed\n ', tunable_type=int, default=1, minimum=1)), 'buffs_to_ignore': TunableList( description= "\n If 'remove_all_buffs' is checked, no buffs included in this list will\n be removed. If 'remove_all_buffs' is unchecked, this list will be\n ignored.\n ", tunable=TunableReference( description= '\n Buff to be removed.\n ', manager=services.buff_manager())) } def __init__(self, remove_all_visible_buffs, buffs_to_remove, buff_tags_to_remove, count_to_remove_by_tags, buffs_to_ignore, **kwargs): super().__init__(**kwargs) self._remove_all_visible_buffs = remove_all_visible_buffs self._buffs_to_remove = buffs_to_remove self._buff_tags_to_remove = buff_tags_to_remove self._count_to_remove_by_tags = count_to_remove_by_tags self._buffs_to_ignore = buffs_to_ignore def _apply_to_subject_and_target(self, subject, target, resolver): if self._remove_all_visible_buffs: removal_list = [] removal_list.extend(subject.Buffs) for buff in removal_list: if type(buff) in self._buffs_to_ignore: continue if not buff.visible: continue if buff.commodity is not None: if subject.is_statistic_type_added_by_modifier( buff.commodity): continue tracker = subject.get_tracker(buff.commodity) commodity_inst = tracker.get_statistic(buff.commodity) if commodity_inst is not None and commodity_inst.core: continue subject.Buffs.remove_buff_entry(buff) else: subject.Buffs.remove_buffs_by_tags( self._buff_tags_to_remove, count_to_remove=self._count_to_remove_by_tags) else: for buff_type in self._buffs_to_remove: subject.Buffs.remove_buff_by_type(buff_type) subject.Buffs.remove_buffs_by_tags( self._buff_tags_to_remove, count_to_remove=self._count_to_remove_by_tags)