class NarrativeAwareComponent(Component, HasTunableFactory, AutoFactoryInit, component_name=NARRATIVE_AWARE_COMPONENT): FACTORY_TUNABLES = { 'narrative_state_mapping': TunableMapping( description= '\n A tunable mapping linking a narrative to the states the component\n owner should have.\n ', key_type=TunableReference( description= '\n The narrative we are interested in.\n ', manager=services.get_instance_manager(Types.NARRATIVE)), value_type=TunableList( description= '\n A tunable list of states to apply to the owning object of\n this component when this narrative is active.\n ', tunable=TunableStateValueReference(pack_safe=True))) } def on_add(self): self.on_narratives_set(services.narrative_service().active_narratives) def on_finalize_load(self): self.on_narratives_set(services.narrative_service().active_narratives) def on_narratives_set(self, narratives): for narrative in narratives: if narrative in self.narrative_state_mapping: for state_value in self.narrative_state_mapping[narrative]: self.owner.set_state(state_value.state, state_value)
class TimeOfDayComponent(Component, HasTunableFactory, component_name=types.TIME_OF_DAY_COMPONENT): __qualname__ = 'TimeOfDayComponent' DAILY_REPEAT = date_and_time.create_time_span(hours=24) FACTORY_TUNABLES = { 'state_changes': TunableMapping( description= '\n A mapping from state to times of the day when the state should be \n set to a tuned value.\n ', key_type=TunableStateTypeReference( description='The state to be set.'), value_type=TunableList( description='List of times to modify the state at.', tunable=TunableTuple(start_time=TunableRange( float, 0, description= 'The start time (24 hour clock time) for the Day_Time state.', minimum=0, maximum=24), value=TunableStateValueReference( description='New state value.')))) } def __init__(self, owner, *, state_changes): super().__init__(owner) self.state_changes = state_changes self.alarm_handles = [] def _add_alarm(self, cur_state, game_clock, state, change): time_to_day = game_clock.time_until_hour_of_day(change.start_time) def change_state(_): self.owner.set_state(state, change.value) self.alarm_handles.append( alarms.add_alarm(self.owner, time_to_day, change_state, repeating=True, repeating_time_span=self.DAILY_REPEAT)) if cur_state is None or time_to_day > cur_state[0]: return (time_to_day, change.value) return cur_state def on_add(self): game_clock_service = services.game_clock_service() for (state, changes) in self.state_changes.items(): current_state = None for change in changes: current_state = self._add_alarm(current_state, game_clock_service, state, change) while current_state is not None: self.owner.set_state(state, current_state[1]) def on_remove(self): for handle in self.alarm_handles: alarms.cancel_alarm(handle)
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)
def __init__(self, *args, **kwargs): super().__init__( state=TunableStateValueReference( description= '\n An state that we want to set the object to.\n ' ), description= '\n Change the state of an object to the tuned state.\n ' )
class StereoComponent(Component, HasTunableFactory, AutoFactoryInit, component_name=types.STEREO_COMPONENT): FACTORY_TUNABLES = { 'channel_state': TunableStateTypeReference( description= '\n The state used to populate the radio stations'), 'off_state': TunableStateValueReference( description= '\n The channel that represents the off state.'), 'listen_affordances': TunableList( description= '\n An ordered list of affordances that define "listening" to this\n stereo. The first succeeding affordance is used.\n ', tunable=TunableReference(manager=services.get_instance_manager( sims4.resources.Types.INTERACTION), pack_safe=True)), 'play_on_active_sim_only': Tunable( description= '\n If enabled, and audio target is Sim, the audio will only be \n played on selected Sim. Otherwise it will be played regardless \n Sim is selected or not.\n \n If audio target is Object, always set this to False. Otherwise\n the audio will never be played.\n \n ex. This will be useful for Earbuds where we want to hear the\n music only when the Sim is selected.\n \n This is passed down to the audio state when it is triggered, and thus\n will overwrite any tuning on the state value.\n ', tunable_type=bool, default=False), 'immediate': Tunable( description= '\n If checked, this audio will be triggered immediately, nothing\n will block.\n \n ex. Earbuds audio will be played immediately while \n the Sim is routing or animating.\n \n This is passed down to the audio state when it is triggered, and thus\n will overwrite any tuning on the state value.\n ', tunable_type=bool, default=False) } def is_stereo_turned_on(self): current_channel = self.owner.get_state(self.channel_state) return current_channel != self.off_state def get_available_picker_channel_states(self, context): for client_state in self.owner.get_client_states(self.channel_state): if client_state.show_in_picker: if client_state.test_channel(self.owner, context): yield client_state def component_potential_interactions_gen(self, context, **kwargs): current_channel = self.owner.get_state(self.channel_state) if current_channel != self.off_state: for listen_affordance in self.listen_affordances: yield from listen_affordance.potential_interactions( self.owner, context, required_station=current_channel, off_state=self.off_state, **kwargs)
class _SpawnFiremeterStateBased(HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = { 'state_values': TunableMapping( description= '\n A mapping of state value to cap. If the object has the specified\n state set, the associated value is used as a cap. The order is\n evaluated arbitrarily, so avoid tuning states that are not\n mutually exclusive.\n \n If the object has no state set, no firemeter cap applies.\n ', key_type=TunableStateValueReference(pack_safe=True), value_type=Tunable(tunable_type=int, default=1)) } def __call__(self, obj): for (state_value, value) in self.state_values.items(): if obj.state_value_active(state_value): return value
def __init__( self, description="If the target object isn't in the given state, use an asm and set that object's state.", **kwargs): super().__init__( value=TunableStateValueReference( description='The value to require'), xevt_id=Tunable( int, None, description= "An xevt on which to change the state's value (optional)."), animation_ref=TunableAnimationReference(), description=description, **kwargs)
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 Photography: SMALL_PORTRAIT_OBJ_DEF = TunablePackSafeReference( description= '\n Object definition for a small portrait photo.\n ', manager=services.definition_manager()) SMALL_LANDSCAPE_OBJ_DEF = TunablePackSafeReference( description= '\n Object definition for a small landscape photo.\n ', manager=services.definition_manager()) MEDIUM_PORTRAIT_OBJ_DEF = TunablePackSafeReference( description= '\n Object definition for a medium portrait photo.\n ', manager=services.definition_manager()) MEDIUM_LANDSCAPE_OBJ_DEF = TunablePackSafeReference( description= '\n Object definition for a medium landscape photo.\n ', manager=services.definition_manager()) LARGE_PORTRAIT_OBJ_DEF = TunablePackSafeReference( description= '\n Object definition for a large portrait photo.\n ', manager=services.definition_manager()) LARGE_LANDSCAPE_OBJ_DEF = TunablePackSafeReference( description= '\n Object definition for a large landscape photo.\n ', manager=services.definition_manager()) PAINTING_INTERACTION_TAG = TunableEnumEntry( description= '\n Tag to specify a painting interaction.\n ', tunable_type=tag.Tag, default=tag.Tag.INVALID) PHOTOGRAPHY_LOOT_LIST = TunableList( description= '\n A list of loot operations to apply to the photographer when photo mode exits.\n ', tunable=TunableReference(manager=services.get_instance_manager( sims4.resources.Types.ACTION), class_restrictions=('LootActions', ), pack_safe=True)) FAIL_PHOTO_QUALITY_RANGE = TunableInterval( description= '\n The random quality statistic value that a failure photo will be\n given between the min and max tuned values.\n ', tunable_type=int, default_lower=0, default_upper=100) BASE_PHOTO_QUALITY_MAP = TunableMapping( description= '\n The mapping of CameraQuality value to an interval of quality values\n that will be used to asign a random base quality value to a photo\n as it is created.\n ', key_type=TunableEnumEntry( description= '\n The CameraQuality value. If this photo has this CameraQuality,\n value, then a random quality between the min value and max value\n will be assigned to the photo.\n ', tunable_type=CameraQuality, default=CameraQuality.CHEAP), value_type=TunableInterval( description= '\n The range of base quality values from which a random value will be\n given to the photo.\n ', tunable_type=int, default_lower=1, default_upper=100)) QUALITY_MODIFIER_PER_SKILL_LEVEL = Tunable( description= '\n For each level of skill in Photography, this amount will be added to\n the quality statistic.\n ', tunable_type=float, default=0) PHOTO_VALUE_MODIFIER_MAP = TunableMapping( description= '\n The mapping of state values to Simoleon value modifiers.\n The final value of a photo is decided based on its\n current value multiplied by the sum of all modifiers for\n states that apply to the photo. All modifiers are\n added together first, then the sum will be multiplied by\n the current price.\n ', key_type=TunableStateValueReference( description= '\n The quality state values. If this photo has this state,\n then a random modifier between min_value and max_value\n will be multiplied to the current price.' ), value_type=TunableInterval( description= '\n The maximum modifier multiplied to the current price based on the provided state value\n ', tunable_type=float, default_lower=1, default_upper=1)) PHOTO_VALUE_SKILL_CURVE = TunableStatisticModifierCurve.TunableFactory( description= "\n Allows you to adjust the final value of the photo based on the Sim's\n level of a given skill.\n ", axis_name_overrides=('Skill Level', 'Simoleon Multiplier'), locked_args={'subject': ParticipantType.Actor}) PHOTOGRAPHY_SKILL = Skill.TunablePackSafeReference( description='\n A reference to the photography skill.\n ' ) EMOTION_STATE_MAP = TunableMapping( description= "\n The mapping of moods to states, used to give photo objects a mood\n based state. These states are then used by the tooltip component to\n display emotional content on the photo's tooltip.\n ", key_type=TunableReference( description= '\n The mood to associate with a state.\n ', manager=services.mood_manager()), value_type=TunableStateValueReference( description= '\n The state that represents the mood for the purpose of displaying\n emotional content in a tooltip.\n ' )) PHOTO_OBJECT_LOOT_PER_TARGET = TunableList( description= '\n A list of loots which will be applied once PER target. The participants\n for each application will be Actor: photographer, Target: photograph\n target and Object: the Photograph itself. If a photo interaction has 2\n target sims, this loot will be applied twice.\n ', tunable=TunableReference(manager=services.get_instance_manager( sims4.resources.Types.ACTION), pack_safe=True)) MOOD_PARAM_TO_MOOD_CATEGORY_STATE = TunableMapping( description= '\n If the player took a picture in a photo mode that supports mood\n categories, we will perform a state change to the corresponding state\n based on the mood that each picture was taken in.\n ', key_type=Tunable( description= '\n The mood ASM parameter value to associate with a state.\n ', tunable_type=str, default=None), value_type=TunableStateValueReference( description= '\n The state that represents the mood category.\n ' )) GROUP_PHOTO_X_ACTOR_TAG = TunableEnumEntry( description= '\n Tag to specify the photo studio interaction that the photo target sim\n who should be considered the x actor will run.\n ', tunable_type=tag.Tag, default=tag.Tag.INVALID, invalid_enums=(tag.Tag.INVALID, )) GROUP_PHOTO_Y_ACTOR_TAG = TunableEnumEntry( description= '\n Tag to specify the photo studio interaction that the photo target sim\n who should be considered the y actor will run.\n ', tunable_type=tag.Tag, default=tag.Tag.INVALID, invalid_enums=(tag.Tag.INVALID, )) GROUP_PHOTO_Z_ACTOR_TAG = TunableEnumEntry( description= '\n Tag to specify the photo studio interaction that the photo target sim\n who should be considered the z actor will run.\n ', tunable_type=tag.Tag, default=tag.Tag.INVALID, invalid_enums=(tag.Tag.INVALID, )) NUM_PHOTOS_PER_SESSION = Tunable( description= '\n Max possible photos that can be taken during one photo session. Once\n this number has been reached, the photo session will exit.\n ', tunable_type=int, default=5) @classmethod def _is_fail_photo(cls, photo_style_type): if photo_style_type == PhotoStyleType.EFFECT_GRAINY or ( photo_style_type == PhotoStyleType.EFFECT_OVERSATURATED or (photo_style_type == PhotoStyleType.EFFECT_UNDERSATURATED or (photo_style_type == PhotoStyleType.PHOTO_FAIL_BLURRY or (photo_style_type == PhotoStyleType.PHOTO_FAIL_FINGER or photo_style_type == PhotoStyleType.PHOTO_FAIL_GNOME))) ) or photo_style_type == PhotoStyleType.PHOTO_FAIL_NOISE: return True return False @classmethod def _apply_quality_and_value_to_photo(cls, photographer_sim, photo_obj, photo_style, camera_quality): quality_stat = CraftingTuning.QUALITY_STATISTIC quality_stat_tracker = photo_obj.get_tracker(quality_stat) if cls._is_fail_photo(photo_style): final_quality = cls.FAIL_PHOTO_QUALITY_RANGE.random_int() else: quality_range = cls.BASE_PHOTO_QUALITY_MAP.get( camera_quality, None) if quality_range is None: logger.error( 'Photography tuning BASE_PHOTO_QUALITY_MAP does not have an expected quality value: []', str(camera_quality)) return base_quality = quality_range.random_int() skill_quality_modifier = 0 if cls.PHOTOGRAPHY_SKILL is not None: effective_skill_level = photographer_sim.get_effective_skill_level( cls.PHOTOGRAPHY_SKILL) if effective_skill_level: skill_quality_modifier = effective_skill_level * cls.QUALITY_MODIFIER_PER_SKILL_LEVEL final_quality = base_quality + skill_quality_modifier quality_stat_tracker.set_value(quality_stat, final_quality) value_multiplier = 1 for (state_value, value_mods) in cls.PHOTO_VALUE_MODIFIER_MAP.items(): if photo_obj.has_state(state_value.state): actual_state_value = photo_obj.get_state(state_value.state) if state_value is actual_state_value: value_multiplier *= value_mods.random_float() break value_multiplier *= cls.PHOTO_VALUE_SKILL_CURVE.get_multiplier( SingleSimResolver(photographer_sim), photographer_sim) photo_obj.base_value = int(photo_obj.base_value * value_multiplier) @classmethod def _get_mood_sim_info_if_exists(cls, photographer_sim_info, target_sim_ids, camera_mode): if camera_mode is CameraMode.SELFIE_PHOTO: return photographer_sim_info else: num_target_sims = len(target_sim_ids) if num_target_sims == 1: sim_info_manager = services.sim_info_manager() target_sim_info = sim_info_manager.get(target_sim_ids[0]) return target_sim_info @classmethod def _apply_mood_state_if_appropriate(cls, photographer_sim_info, target_sim_ids, camera_mode, photo_object): mood_sim_info = cls._get_mood_sim_info_if_exists( photographer_sim_info, target_sim_ids, camera_mode) if mood_sim_info: mood = mood_sim_info.get_mood() mood_state = cls.EMOTION_STATE_MAP.get(mood, None) if mood_state: photo_object.set_state(mood_state.state, mood_state) @classmethod def _apply_mood_category_state_if_appropriate(cls, selected_mood_param, camera_mode, photo_object): if camera_mode in (CameraMode.TRIPOD, CameraMode.SIM_PHOTO, CameraMode.PHOTO_STUDIO_PHOTO): mood_category_state = cls.MOOD_PARAM_TO_MOOD_CATEGORY_STATE.get( selected_mood_param, None) if mood_category_state: photo_object.set_state(mood_category_state.state, mood_category_state) @classmethod def create_photo_from_photo_data(cls, camera_mode, camera_quality, photographer_sim_id, target_obj_id, target_sim_ids, res_key, photo_style, photo_size, photo_orientation, photographer_sim_info, photographer_sim, time_stamp, selected_mood_param): photo_object = None is_paint_by_reference = camera_mode is CameraMode.PAINT_BY_REFERENCE if is_paint_by_reference: current_zone = services.current_zone() photo_object = current_zone.object_manager.get(target_obj_id) if photo_object is None: photo_object = current_zone.inventory_manager.get( target_obj_id) else: if photo_orientation == PhotoOrientation.LANDSCAPE: if photo_size == PhotoSize.LARGE: photo_object_def = cls.LARGE_LANDSCAPE_OBJ_DEF elif photo_size == PhotoSize.MEDIUM: photo_object_def = cls.MEDIUM_LANDSCAPE_OBJ_DEF elif photo_size == PhotoSize.SMALL: photo_object_def = cls.SMALL_LANDSCAPE_OBJ_DEF elif photo_orientation == PhotoOrientation.PORTRAIT: if photo_size == PhotoSize.LARGE: photo_object_def = cls.LARGE_PORTRAIT_OBJ_DEF elif photo_size == PhotoSize.MEDIUM: photo_object_def = cls.MEDIUM_PORTRAIT_OBJ_DEF elif photo_size == PhotoSize.SMALL: photo_object_def = cls.SMALL_PORTRAIT_OBJ_DEF else: photo_object_def = cls.SMALL_LANDSCAPE_OBJ_DEF if photo_object_def is None: return photo_object = create_object(photo_object_def) if photo_object is None: logger.error('photo object could not be found.') return for target_sim_id in target_sim_ids: target_sim_info = services.sim_info_manager().get(target_sim_id) target_sim = target_sim_info.get_sim_instance() resolver = DoubleSimAndObjectResolver(photographer_sim, target_sim, photo_object, source=cls) for loot in cls.PHOTO_OBJECT_LOOT_PER_TARGET: loot.apply_to_resolver(resolver) photography_service = services.get_photography_service() loots = photography_service.get_loots_for_photo() for photoloot in loots: if photoloot._AUTO_FACTORY.FACTORY_TYPE is RotateTargetPhotoLoot: photographer_sim = photoloot.photographer photographer_sim_info = photographer_sim.sim_info break reveal_level = PaintingState.REVEAL_LEVEL_MIN if is_paint_by_reference else PaintingState.REVEAL_LEVEL_MAX painting_state = PaintingState.from_key(res_key, reveal_level, False, photo_style) photo_object.canvas_component.painting_state = painting_state photo_object.canvas_component.time_stamp = time_stamp photo_object.set_household_owner_id(photographer_sim.household_id) if selected_mood_param: cls._apply_mood_category_state_if_appropriate( selected_mood_param, camera_mode, photo_object) if not is_paint_by_reference: cls._apply_quality_and_value_to_photo(photographer_sim, photo_object, photo_style, camera_quality) cls._apply_mood_state_if_appropriate(photographer_sim_info, target_sim_ids, camera_mode, photo_object) photo_object.add_dynamic_component(STORED_SIM_INFO_COMPONENT, sim_id=photographer_sim.id) photo_object.update_object_tooltip() if not (photographer_sim.inventory_component.can_add(photo_object) and photographer_sim.inventory_component. player_try_add_object(photo_object)): logger.error( "photo object could not be put in the sim's inventory, deleting photo." ) photo_object.destroy() photo_targets = [ services.sim_info_manager().get(sim_id) for sim_id in target_sim_ids ] if camera_mode == CameraMode.TWO_SIM_SELFIE_PHOTO: photo_targets.append(photographer_sim_info) photo_targets = frozenset(photo_targets) services.get_event_manager().process_event( test_events.TestEvent.PhotoTaken, sim_info=photographer_sim_info, photo_object=photo_object, photo_targets=photo_targets)
class ObjectTeleportationComponent(Component, HasTunableFactory, AutoFactoryInit, component_name=types.OBJECT_TELEPORTATION_COMPONENT): ON_CLIENT_CONNECT = 0 FACTORY_TUNABLES = {'when_to_teleport': TunableVariant(description='\n When this object should teleport around.\n ', locked_args={'on_client_connect': ON_CLIENT_CONNECT}, default='on_client_connect'), 'chance_to_teleport': TunablePercent(description='\n A percent chance that this object will teleport when the\n appropriate situation arises.\n ', default=100), 'required_states': OptionalTunable(TunableList(description='\n The states this object is required to be in in order to teleport.\n ', tunable=TunableStateValueReference())), 'objects_to_teleport_near': TunableList(description='\n A tunable list of static commodities, weights and behavior. When\n choosing where to teleport, objects with higher weights have a\n greater chance of being chosen.\n \n If we fail to find a valid location near an object advertising the\n chosen static commodity, we will search try again with a new object\n until the list has been exhausted.\n ', tunable=TunableTuple(description='\n A static commodity and weight.\n ', weight=TunableRange(description='\n A weight, between 0 and 1, that determines how likely this\n static commodity is to be chosen over the others listed.\n ', tunable_type=float, minimum=0, maximum=1, default=1), static_commodity=TunableReference(description='\n Reference to a type of static commodity.\n ', manager=services.static_commodity_manager()), state_change=OptionalTunable(TunableStateValueReference(description='\n A state value to apply to the object advertising this\n commodity if the teleport succeeds.\n '))))} def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) zone = services.current_zone() if self.when_to_teleport == self.ON_CLIENT_CONNECT and not zone.is_zone_running: zone.register_callback(zone_types.ZoneState.CLIENT_CONNECTED, self.teleport) def teleport(self): if random.random() > self.chance_to_teleport: return if self.required_states is not None: for state in self.required_states: if not self.owner.state_value_active(state): return weights_and_commodities = [(obj_dict.weight, obj_dict.static_commodity, obj_dict.state_change) for obj_dict in self.objects_to_teleport_near] while weights_and_commodities: index = sims4.random._weighted(weights_and_commodities) (_, static_commodity, state_change) = weights_and_commodities.pop(index) motives = set() motives.add(static_commodity) all_objects = list(services.object_manager().valid_objects()) random.shuffle(all_objects) for obj in all_objects: if obj is self.owner: continue if obj.commodity_flags & motives: starting_location = placement.create_starting_location(position=obj.position) if self.owner.is_sim: fgl_context = placement.create_fgl_context_for_sim(starting_location, self.owner) else: fgl_context = placement.create_fgl_context_for_object(starting_location, self.owner) (position, orientation) = placement.find_good_location(fgl_context) if position is not None and orientation is not None: self.owner.transform = sims4.math.Transform(position, orientation) if state_change is not None: obj.set_state(state_change.state, state_change) break
class Baby(GameObject): __qualname__ = 'Baby' MAX_PLACEMENT_ATTEMPTS = 8 BABY_BASSINET_DEFINITION_MAP = TunableMapping( description= '\n The corresponding mapping for each definition pair of empty bassinet\n and bassinet with baby inside. The reason we need to have two of\n definitions is one is deletable and the other one is not.\n ', key_name='Baby', key_type=TunableReference( description= '\n Bassinet with Baby object definition id.\n ', manager=services.definition_manager()), value_name='EmptyBassinet', value_type=TunableReference( description= '\n Bassinet with Baby object definition id.\n ', manager=services.definition_manager())) BASSINET_EMPTY_STATE = TunableStateValueReference( description='\n The state value for an empty bassinet.\n ' ) BASSINET_BABY_STATE = TunableStateValueReference( description= '\n The state value for a non-empty bassinet.\n ') STATUS_STATE = ObjectState.TunableReference( description= '\n The state defining the overall status of the baby (e.g. happy, crying,\n sleeping). We use this because we need to reapply this state to restart\n animations after a load.\n ' ) BABY_SKIN_TONE_STATE_MAPPING = TunableMapping( description= '\n From baby skin tone enum to skin tone state mapping.\n ', key_type=TunableEnumEntry(tunable_type=BabySkinTone, default=BabySkinTone.MEDIUM), value_type=TunableTuple(boy=TunableStateValueReference(), girl=TunableStateValueReference())) BABY_MOOD_MAPPING = TunableMapping( description= '\n From baby state (happy, crying, sleep) to in game mood.\n ', key_type=TunableStateValueReference(), value_type=Mood.TunableReference()) BABY_SKIN_TONE_TO_CAS_SKIN_TONE = TunableMapping( description= '\n From baby skin tone enum to cas skin tone id mapping.\n ', key_type=TunableEnumEntry(tunable_type=BabySkinTone, default=BabySkinTone.MEDIUM), value_type=TunableList( description= '\n The Skin Tones CAS reference under Catalog/InGame/CAS/Skintones.\n ', tunable=TunableSkinTone()), export_modes=ExportModes.All, tuple_name='BabySkinToneToCasTuple') SEND_BABY_TO_DAYCARE_NOTIFICATION_SINGLE_BABY = TunableUiDialogNotificationSnippet( description= '\n The message appearing when a single baby is sent to daycare. You can\n reference this single baby by name.\n ' ) SEND_BABY_TO_DAYCARE_NOTIFICATION_MULTIPLE_BABIES = TunableUiDialogNotificationSnippet( description= '\n The message appearing when multiple babies are sent to daycare. You can\n not reference any of these babies by name.\n ' ) BRING_BABY_BACK_FROM_DAYCARE_NOTIFICATION_SINGLE_BABY = TunableUiDialogNotificationSnippet( description= '\n The message appearing when a single baby is brought back from daycare.\n You can reference this single baby by name.\n ' ) BRING_BABY_BACK_FROM_DAYCARE_NOTIFICATION_MULTIPLE_BABIES = TunableUiDialogNotificationSnippet( description= '\n The message appearing when multiple babies are brought back from\n daycare. You can not reference any of these babies by name.\n ' ) BABY_AGE_UP = TunableTuple( description= '\n Multiple settings for baby age up moment.\n ', age_up_affordance=TunableReference( description= '\n The affordance to run when baby age up to kid.\n ', manager=services.affordance_manager(), class_restrictions='SuperInteraction'), copy_states=TunableList( description= '\n The list of the state we want to copy from the original baby\n bassinet to the new bassinet to play idle.\n ', tunable=TunableReference(manager=services.object_state_manager(), class_restrictions='ObjectState')), idle_state_value=TunableReference( description= '\n The state value to apply on the new baby bassinet with the age up\n special idle animation/vfx linked.\n ', manager=services.object_state_manager(), class_restrictions='ObjectStateValue')) BABY_PLACEMENT_TAGS = TunableList( TunableEnumEntry( tag.Tag, tag.Tag.INVALID, description= '\n Attempt to place the baby near objects with this tag set.\n ' ), description= '\n When trying to place a baby bassinet on the lot, we attempt to place it\n near other objects on the lot. Those objects are determined in priority\n order by this tuned list. It will try to place next to all objects of the\n matching types, before trying to place the baby in the middle of the lot,\n and then finally trying the mailbox. If all FGL placements fail, we put\n the baby into the household inventory.\n ' ) BABY_THUMBNAIL_DEFINITION = TunableReference( description= '\n The thumbnail definition for client use only.\n ', manager=services.definition_manager(), export_modes=(ExportModes.ClientBinary, )) NEGLECTED_STATES = TunableList( description= '\n If the baby enters any of these states, the neglected moment will begin.\n ', tunable=TunableStateValueReference( description= '\n The state to listen for in order to push the neglected moment on the baby.\n ' )) NEGLECT_ANIMATION = TunableReference( description= '\n The animation to play on the baby for the neglect moment.\n ', manager=services.get_instance_manager(sims4.resources.Types.ANIMATION), class_restrictions='ObjectAnimationElement') NEGLECT_NOTIFICATION = UiDialogNotification.TunableFactory( description= '\n The notification to show when the baby neglect moment happens.\n ' ) NEGLECT_EFFECT = Tunable( description= '\n The VFX to play during the neglect moment.\n ', tunable_type=str, default='s40_Sims_neglected') NEGLECT_EMPTY_BASSINET_STATE = TunableStateValueReference( description= '\n The state that will be set on the bassinet when it is emptied due to\n neglect. This should control any reaction broadcasters that we want to\n happen when the baby is taken away. This MUST be tuned.\n ' ) NEGLECT_BUFF_IMMEDIATE_FAMILY = TunableBuffReference( description= "\n The buff to be applied to the baby's immediate family during the \n neglect moment.\n " ) FAILED_PLACEMENT_NOTIFICATION = UiDialogNotification.TunableFactory( description= '\n The notification to show if a baby could not be spawned into the world\n because FGL failed. This is usually due to the player cluttering their\n lot with objects. Token 0 is the baby.\n ' ) @classmethod def get_baby_skin_tone_enum(cls, sim_info): if sim_info.is_baby: skin_tone_id = sim_info.skin_tone for (skin_enum, tone_ids) in cls.BABY_SKIN_TONE_TO_CAS_SKIN_TONE.items(): while skin_tone_id in tone_ids: return skin_enum logger.error( 'Baby with skin tone id {} not in BABY_SKIN_TONE_TO_CAS_SKIN_TONE. Setting light skin tone instead.', skin_tone_id, owner='jjacobson') return BabySkinTone.LIGHT return BabySkinTone.ADULT_SIM @classmethod def get_baby_skin_tone_state(cls, sim_info): skin_tone_state_value = None baby_skin_enum = cls.get_baby_skin_tone_enum(sim_info) if baby_skin_enum is not None and baby_skin_enum in cls.BABY_SKIN_TONE_STATE_MAPPING: skin_state_tuple = cls.BABY_SKIN_TONE_STATE_MAPPING[baby_skin_enum] if sim_info.gender == Gender.FEMALE: skin_tone_state_value = skin_state_tuple.girl elif sim_info.gender == Gender.MALE: skin_tone_state_value = skin_state_tuple.boy return skin_tone_state_value @classmethod def get_corresponding_definition(cls, definition): if definition in cls.BABY_BASSINET_DEFINITION_MAP: return cls.BABY_BASSINET_DEFINITION_MAP[definition] for (baby_def, bassinet_def) in cls.BABY_BASSINET_DEFINITION_MAP.items(): while bassinet_def is definition: return baby_def @classmethod def get_default_baby_def(cls): return next(iter(cls.BABY_BASSINET_DEFINITION_MAP), None) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._sim_info = None self.state_component.state_trigger_enabled = False self._started_neglect_moment = False self._ignore_daycare = False def set_sim_info(self, sim_info, ignore_daycare=False): self._sim_info = sim_info self._ignore_daycare = ignore_daycare if self._sim_info is not None: self.state_component.state_trigger_enabled = True self.enable_baby_state() @property def sim_info(self): return self._sim_info @property def is_selectable(self): return self.sim_info.is_selectable @property def sim_id(self): if self._sim_info is not None: return self._sim_info.sim_id return self.id @property def household_id(self): if self._sim_info is not None: return self._sim_info.household.id return 0 def populate_localization_token(self, *args, **kwargs): if self.sim_info is not None: return self.sim_info.populate_localization_token(*args, **kwargs) logger.warn( 'self.sim_info is None in baby.populate_localization_token', owner='epanero', trigger_breakpoint=True) return super().populate_localization_token(*args, **kwargs) def enable_baby_state(self): if self._sim_info is None: return self.set_state(self.BASSINET_BABY_STATE.state, self.BASSINET_BABY_STATE) status_state = self.get_state(self.STATUS_STATE) self.set_state(status_state.state, status_state, force_update=True) skin_tone_state = self.get_baby_skin_tone_state(self._sim_info) if skin_tone_state is not None: self.set_state(skin_tone_state.state, skin_tone_state) def empty_baby_state(self): self.set_state(self.BASSINET_EMPTY_STATE.state, self.BASSINET_EMPTY_STATE) def on_state_changed(self, state, old_value, new_value): super().on_state_changed(state, old_value, new_value) if new_value in self.NEGLECTED_STATES and not self._started_neglect_moment: start_baby_neglect(self) elif self.manager is not None and new_value in self.BABY_MOOD_MAPPING: mood = self.BABY_MOOD_MAPPING[new_value] mood_msg = Commodities_pb2.MoodUpdate() mood_msg.sim_id = self.id mood_msg.mood_key = mood.guid64 mood_msg.mood_intensity = 1 distributor.shared_messages.add_object_message( self, MSG_SIM_MOOD_UPDATE, mood_msg, False) def load_object(self, object_data): super().load_object(object_data) self._sim_info = services.sim_info_manager().get(self.sim_id) def on_finalize_load(self): sim_info = services.sim_info_manager().get(self.sim_id) if sim_info is None or sim_info.household is not services.active_lot( ).get_household(): _replace_bassinet(sim_info, bassinet=self) else: self.set_sim_info(sim_info)
class ObjectCreationElement(XevtTriggeredElement): __qualname__ = 'ObjectCreationElement' POSITION = 'position' INVENTORY = 'inventory' SLOT = 'slot' class ObjectDefinition(HasTunableSingletonFactory, AutoFactoryInit): __qualname__ = 'ObjectCreationElement.ObjectDefinition' FACTORY_TUNABLES = { 'definition': TunableReference( description= '\n The definition of the object that is created.\n ', manager=services.definition_manager()) } def get_definition(self): return self.definition def setup_created_object(self, interaction, created_object): pass class RecipeDefinition(HasTunableSingletonFactory, AutoFactoryInit): __qualname__ = 'ObjectCreationElement.RecipeDefinition' FACTORY_TUNABLES = { 'recipe': TunableReference( description= '\n The recipe to use to create the object.\n ', manager=services.recipe_manager()) } def get_definition(self): return self.recipe.final_product.definition def setup_created_object(self, interaction, created_object): from crafting.crafting_process import CraftingProcess crafting_process = CraftingProcess(crafter=interaction.sim, recipe=self.recipe) crafting_process.setup_crafted_object(created_object, is_final_product=True) FACTORY_TUNABLES = { 'description': '\n Create an object as part of an interaction.\n ', 'creation_data': TunableVariant( description='\n Define what to create.\n ', definition=ObjectDefinition.TunableFactory(), recipe=RecipeDefinition.TunableFactory(), default='definition'), 'initial_states': TunableList( description= '\n A list of states to apply to the object as soon as it is created.\n ', tunable=TunableStateValueReference()), 'destroy_on_placement_failure': Tunable( description= '\n If checked, the created object will be destroyed on placement failure.\n If unchecked, the created object will be placed into an appropriate\n inventory on placement failure if possible. If THAT fails, object\n will be destroyed.\n ', tunable_type=bool, default=False), 'cancel_on_destroy': Tunable( description= '\n If checked, the interaction will be canceled if object is destroyed\n due to placement failure or if destroy on placement failure is\n unchecked and the fallback fails.\n ', tunable_type=bool, default=True), 'transient': Tunable( description= '\n If checked, the created object will be destroyed when the interaction ends.\n ', tunable_type=bool, default=False), 'location': TunableVariant( description= '\n Where the object should be created.\n ', default='position', position=TunableTuple( description= '\n An in-world position based off of the chosen Participant Type.\n ', locked_args={'location': POSITION}, location_target=TunableEnumEntry( description= '\n Who or what to create this object next to.\n ', tunable_type=ParticipantType, default=ParticipantType.Actor), offset_tuning=TunableTuple( default_offset=TunableVector3( description= "\n The default Vector3 offset from the location target's\n position.\n ", default=sims4.math.Vector3.ZERO()), x_randomization_range=OptionalTunable( TunableInterval( description= '\n A random number in this range will be applied to the\n default offset along the x axis.\n ', tunable_type=float, default_lower=0, default_upper=0)), z_randomization_range=OptionalTunable( TunableInterval( description= '\n A random number in this range will be applied to the\n default offset along the z axis.\n ', tunable_type=float, default_lower=0, default_upper=0))), ignore_bb_footprints=Tunable( description= '\n Ignores the build buy object footprints when trying to find\n a position for creating this object. This will allow \n objects to appear on top of each other.\n e.g. Trash cans when tipped over want to place the trash \n right under them so it looks like the pile came out from \n the object while it was tipped.\n ', tunable_type=bool, default=True), allow_off_lot_placement=Tunable( description= '\n If checked, objects will be allowed to be placed off-lot.\n If unchecked, we will always attempt to place created\n objects on the active lot.\n ', tunable_type=bool, default=False)), inventory=TunableTuple( description= '\n An inventory based off of the chosen Participant Type.\n ', locked_args={'location': INVENTORY}, location_target=TunableEnumEntry( description= '\n "The owner of the inventory the object will be created in."\n ', tunable_type=ParticipantType, default=ParticipantType.Actor)), slot=TunableTuple( description= '\n Slot the object into the specified slot on the tuned location_target.\n ', locked_args={'location': SLOT}, location_target=TunableEnumEntry( description= '\n The object which will contain the specified slot.\n ', tunable_type=ParticipantType, default=ParticipantType.Object), parent_slot=TunableVariant( description= '\n The slot on location_target where the object should go. This\n may be either the exact name of a bone on the location_target or a\n slot type, in which case the first empty slot of the specified type\n in which the child object fits will be used.\n ', by_name=Tunable( description= '\n The exact name of a slot on the location_target in which the target\n object should go. \n ', tunable_type=str, default='_ctnm_'), by_reference=TunableReference( description= '\n A particular slot type in which the target object should go. The\n first empty slot of this type found on the location_target will be used.\n ', manager=services.get_instance_manager( sims4.resources.Types.SLOT_TYPE))))), 'reserve_object': OptionalTunable( description= '\n If this is enabled, the created object will be reserved for use by\n the set Sim.\n ', tunable=TunableEnumEntry( tunable_type=ParticipantTypeActorTargetSim, default=ParticipantTypeActorTargetSim.Actor)) } def __init__(self, interaction, *args, sequence=(), **kwargs): super().__init__(interaction, sequence=sequence, *args, **kwargs) self._placement_failed = False if self.reserve_object is not None: reserved_sim = self.interaction.get_participant( self.reserve_object) else: reserved_sim = None self._object_helper = CreateObjectHelper( reserved_sim, self.definition, self, init=self._setup_created_object) @property def definition(self): return self.creation_data.get_definition() @property def placement_failed(self): return self._placement_failed def create_object(self): created_object = create_object(self.definition, init=self._setup_created_object, post_add=self._place_object) if self._placement_failed: created_object.destroy( source=self.interaction, cause='Failed to place object created by basic extra.') return return created_object def _build_outer_elements(self, sequence): return self._object_helper.create(sequence) def _do_behavior(self): self._place_object(self._object_helper.object) if self._placement_failed: if self.cancel_on_destroy: self.interaction.cancel( FinishingType.FAILED_TESTS, cancel_reason_msg='Cannot place object') return False return True if not self.transient: self._object_helper.claim() return True def _setup_created_object(self, created_object): self.creation_data.setup_created_object(self.interaction, created_object) for initial_state in self.initial_states: created_object.set_state(initial_state.state, initial_state) def _get_ignored_object_ids(self): pass def _place_object_no_fallback(self, created_object): participant = self.interaction.get_participant( self.location.location_target) if self.location.location == self.POSITION: offset_tuning = self.location.offset_tuning default_offset = sims4.math.Vector3(offset_tuning.default_offset.x, offset_tuning.default_offset.y, offset_tuning.default_offset.z) x_range = offset_tuning.x_randomization_range z_range = offset_tuning.z_randomization_range start_orientation = sims4.random.random_orientation() if x_range is not None: x_axis = start_orientation.transform_vector( sims4.math.Vector3.X_AXIS()) default_offset += x_axis * random.uniform( x_range.lower_bound, x_range.upper_bound) if z_range is not None: z_axis = start_orientation.transform_vector( sims4.math.Vector3.Z_AXIS()) default_offset += z_axis * random.uniform( z_range.lower_bound, z_range.upper_bound) offset = sims4.math.Transform(default_offset, sims4.math.Quaternion.IDENTITY()) start_position = sims4.math.Transform.concatenate( offset, participant.transform).translation routing_surface = participant.routing_surface active_lot = services.active_lot() search_flags = placement.FGLSearchFlagsDefault if self.location.allow_off_lot_placement and not active_lot.is_position_on_lot( start_position): created_object.location = sims4.math.Location( sims4.math.Transform(start_position, start_orientation), routing_surface) polygon = placement.get_accurate_placement_footprint_polygon( created_object.position, created_object.orientation, created_object.scale, created_object.get_footprint()) context = placement.FindGoodLocationContext( starting_position=start_position, starting_orientation=start_orientation, starting_routing_surface=routing_surface, ignored_object_ids=(created_object.id, ), search_flags=search_flags, object_polygons=(polygon, )) else: if not self.location.ignore_bb_footprints: search_flags |= placement.FGLSearchFlag.SHOULD_TEST_BUILDBUY | placement.FGLSearchFlag.STAY_IN_CURRENT_BLOCK if not active_lot.is_position_on_lot(start_position): start_position = active_lot.get_default_position( position=start_position) context = placement.FindGoodLocationContext( starting_position=start_position, starting_orientation=start_orientation, starting_routing_surface=routing_surface, object_id=created_object.id, ignored_object_ids=self._get_ignored_object_ids(), search_flags=search_flags, object_footprints=(self.definition.get_footprint(0), )) (translation, orientation) = placement.find_good_location(context) if translation is not None: created_object.move_to(routing_surface=routing_surface, translation=translation, orientation=orientation) return True elif self.location.location == self.SLOT: parent_slot = self.location.parent_slot if participant.slot_object(parent_slot=parent_slot, slotting_object=created_object): return True return False def _place_object(self, created_object): if self._place_object_no_fallback(created_object): return True if not self.destroy_on_placement_failure: participant = self.interaction.get_participant( self.location.location_target) if participant.inventory_component is not None and created_object.inventoryitem_component is not None: if participant.is_sim: participant_household_id = participant.household.id else: participant_household_id = participant.get_household_owner_id( ) created_object.set_household_owner_id(participant_household_id) participant.inventory_component.system_add_object( created_object, participant) return True sim = self.interaction.sim if sim is not None: if not sim.household.is_npc_household: try: created_object.set_household_owner_id(sim.household.id) build_buy.move_object_to_household_inventory( created_object) return True except KeyError: pass self._placement_failed = True return False
class RetailComponent(Component, HasTunableFactory, AutoFactoryInit, component_name=types.RETAIL_COMPONENT, persistence_priority=ComponentPriority.PRIORITY_RETAIL, persistence_key=SimObjectAttributes_pb2. PersistenceMaster.PersistableData.RetailComponent): FACTORY_TUNABLES = { 'sellable': OptionalTunable( description= '\n If enabled, this object can be sold on a retail lot.\n ', disabled_name='Not_For_Sale', enabled_by_default=True, tunable=TunableTuple( description= '\n The data associated with selling this item.\n ', placard_override=OptionalTunable( description= '\n If enabled, we will only use this placard when this object\n is sold. If disabled, the game will attempt to smartly\n choose a placard.\n ', tunable=TunableTuple( description= '\n The placard and vfx to use for this object.\n ', model=TunablePackSafeReference( description= '\n The placard to use when this object is sold.\n ', manager=services.definition_manager()), vfx=PlayEffect.TunableFactory( description= '\n The effect to play when the object is sold.\n ' ))), for_sale_extra_affordances=TunableList( description= "\n When this object is marked For Sale, these are the extra\n interactions that will still be available. For instance, a\n debug interaction to sell the object could go here or you\n may want Sit to still be on chairs so customers can try\n them out. You may also want Clean on anything that can get\n dirty so employees can clean them.\n \n Note: These interactions are specific to this object. Do not\n add interactions that don't make sense for this object.\n ", tunable=TunableReference( description= '\n The affordance to be left available on the object.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION), pack_safe=True)), buy_affordance=TunablePackSafeReference( description= '\n The affordance a Sim will run to buy retail objects. This\n affordance should handle destroying the object and adding\n the money to the retail funds.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION)), restock_affordances=TunableList( description= '\n The affordances a Sim will run to restock retail objects.\n ', tunable=TunableReference( manager=services.get_instance_manager( sims4.resources.Types.INTERACTION), pack_safe=True)), clear_placard_affordance=TunablePackSafeReference( description= '\n The affordance a Sim will run to remove a placard from the lot.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION), allow_none=True), browse_affordance=TunablePackSafeReference( description= '\n The affordance a Sim will run to browse retail objects.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION)), set_for_sale_affordance=TunablePackSafeReference( description= '\n The affordance a Sim will run to set a retail object as For\n Sale.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION), allow_none=True), set_not_for_sale_affordance=TunablePackSafeReference( description= '\n The affordance a Sim will run to set a retail object as Not\n For Sale.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION), allow_none=True), allowed_occupied_slots_for_sellable=TunableSet( description= '\n By default, an object will become unsellable if anything is\n parented to it. This is to prevent awkward scenarios such\n as selling a counter with a sink is parented, leaving the\n sink floating in mid-air. However, some slots on objects\n are okay occupied, e.g. a table should still be sellable if\n chairs are parented. Adding those slots to this set (e.g.\n slot_SitChair on tables) will allow this object to remain\n sellable even if those slots are occupied.\n ', tunable=SlotType.TunableReference()))), 'advertises': OptionalTunable( description= '\n If enabled, this object will contribute to the curb appeal for the\n retail lot on which it is placed. Curb appeal affects foot traffic\n for the store as well as the type of clientele.\n ', disabled_name='Does_Not_Advertise', enabled_by_default=True, tunable=RetailCurbAppeal.TunableFactory()) } NOT_FOR_SALE_STATE = TunableStateValueReference( description= '\n The state value that represents and object, on a retail lot, that is\n not for sale at all. Objects in this state should function like normal.\n ', pack_safe=True) FOR_SALE_STATE = TunableStateValueReference( description= '\n The state value that represents an object that is valid for sale on a\n retail lot. This is the state that will be tested in order to show sale\n interactions.\n ', pack_safe=True) SOLD_STATE = TunableStateValueReference( description= "\n The state value that represents an object that is no longer valid for\n sale on a retail lot. This is the state that will be set on the object\n when it's sold and in its Placard form.\n ", pack_safe=True) DEFAULT_SALE_STATE = TunableStateValueReference( description= '\n This is the default for sale state for an object. When it is given a\n retail component for this first time, this is what the For Sale state\n will be set to.\n ', pack_safe=True) SET_FOR_SALE_VFX = PlayEffect.TunableFactory( description= '\n An effect that will play on an object when it gets set for sale.\n ' ) SET_NOT_FOR_SALE_VFX = PlayEffect.TunableFactory( description= '\n An effect that will play on an object when it gets set not for sale.\n ' ) PLACARD_FLOOR = TunableTuple( description= '\n The placard to use, and vfx to show, for objects that were on the floor\n when sold.\n ', model=TunableReference( description= '\n The placard to use when the object is sold.\n ', manager=services.definition_manager(), pack_safe=True), vfx=PlayEffect.TunableFactory( description= '\n The effect to play when the object is sold.\n ' )) PLACARD_SURFACE = TunableTuple( description= '\n The placard to use, and vfx to show, for objects that were on a surface\n when sold.\n ', model=TunableReference( description= '\n The placard to use when the object is sold.\n ', manager=services.definition_manager(), pack_safe=True), vfx=PlayEffect.TunableFactory( description= '\n The effect to play when the object is sold.\n ' )) PLACARD_WALL = TunableTuple( description= '\n The placard to use, and vfx to show, for objects that were on the wall\n when sold.\n ', model=TunableReference( description= '\n The placard to use when the object is sold.\n ', manager=services.definition_manager(), pack_safe=True), vfx=PlayEffect.TunableFactory( description= '\n The effect to play when the object is sold.\n ' )) PLACARD_CEILING = TunableTuple( description= '\n The placard to use, and vfx to show, for objects that were on the\n ceiling when sold.\n ', model=TunableReference( description= '\n The placard to use when the object is sold.\n ', manager=services.definition_manager(), pack_safe=True), vfx=PlayEffect.TunableFactory( description= '\n The effect to play when the object is sold.\n ' )) UNINTERACTABLE_AUTONOMY_MODIFIER = TunableAutonomyModifier( description= '\n Autonomy modifier that disables interactions on this object. Applied\n to this object and its children when marked as sellable or sold.\n ', locked_args={'relationship_multipliers': None}) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._cached_value = None self._uninteractable_autonomy_modifier_handle = None @classproperty def required_packs(cls): return (Pack.EP01, ) def on_add(self): if self.owner.state_component is None: self.owner.add_component( StateComponent(self.owner, states=(), state_triggers=(), unique_state_changes=None, delinquency_state_changes=None, timed_state_triggers=None)) self.owner.set_state_dynamically(state=self.DEFAULT_SALE_STATE.state, new_value=self.DEFAULT_SALE_STATE, seed_value=self.SOLD_STATE) self.owner.update_component_commodity_flags() def save(self, persistence_master_message): persistable_data = SimObjectAttributes_pb2.PersistenceMaster.PersistableData( ) persistable_data.type = SimObjectAttributes_pb2.PersistenceMaster.PersistableData.RetailComponent retail_data = persistable_data.Extensions[ SimObjectAttributes_pb2.PersistableRetailComponent. persistable_data] if self._cached_value is not None: retail_data.cached_value = self._cached_value persistence_master_message.data.extend([persistable_data]) def load(self, persistence_master_message): retail_data = persistence_master_message.Extensions[ SimObjectAttributes_pb2.PersistableRetailComponent. persistable_data] if retail_data.HasField('cached_value'): self._cached_value = retail_data.cached_value def get_current_curb_appeal(self): curb_appeal = 0 if self.advertises: curb_appeal = self.advertises.base_curb_appeal curb_appeal += sum([ curb_appeal for (state, curb_appeal ) in self.advertises.state_based_curb_appeal.items() if self.owner.state_value_active(state) ]) return curb_appeal def on_state_changed(self, state, old_value, new_value, from_init): reapply_state = False if old_value is new_value: if state is not self.FOR_SALE_STATE.state: return reapply_state = True retail_manager = services.business_service( ).get_retail_manager_for_zone() if retail_manager is None: return if new_value is self.NOT_FOR_SALE_STATE or new_value is self.DEFAULT_SALE_STATE: show_vfx = not reapply_state and ( old_value is self.FOR_SALE_STATE or old_value is self.DEFAULT_SALE_STATE) if show_vfx: if self.owner.swapping_to_parent is not None: if self.FOR_SALE_STATE in self.owner.swapping_to_parent.slot_component.state_values: show_vfx = False self._set_not_for_sale_internal(show_vfx=show_vfx) elif new_value is self.FOR_SALE_STATE: show_vfx = not reapply_state and ( old_value is self.NOT_FOR_SALE_STATE or old_value is self.DEFAULT_SALE_STATE) if show_vfx: if self.owner.swapping_to_parent is not None: if self.NOT_FOR_SALE_STATE in self.owner.swapping_to_parent.slot_component.state_values: show_vfx = False self._set_for_sale_internal(show_vfx=show_vfx) if old_value is self.SOLD_STATE: self.owner.remove_client_state_suppressor(state) self.owner.reset_states_to_default() elif new_value is self.SOLD_STATE: self.owner.add_client_state_suppressor(self.SOLD_STATE.state) self._set_sold_internal() self.owner.update_component_commodity_flags() def on_added_to_inventory(self): inventory = self.owner.get_inventory() if inventory is None: return if inventory.inventory_type in RetailUtils.RETAIL_INVENTORY_TYPES: if not self.is_sold: self.set_for_sale() else: if self.owner.get_state( self.DEFAULT_SALE_STATE.state) == self.DEFAULT_SALE_STATE: return if not self.is_sold: self.set_not_for_sale() def on_child_added(self, child, location): if self.is_for_sale and not self.is_allowed_slot(location.slot_hash): self.set_not_for_sale() child_retail = child.retail_component if child_retail is not None and self.is_for_sale_or_sold: child_retail.set_uninteractable() def on_child_removed(self, child, new_parent=None): child_retail = child.retail_component if child_retail is not None: child_retail.set_interactable(from_unparent=True) def on_finalize_load(self): self.owner.update_component_commodity_flags() retail_manager = services.business_service( ).get_retail_manager_for_zone() if retail_manager is None: if self.is_sold or self.is_for_sale: self.set_not_for_sale() elif self.is_sold: self.owner.add_client_state_suppressor(self.SOLD_STATE.state) @property def is_for_sale(self): return self.owner.state_value_active(self.FOR_SALE_STATE) @property def is_sold(self): return self.owner.state_value_active(self.SOLD_STATE) @property def is_not_for_sale(self): return self.owner.state_value_active( self.NOT_FOR_SALE_STATE) or self.owner.state_value_active( self.DEFAULT_SALE_STATE) @property def is_for_sale_or_sold(self): return not self.is_not_for_sale def get_retail_value(self): obj = self.owner crafting_component = obj.get_component(types.CRAFTING_COMPONENT) if crafting_component is None: return obj.catalog_value if self._cached_value is None: return obj.current_value state_component = obj.state_component if state_component is not None: return max( round(self._cached_value * state_component.state_based_value_mod), 0) else: return self._cached_value def get_sell_price(self): base_value = self.get_retail_value() retail_manager = services.business_service( ).get_retail_manager_for_zone() if retail_manager is None: logger.error( "Trying to get the sell price of a retail item but no retail_manager was found for this lot. Defaulting to the object's value without a markup applied." ) return base_value return retail_manager.get_value_with_markup(base_value) def get_buy_affordance(self): sellable_data = self.sellable if sellable_data is not None: return sellable_data.buy_affordance def is_allowed_slot(self, slot_hash): if not self.sellable: return False for runtime_slot in self.owner.get_runtime_slots_gen( bone_name_hash=slot_hash): if self.sellable.allowed_occupied_slots_for_sellable & runtime_slot.slot_types: return True return False def get_can_be_sold(self): if not self.sellable: return False elif any(not self.is_allowed_slot(child.slot_hash) for child in self.owner.children): return False return True def set_not_for_sale(self): self.owner.set_state(self.NOT_FOR_SALE_STATE.state, self.NOT_FOR_SALE_STATE) def set_for_sale(self): self.owner.set_state(self.FOR_SALE_STATE.state, self.FOR_SALE_STATE) def _set_sold_internal(self): retail_manager = services.business_service( ).get_retail_manager_for_zone() if retail_manager is None: logger.error( 'Trying to set an item as sold but the retail manager is None.' ) return item = self.owner self._handle_children_of_sold_object(item) self._change_to_placard() item.on_set_sold() def set_uninteractable(self, propagate_to_children=False): if self._uninteractable_autonomy_modifier_handle is not None: return self._uninteractable_autonomy_modifier_handle = self.owner.add_statistic_modifier( self.UNINTERACTABLE_AUTONOMY_MODIFIER) if propagate_to_children: for child in self.owner.children: retail_component = child.retail_component if retail_component is not None: child.retail_component.set_uninteractable( propagate_to_children=False) def set_interactable(self, propagate_to_children=False, from_unparent=False): if self._uninteractable_autonomy_modifier_handle is None: return if not from_unparent: parent = self.owner.parent if parent is not None: parent_retail = parent.retail_component if parent_retail is not None and parent_retail.is_for_sale_or_sold: return self.owner.remove_statistic_modifier( self._uninteractable_autonomy_modifier_handle) self._uninteractable_autonomy_modifier_handle = None if propagate_to_children: for child in self.owner.children: retail_component = child.retail_component if retail_component is not None: if not retail_component.is_for_sale_or_sold: child.retail_component.set_interactable( propagate_to_children=False) def _set_not_for_sale_internal(self, show_vfx=True): self.set_interactable(propagate_to_children=True) retail_manager = services.business_service( ).get_retail_manager_for_zone() if retail_manager is None: return retail_manager.refresh_for_sale_vfx_for_object(self.owner) if show_vfx: self.SET_NOT_FOR_SALE_VFX(self.owner).start_one_shot() def _set_for_sale_internal(self, show_vfx=True): self.set_uninteractable(propagate_to_children=True) item = self.owner if item.standin_model is not None: item.standin_model = None item.on_restock() if self._cached_value is not None: item.base_value = self._cached_value self._cached_value = None retail_manager = services.business_service( ).get_retail_manager_for_zone() if retail_manager is None: logger.error( "Trying to set an item For Sale but it's not on a valid retail lot." ) return False retail_manager.refresh_for_sale_vfx_for_object(self.owner) if show_vfx: self.SET_FOR_SALE_VFX(self.owner).start_one_shot() return True def _choose_placard_info(self): placard_override = self.sellable.placard_override if placard_override is not None: if placard_override.model is not None: return placard_override logger.error( 'Object [{}] has a placard override enabled on the retail component [{}] but the placard override has no model set. We will attempt to pick the correct placard.', self.owner, self) item = self.owner if item.parent is not None: return self.PLACARD_SURFACE if item.wall_or_fence_placement: return self.PLACARD_WALL if item.ceiling_placement: return self.PLACARD_CEILING else: return self.PLACARD_FLOOR def _change_to_placard(self, play_vfx=True): item = self.owner if self._cached_value is None: self._cached_value = item.base_value item.base_value = 0 placard_info = self._choose_placard_info() item.standin_model = placard_info.model.definition.get_model(0) item.set_state(self.SOLD_STATE.state, self.SOLD_STATE) if play_vfx: effect = placard_info.vfx(item) effect.start_one_shot() retail_manager = services.business_service( ).get_retail_manager_for_zone() if retail_manager is None: logger.error( "Trying to change a retail object to a placard but it's not on a valid retail lot." ) return False retail_manager.refresh_for_sale_vfx_for_object(self.owner) def _handle_children_of_sold_object(self, obj): retail_manager = services.business_service( ).get_retail_manager_for_zone() active_household_is_owner = retail_manager.is_owner_household_active should_show_notification = False for child in tuple(obj.children): if child.has_component(types.RETAIL_COMPONENT): child.retail_component.set_not_for_sale() if active_household_is_owner: should_show_notification = True build_buy.move_object_to_household_inventory(child) else: child.schedule_destroy_asap() if should_show_notification: notification = retail_manager.ITEMS_SENT_TO_HH_INVENTORY_NOTIFICATION( obj) notification.show_dialog() def component_super_affordances_gen(self, **kwargs): retail_manager = services.business_service( ).get_retail_manager_for_zone() if retail_manager is None: return sellable_data = self.sellable if sellable_data is not None: if self.is_for_sale: yield from sellable_data.for_sale_extra_affordances if sellable_data.set_not_for_sale_affordance is not None: yield sellable_data.set_not_for_sale_affordance if retail_manager.is_open: if sellable_data.buy_affordance is not None: yield sellable_data.buy_affordance if sellable_data.browse_affordance is not None: yield sellable_data.browse_affordance elif self.is_not_for_sale: if sellable_data.set_for_sale_affordance is not None and self.get_can_be_sold( ): yield sellable_data.set_for_sale_affordance elif self.is_sold: yield from sellable_data.restock_affordances if sellable_data.clear_placard_affordance is not None: yield sellable_data.clear_placard_affordance def modify_interactable_flags(self, interactable_flag_field): if self.is_for_sale: if not self.is_sold: interactable_flag_field.flags |= interaction_protocol.Interactable.FORSALE
class InventoryTuning: INVALID_ACCESS_STATES = TunableList( TunableStateValueReference( description= '\n If an inventory owner is in any of the states tuned here, it will not\n be available to grab objects out of.\n ', pack_safe=True))
class StateChangeLootOp(BaseLootOperation): __qualname__ = 'StateChangeLootOp' FACTORY_TUNABLES = {'description': '\n This loot will change the state of the subject.\n ', 'state_value': TunableStateValueReference()} def __init__(self, state_value, **kwargs): super().__init__(**kwargs) self.state_value = state_value def _apply_to_subject_and_target(self, subject, target, resolver): subject_obj = self._get_object_from_recipient(subject) if subject_obj is not None: state_value = self.state_value subject_obj.set_state(state_value.state, state_value)
class ModifyAllLotItems(HasTunableFactory, AutoFactoryInit): DESTROY_OBJECT = 0 SET_STATE = 1 INVENTORY_TRANSFER = 2 DELIVER_BILLS = 3 SET_ON_FIRE = 4 CLEANUP_VEHICLE = 5 LOOT = 6 FACTORY_TUNABLES = { 'description': '\n Tune modifications to apply to all objects on a lot.\n Can do state changes, destroy certain items, etc.\n \n EX: for auto cleaning, tune to have objects with Dirtiness state that\n equals dirty to be set to the clean state and tune to have dirty dishes\n and spoiled food to be deleted\n ', 'modifications': TunableList( description= "\n A list of where the elements define how to modify objects on the\n lot. Each entry is a triplet of an object modification action\n (currently either destroy the object or set its state), a list of\n tests to run on the object to determine if we should actually apply\n the modification, and a priority in case some modifications should\n take precedence over other ones when both of their tests pass.\n \n EX: test list: object's dirtiness state != dirtiness clean\n action: set state to Dirtiness_clean\n \n So dirty objects will become clean\n ", tunable=TunableTuple( action=TunableVariant( set_state=TunableTuple( action_value=TunableStateValueReference( description='An object state to set the object to', pack_safe=True), locked_args={'action_type': SET_STATE}), destroy_object=TunableTuple( locked_args={'action_type': DESTROY_OBJECT}), inventory_transfer=TunableTuple( action_value=InventoryTransferFakePerform. TunableFactory(), locked_args={'action_type': INVENTORY_TRANSFER}), deliver_bills=TunableTuple( action_value=DeliverBillFakePerform.TunableFactory(), locked_args={'action_type': DELIVER_BILLS}), set_on_fire=TunableTuple( locked_args={'action_type': SET_ON_FIRE}), cleanup_vehicle=TunableTuple( description= '\n Cleanup vehicles that are left around.\n ', locked_args={'action_type': CLEANUP_VEHICLE}), loot=TunableTuple( description= '\n Apply loots to the object.\n ', loot_actions=TunableSet( description= '\n Loot(s) to apply.\n ', tunable=TunableReference( manager=services .get_instance_manager( Types.ACTION), pack_safe =True)), locked_args={'action_type': LOOT})), chance=TunablePercent( description= '\n Chance this modification will occur.\n ', default=100, minimum=1), global_tests=TunableObjectModifyGlobalTestList( description= "\n Non object-related tests that gate this modification from occurring. Use this for any global\n tests that don't require the object, such as zone/location/time-elapsed tests. These tests\n will run only ONCE for this action, unlike 'Tests', which runs PER OBJECT. \n " ), tests=TunableObjectModifyTestSet( description= '\n All least one subtest group (AKA one list item) must pass\n within this list before the action associated with this\n tuning will be run.\n ', additional_tests={ 'elapsed_time': TimeElapsedZoneTest.TunableFactory( locked_args={'tooltip': None}), 'statistic': StatThresholdTest.TunableFactory( locked_args={'tooltip': None}) }), weighted_tests=TunableList( description= '\n Weighted tests for the individual object. One is chosen \n based on weight, and all objects are run against that chosen\n test set.\n ', tunable=TunableTuple( tests=TunableObjectModifyTestSet( description= '\n All least one subtest group (AKA one list item) must pass\n within this list before the action associated with this\n tuning will be run.\n ', additional_tests={ 'elapsed_time': TimeElapsedZoneTest.TunableFactory( locked_args={'tooltip': None}), 'statistic': StatThresholdTest.TunableFactory( locked_args={'tooltip': None}) }), weight=TunableRange( description= '\n Weight to use.\n ', tunable_type=int, default=1, minimum=1))))) } def modify_objects(self, object_criteria=None): objects_to_destroy = [] num_modified = 0 modifications = defaultdict(CompoundTestList) for mod in self.modifications: if not random_chance(mod.chance * 100): continue if mod.global_tests and not mod.global_tests.run_tests( GlobalResolver()): continue if mod.tests: modifications[mod.action].extend(mod.tests) if mod.weighted_tests: weighted_tests = [] for test_weight_pair in mod.weighted_tests: weighted_tests.append( (test_weight_pair.weight, test_weight_pair.tests)) modifications[mod.action].extend( weighted_random_item(weighted_tests)) if not modifications: return num_modified all_objects = list(services.object_manager().values()) for obj in all_objects: if obj.is_sim: continue if object_criteria is not None and not object_criteria(obj): continue resolver = SingleObjectResolver(obj) modified = False for (action, tests) in modifications.items(): if not tests.run_tests(resolver): continue modified = True action_type = action.action_type if action_type == ModifyAllLotItems.DESTROY_OBJECT: objects_to_destroy.append(obj) break elif action_type == ModifyAllLotItems.SET_STATE: new_state_value = action.action_value if obj.state_component and obj.has_state( new_state_value.state): obj.set_state(new_state_value.state, new_state_value, immediate=True) if action_type in ( ModifyAllLotItems.INVENTORY_TRANSFER, ModifyAllLotItems.DELIVER_BILLS): element = action.action_value() element._do_behavior() elif action_type == ModifyAllLotItems.SET_ON_FIRE: fire_service = services.get_fire_service() fire_service.spawn_fire_at_object(obj) elif action_type == ModifyAllLotItems.CLEANUP_VEHICLE: if self._should_cleanup_vehicle(obj): objects_to_destroy.append(obj) if action_type == ModifyAllLotItems.LOOT: for loot_action in action.loot_actions: loot_action.apply_to_resolver(resolver) else: raise NotImplementedError else: raise NotImplementedError elif action_type == ModifyAllLotItems.LOOT: for loot_action in action.loot_actions: loot_action.apply_to_resolver(resolver) else: raise NotImplementedError else: raise NotImplementedError elif action_type in (ModifyAllLotItems.INVENTORY_TRANSFER, ModifyAllLotItems.DELIVER_BILLS): element = action.action_value() element._do_behavior() elif action_type == ModifyAllLotItems.SET_ON_FIRE: fire_service = services.get_fire_service() fire_service.spawn_fire_at_object(obj) elif action_type == ModifyAllLotItems.CLEANUP_VEHICLE: if self._should_cleanup_vehicle(obj): objects_to_destroy.append(obj) if action_type == ModifyAllLotItems.LOOT: for loot_action in action.loot_actions: loot_action.apply_to_resolver(resolver) else: raise NotImplementedError else: raise NotImplementedError elif action_type == ModifyAllLotItems.LOOT: for loot_action in action.loot_actions: loot_action.apply_to_resolver(resolver) else: raise NotImplementedError else: raise NotImplementedError if modified: num_modified += 1 for obj in objects_to_destroy: obj.destroy(source=self, cause='Destruction requested by modify lot tuning') objects_to_destroy = None return num_modified def _should_cleanup_vehicle(self, obj): vehicle_component = obj.get_component(VEHICLE_COMPONENT) if vehicle_component is None: return False household_owner_id = obj.get_household_owner_id() if household_owner_id is not None and household_owner_id != 0: return False elif obj.interaction_refs: return False return True
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 ObjectRelationshipComponent( Component, HasTunableFactory, AutoFactoryInit, component_name=types.OBJECT_RELATIONSHIP_COMPONENT, persistence_key=protocols.PersistenceMaster.PersistableData. ObjectRelationshipComponent): FACTORY_TUNABLES = { 'number_of_allowed_relationships': OptionalTunable( description= '\n Number of Sims who can have a relationship with this object at one\n time. If not specified, an infinite number of Sims can have a \n relationship with the object.\n ', tunable=TunableRange(tunable_type=int, default=1, minimum=1)), 'icon_override': OptionalTunable( description= "\n If enabled, this will override the object's thumbnail generated \n default icon on Relationship panel.\n ", tunable=TunableResourceKey( description= '\n The icon to be displayed in the Relationship panel.\n ', resource_types=sims4.resources.CompoundTypes.IMAGE, export_modes=ExportModes.All)), 'relationship_stat': Statistic.TunableReference( description= "\n The statistic which will be created for each of this object's\n relationships.\n " ), 'relationship_track_visual': OptionalTunable( description= '\n If enabled, the relationship track to send the client and where\n it should be displayed. If this is None then this relationship will \n not be sent down to the client.\n ', tunable=TunableTuple( relationship_track=RelationshipTrack.TunableReference( description= '\n The relationship that this track will visually try and imitate in\n regards to static track tack data.\n ' ), visible_in_relationship_panel=Tunable( description= "\n By default the relationship is visible in the relationship \n panel and the object's tooltip. If this is set to false, \n hide the relationship from the relationship panel. \n ", tunable_type=bool, default=True))), 'relationship_based_state_change_tuning': OptionalTunable( TunableTuple( description= '\n A list of value ranges and associated states. If the active Sim\n has a relationship with this object that falls within one of the\n value ranges specified here, the object will change state to match\n the specified state.\n \n These state changes exist on a per Sim basis, so this tuning will\n effectively make the same object appear different depending on\n which Sim is currently active.\n ', state_changes=TunableList( tunable=TunableTuple( value_threshold=TunableThreshold( description= "\n The range that the active Sim's relationship with this\n object must fall within in order for this state change to\n take place.\n " ), state=TunableStateValueReference( description= "\n The state this object will change to if it's relationship\n with the active Sim falls within the specified range.\n " ))), default_state=TunableStateValueReference( description= '\n The state this object will change to if there is no other tuned\n relationship based state change for the currently active Sim.\n ' ))) } def __init__(self, owner, **kwargs): super().__init__(owner, **kwargs) self._state_changes = None self._default_state = None self._object_social_mixin = None self._relationships = {} self._relationship_changed_callbacks = defaultdict(CallableList) self._definition_changed_in_buildbuy = False @staticmethod def setup_relationship(sim, target_object): if target_object.objectrelationship_component is None: logger.error( "Failed to add object relationship because {} doesn't have objectrelationship_component tuned", target_object) return if target_object.objectrelationship_component.has_relationship(sim.id): logger.error('Relationship already exists between {} and {}.', sim, target_object) return if not target_object.objectrelationship_component.add_relationship( sim.id): logger.error( 'Failed to add new object relationship between {} and {}.', sim, target_object) @property def relationships(self): return self._relationships def get_number_of_allowed_relationships(self): return self.number_of_allowed_relationships def _on_active_sim_change(self, _, new_sim): if new_sim is None: return relationship = self._get_relationship(new_sim.id) self._update_state(relationship) def _update_state(self, relationship): if self._default_state is None: return if relationship is None: new_state = self._default_state elif self._state_changes is None: new_state = self._default_state else: for state_change in self._state_changes: if state_change.value_threshold.compare( relationship.get_value()): new_state = state_change.state break else: new_state = self._default_state self.owner.set_state(new_state.state, new_state) @property def _can_add_new_relationship(self): if self.number_of_allowed_relationships is not None and len( self._relationships) >= self.number_of_allowed_relationships: return False return True def on_add(self): services.current_zone().register_callback( zone_types.ZoneState.HOUSEHOLDS_AND_SIM_INFOS_LOADED, self._publish_relationship_data) if self.relationship_based_state_change_tuning is None: return self._state_changes = self.relationship_based_state_change_tuning.state_changes self._default_state = self.relationship_based_state_change_tuning.default_state services.current_zone().register_callback( zone_types.ZoneState.CLIENT_CONNECTED, self._register_active_sim_change) def on_remove(self): client = services.client_manager().get_first_client() if client is not None: client.unregister_active_sim_changed(self._on_active_sim_change) self.owner.remove_name_changed_callback(self._on_name_changed) self.destroy_all_relationship() def apply_definition(self, definition, obj_state=0): if not services.current_zone().is_in_build_buy: return self._definition_changed_in_buildbuy |= self.owner.definition != definition def on_buildbuy_exit(self): if not self._definition_changed_in_buildbuy: return self._publish_relationship_data() self._definition_changed_in_buildbuy = False def _register_active_sim_change(self): client = services.client_manager().get_first_client() if client is not None: client.register_active_sim_changed(self._on_active_sim_change) def _publish_relationship_data(self): if self.relationship_track_visual is None: return for sim_id in self._relationships.keys(): self._send_relationship_data(sim_id) def _update_object_relationship_name(self): ownable_component = self.owner.get_component(types.OWNABLE_COMPONENT) if ownable_component is not None: sim_owner_id = ownable_component.get_sim_owner_id() obj_def_id = self.owner.definition.id relationship_service = services.relationship_service() obj_tag_set = relationship_service.get_mapped_tag_set_of_id( obj_def_id) if obj_tag_set is not None: obj_relationship = relationship_service.get_object_relationship( sim_owner_id, obj_tag_set) if obj_relationship is not None and self.owner.has_custom_name( ): obj_relationship.set_object_rel_name( self.owner.custom_name) def _on_name_changed(self, *_, **__): self._publish_relationship_data() self._update_object_relationship_name() def add_relationship_changed_callback_for_sim_id(self, sim_id, callback): self._relationship_changed_callbacks[sim_id].append(callback) def remove_relationship_changed_callback_for_sim_id( self, sim_id, callback): if sim_id in self._relationship_changed_callbacks and callback in self._relationship_changed_callbacks[ sim_id]: self._relationship_changed_callbacks[sim_id].remove(callback) def _trigger_relationship_changed_callbacks_for_sim_id(self, sim_id): callbacks = self._relationship_changed_callbacks[sim_id] if callbacks is not None: callbacks() def add_relationship(self, sim_id): if sim_id in self._relationships: return False if not self._can_add_new_relationship: return False self.owner.on_hovertip_requested() stat = self.relationship_stat(None) self._relationships[sim_id] = stat stat.on_add() self._send_relationship_data(sim_id) self._trigger_relationship_changed_callbacks_for_sim_id(sim_id) self.owner.add_name_changed_callback(self._on_name_changed) return True def remove_relationship(self, sim_id): if sim_id not in self._relationships: return del self._relationships[sim_id] self._trigger_relationship_changed_callbacks_for_sim_id(sim_id) self._send_relationship_destroy(sim_id) def destroy_all_relationship(self): sim_ids = list(self._relationships.keys()) for sim_id in sim_ids: self.remove_relationship(sim_id) def modify_relationship(self, sim_id, value, add=True, from_load=False): if sim_id not in self._relationships: if not add: return if not self.add_relationship(sim_id): return if from_load: self._relationships[sim_id].set_value(value) else: self._relationships[sim_id].add_value(value) self._send_relationship_data(sim_id) self._trigger_relationship_changed_callbacks_for_sim_id(sim_id) client = services.client_manager().get_first_client() if client is not None and client.active_sim is not None and client.active_sim.sim_id == sim_id: self._update_state(self._relationships[sim_id]) def on_social_start(self, sim): if self.has_relationship(sim.id) or self.add_relationship(sim.id): self._object_social_mixin = ObjectRelationshipSocialMixin( sim, self.owner.id, self._get_relationship(sim.id)) self._object_social_mixin.send_social_start_message() def on_social_end(self): if self._object_social_mixin is not None: self._object_social_mixin.send_social_end_message() self._object_social_mixin = None def _get_relationship(self, sim_id): return self._relationships.get(sim_id) def has_relationship(self, sim_id): return sim_id in self._relationships def get_relationship_value(self, sim_id): relationship = self._get_relationship(sim_id) if relationship is not None: return relationship.get_value() return self.relationship_stat.initial_value def get_relationship_initial_value(self): return self.relationship_stat.initial_value def get_relationship_max_value(self): return self.relationship_stat.max_value def get_relationship_min_value(self): return self.relationship_stat.min_value def _send_relationship_data(self, sim_id): if self.relationship_track_visual is None: return relationship_to_send = self._get_relationship(sim_id) if not relationship_to_send: return sim_info = services.sim_info_manager().get(sim_id) if sim_info is None: return msg = commodity_protocol.RelationshipUpdate() msg.actor_sim_id = sim_id (msg.target_id.object_id, msg.target_id.manager_id) = self.owner.icon_info msg.target_instance_id = self.owner.id if self.icon_override is not None: build_icon_info_msg(IconInfoData(icon_resource=self.icon_override), None, msg.target_icon_override) with ProtocolBufferRollback(msg.tracks) as relationship_track_update: relationship_value = relationship_to_send.get_value() relationship_track_update.track_score = relationship_value relationship_track_update.track_bit_id = self.relationship_track_visual.relationship_track.get_bit_at_relationship_value( relationship_value).guid64 relationship_track_update.track_id = self.relationship_track_visual.relationship_track.guid64 relationship_track_update.track_popup_priority = self.relationship_track_visual.relationship_track.display_popup_priority relationship_track_update.visible_in_relationship_panel = self.relationship_track_visual.visible_in_relationship_panel send_relationship_op(sim_info, msg) if self._object_social_mixin is not None: self._object_social_mixin.send_social_update_message() def _send_relationship_destroy(self, sim_id): if self.relationship_track_visual is None or self.relationship_track_visual.relationship_track is None: return sim_info = services.sim_info_manager().get(sim_id) if sim_info is None: return msg = commodity_protocol.RelationshipDelete() msg.actor_sim_id = sim_id msg.target_id = self.owner.id op = GenericProtocolBufferOp( DistributorOps_pb2.Operation.SIM_RELATIONSHIP_DELETE, msg) distributor = Distributor.instance() distributor.add_op(services.sim_info_manager().get(sim_id), op) def save(self, persistence_master_message): if not self._relationships: return persistable_data = protocols.PersistenceMaster.PersistableData() persistable_data.type = protocols.PersistenceMaster.PersistableData.ObjectRelationshipComponent relationship_component_data = persistable_data.Extensions[ protocols.PersistableObjectRelationshipComponent.persistable_data] for (key, value) in self._relationships.items(): if not value.persisted_tuning: continue with ProtocolBufferRollback(relationship_component_data. relationships) as relationship_data: relationship_data.sim_id = key relationship_data.value = value.get_value() persistence_master_message.data.extend([persistable_data]) def load(self, persistable_data): relationship_component_data = persistable_data.Extensions[ protocols.PersistableObjectRelationshipComponent.persistable_data] for relationship in relationship_component_data.relationships: self.modify_relationship(relationship.sim_id, relationship.value, from_load=True)
class MannequinComponent(Component, HasTunableFactory, AutoFactoryInit, component_name=MANNEQUIN_COMPONENT, persistence_key=SimObjectAttributes_pb2.PersistenceMaster.PersistableData.MannequinComponent): MANNEQUIN_GROUP_SHARING_WARNING_NOTIFICATION = TunableUiDialogNotificationSnippet(description='\n A notification to show explaining how outfit merging will clobber\n outfits on the mannequin being placed into the world.\n ', pack_safe=True) class _MannequinGroup(DynamicEnum): INVALID = 0 class _MannequinTemplateExplicit(HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = {'age': TunableEnumEntry(description='\n The default age of this object when placed from Build/Buy.\n ', tunable_type=Age, default=Age.ADULT), 'gender': TunableEnumEntry(description='\n The default gender of this object when placed from\n Build/Buy.\n ', tunable_type=Gender, default=Gender.MALE), 'skin_tone': TunableSkinTone(description='\n The default skin tone of this object when placed from Build/Buy.\n ')} def create_sim_info_data(self, sim_id): return SimInfoBaseWrapper(sim_id=sim_id, age=self.age, gender=self.gender, species=self.species, skin_tone=self.skin_tone) class _MannequinTemplateResource(HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = {'resource_key': TunableResourceKey(description='\n The SimInfo file to use.\n ', default=None, resource_types=(sims4.resources.Types.SIMINFO,)), 'outfit': OptionalTunable(description='\n If enabled, the mannequin will default to the specified outfit.\n ', tunable=TunableTuple(description='\n The outfit to switch the mannequin into.\n ', outfit_category=TunableEnumEntry(description='\n The outfit category.\n ', tunable_type=OutfitCategory, default=OutfitCategory.EVERYDAY), outfit_index=TunableRange(description='\n The outfit index.\n ', tunable_type=int, minimum=0, maximum=4, default=0)))} def create_sim_info_data(self, sim_id): sim_info_data = SimInfoBaseWrapper(sim_id=sim_id) sim_info_data.load_from_resource(self.resource_key) if self.outfit is not None: outfit_to_set = (self.outfit.outfit_category, self.outfit.outfit_index) if sim_info_data.has_outfit(outfit_to_set): sim_info_data.set_current_outfit(outfit_to_set) sim_info_data.set_previous_outfit(outfit_to_set, force=True) return sim_info_data FACTORY_TUNABLES = {'template': TunableVariant(description='\n Define how the initial SimInfo data for this mannequin is created.\n ', explicit=_MannequinTemplateExplicit.TunableFactory(), resource=_MannequinTemplateResource.TunableFactory(), default='resource'), 'cap_modifier': TunableRange(description='\n This mannequin will be worth this many Sims when computing the cap\n limit for NPCs in the world. While mannequins are not simulating\n entities, they might have rendering costs that are equivalent to\n those of Sims. We therefore need to limit how many of them are in\n the world.\n \n Please consult Client Systems or CAS before changing this to a lower\n number, as it might negatively impact performance, especially on\n Minspec.\n ', tunable_type=float, default=0.5, minimum=0, needs_tuning=False), 'outfit_sharing': OptionalTunable(description='\n If enabled, all mannequins sharing the same group, age, and gender\n will share outfits. Objects placed from the Gallery or the household\n inventory will add any unique outfits to the master list, but will\n lose any outfit beyond the maximum per category.\n ', tunable=TunableEnumEntry(description='\n The enum that controls how mannequins share outfits.\n ', tunable_type=_MannequinGroup, default=_MannequinGroup.INVALID)), 'outfit_states': TunableMapping(description='\n A mapping of outfit category to states. When the mannequin is\n wearing the specified outfit category, it will transition into the\n specified state.\n ', key_type=TunableEnumEntry(description='\n The outfit category that will trigger the associated state\n change.\n ', tunable_type=OutfitCategory, default=OutfitCategory.EVERYDAY), value_type=TunableStateValueReference(description='\n The state of the object when the mannequin is wearing the\n associated outfit category.\n ')), 'outfit_modifiers': TunableMapping(description='\n A mapping of modifiers to apply to the mannequin while in the\n specified state.\n ', key_type=TunableStateValueReference(description='\n The state that triggers these outfit modifiers.\n '), value_type=AppearanceModifier.TunableFactory(description='\n An appearance modifier to apply while this state is active.\n ')), 'state_trigger_grubby': TunableStateValueReference(description='\n The state that triggers the mannequin being grubby. Any other state\n on this track will set the mannequin as not grubby.\n ', allow_none=True), 'state_trigger_singed': TunableStateValueReference(description='\n The state that triggers the mannequin being singed. Any other state\n on this track will set the mannequin as not singed.\n ', allow_none=True)} def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._sim_info_data = None self._pose = None self._is_grubby = False self._is_singed = False @property def mannequin_id(self): return self.owner.id @distributor.fields.ComponentField(op=distributor.ops.ChangeSimOutfit) def mannequin_outfit(self): return self._sim_info_data.get_current_outfit() _resend_mannequin_outfit = mannequin_outfit.get_resend() @distributor.fields.ComponentField(op=distributor.ops.SetMannequinPose) def mannequin_pose(self): return self._pose @mannequin_pose.setter def mannequin_pose(self, value): self._pose = value @distributor.fields.ComponentField(op=distributor.ops.PreloadSimOutfit) def mannequin_outfit_preload_list(self): return self._sim_info_data.preload_outfit_list _resend_mannequin_outfit_preload_list = mannequin_outfit_preload_list.get_resend() @distributor.fields.ComponentField(op=distributor.ops.SetMannequinData, priority=distributor.fields.Field.Priority.HIGH) def sim_info_data(self): appearance_override_sim_info = self._sim_info_data.appearance_tracker.appearance_override_sim_info if appearance_override_sim_info is not None: return appearance_override_sim_info return self._sim_info_data _resend_sim_info_data = sim_info_data.get_resend() @distributor.fields.ComponentField(op=distributor.ops.SetGrubby) def mannequin_is_grubby(self): return self._is_grubby @mannequin_is_grubby.setter def mannequin_is_grubby(self, value): self._is_grubby = value @distributor.fields.ComponentField(op=distributor.ops.SetSinged) def mannequin_is_singed(self): return self._is_singed @mannequin_is_singed.setter def mannequin_is_singed(self, value): self._is_singed = value @componentmethod def get_current_outfit(self): return self._sim_info_data.get_current_outfit() @componentmethod def get_previous_outfit(self): return self._sim_info_data.get_previous_outfit() @componentmethod def get_outfits(self): return self._sim_info_data.get_outfits() @componentmethod def set_current_outfit(self, outfit): return self._sim_info_data.set_current_outfit(outfit) def _on_outfit_change(self, *_, **__): self._resend_mannequin_outfit() self._update_outfit_state() def _on_outfit_generated(self, outfit_category, outfit_index): self._sim_info_data.set_outfit_flags(outfit_category, outfit_index, BodyTypeFlag.CLOTHING_ALL) mannequin_group_data = get_mannequin_group_data(self.outfit_sharing, self._sim_info_data) mannequin_group_data.set_mannequin_data(self._sim_info_data) mannequin_group_data.reconcile_mannequin_data() def _on_preload_outfits_changed(self): self._resend_mannequin_outfit_preload_list() def _update_outfit_state(self): if self.owner.id: outfit_category = self.get_current_outfit()[0] outfit_state = self.outfit_states.get(outfit_category) if outfit_state is not None: self.owner.set_state(outfit_state.state, outfit_state) def on_add(self, *_, **__): sim_spawner_service = services.sim_spawner_service() sim_spawner_service.add_npc_cap_modifier(self.cap_modifier) zone = services.current_zone() if not zone.is_zone_loading and not self.owner.is_downloaded: self.reconcile_mannequin_data(is_add=True) self._update_outfit_state() def on_remove(self, *_, **__): sim_spawner_service = services.sim_spawner_service() sim_spawner_service.add_npc_cap_modifier(-self.cap_modifier) if self._sim_info_data is not None: self._sim_info_data.on_outfit_changed.remove(self._on_outfit_change) self._sim_info_data.on_outfit_generated.remove(self._on_outfit_generated) self._sim_info_data.on_preload_outfits_changed.remove(self._on_preload_outfits_changed) def on_state_changed(self, state, old_value, new_value, from_init): old_appearance_modifier = self.outfit_modifiers.get(old_value) if old_appearance_modifier is not None: self._sim_info_data.appearance_tracker.remove_appearance_modifiers(state, source=self) new_appearance_modifier = self.outfit_modifiers.get(new_value) if new_appearance_modifier is not None: self._sim_info_data.appearance_tracker.add_appearance_modifiers(new_appearance_modifier.appearance_modifiers, state, new_appearance_modifier.priority, new_appearance_modifier.apply_to_all_outfits, source=self) if self.state_trigger_singed is not None: if state is self.state_trigger_singed.state: if new_value is self.state_trigger_singed: self.mannequin_is_singed = True else: self.mannequin_is_singed = False if self.state_trigger_grubby is not None: if state is self.state_trigger_grubby.state: if new_value is self.state_trigger_grubby: self.mannequin_is_grubby = True else: self.mannequin_is_grubby = False self._resend_sim_info_data() self._resend_mannequin_outfit() def pre_add(self, manager, obj_id): self._sim_info_data = self.template.create_sim_info_data(obj_id) self._sim_info_data.on_outfit_changed.append(self._on_outfit_change) self._sim_info_data.on_outfit_generated.append(self._on_outfit_generated) self._sim_info_data.on_preload_outfits_changed.append(self._on_preload_outfits_changed) if self.outfit_sharing is not None: mannequin_group_data = get_mannequin_group_data(self.outfit_sharing, self._sim_info_data) mannequin_group_data.add_mannequin(self) def populate_sim_info_data_proto(self, sim_info_data_msg): sim_info_data_msg.mannequin_id = self.mannequin_id self._sim_info_data.save_sim_info(sim_info_data_msg) if self._pose is not None: sim_info_data_msg.animation_pose.asm = get_protobuff_for_key(self._pose.asm) sim_info_data_msg.animation_pose.state_name = self._pose.state_name def save(self, persistence_master_message): persistable_data = SimObjectAttributes_pb2.PersistenceMaster.PersistableData() persistable_data.type = SimObjectAttributes_pb2.PersistenceMaster.PersistableData.MannequinComponent mannequin_component_data = persistable_data.Extensions[SimObjectAttributes_pb2.PersistableMannequinComponent.persistable_data] if self._sim_info_data is not None: self.populate_sim_info_data_proto(mannequin_component_data.sim_info_data) persistence_master_message.data.extend((persistable_data,)) def load(self, persistable_data): sim_info_data_proto = None persistence_service = services.get_persistence_service() if persistence_service is not None: sim_info_data_proto = persistence_service.get_mannequin_proto_buff(self.mannequin_id) if sim_info_data_proto is not None and self.outfit_sharing is not None: set_mannequin_group_data_reference(self.outfit_sharing, self._sim_info_data) if sim_info_data_proto is None: mannequin_component_data = persistable_data.Extensions[SimObjectAttributes_pb2.PersistableMannequinComponent.persistable_data] if mannequin_component_data.HasField('sim_info_data'): sim_info_data_proto = mannequin_component_data.sim_info_data if sim_info_data_proto is not None: self._sim_info_data.load_sim_info(sim_info_data_proto) persistence_service.del_mannequin_proto_buff(self.mannequin_id) zone = services.current_zone() if not zone.is_zone_loading: self.reconcile_mannequin_data(is_add=True, is_loaded=True) def on_finalize_load(self): self.reconcile_mannequin_data() def _replace_outfits(self, sim_info_data): current_outfit = self.get_current_outfit() default_outfit = (OutfitCategory.BATHING, 0) for (outfit_category, outfit_list) in sim_info_data.get_all_outfits(): if outfit_category not in REGULAR_OUTFIT_CATEGORIES: continue self._sim_info_data.remove_outfits_in_category(outfit_category) for (outfit_index, outfit_data) in enumerate(outfit_list): source_outfit = (outfit_category, outfit_index) destination_outfit = self._sim_info_data.add_outfit(outfit_category, outfit_data) self._sim_info_data.generate_merged_outfit(sim_info_data, destination_outfit, default_outfit, source_outfit, preserve_outfit_flags=True) if self._sim_info_data.has_outfit(current_outfit): self._sim_info_data.set_current_outfit(current_outfit) else: self._sim_info_data.set_current_outfit(default_outfit) def _resend_mannequin_data(self): self._resend_sim_info_data() self._resend_mannequin_outfit() def reconcile_mannequin_data(self, *args, **kwargs): self.reconcile_mannequin_data_internal(*args, **kwargs) enable_mannequin_group_sharing_warning_notification() self._resend_mannequin_data() def reconcile_mannequin_data_internal(self, is_add=False, is_loaded=False): if self.outfit_sharing is None: return mannequin_group_sharing_mode = get_mannequin_group_sharing_mode() mannequin_group_data = get_mannequin_group_data(self.outfit_sharing, self._sim_info_data) if is_loaded: if mannequin_group_sharing_mode == MannequinGroupSharingMode.ACCEPT_THEIRS: mannequin_group_data.set_mannequin_data(self._sim_info_data) for mannequin in mannequin_group_data: if mannequin is not self: mannequin._replace_outfits(self._sim_info_data) mannequin._resend_mannequin_data() return if mannequin_group_sharing_mode == MannequinGroupSharingMode.ACCEPT_YOURS: self._replace_outfits(mannequin_group_data.get_mannequin_data()) return mannequin_data = mannequin_group_data.get_mannequin_data() if mannequin_data is None: mannequin_group_data.set_mannequin_data(self._sim_info_data) if is_add: mannequin_group_data.reconcile_mannequin_data() return else: if is_add: if mannequin_group_sharing_mode == MannequinGroupSharingMode.ACCEPT_MERGED: for (outfit_category, outfit_list) in self._sim_info_data.get_all_outfits(): if outfit_category not in REGULAR_OUTFIT_CATEGORIES: continue for (outfit_index, outfit_data) in enumerate(outfit_list): if mannequin_data.is_generated_outfit_duplicate_in_category(self._sim_info_data, (outfit_category, outfit_index)): continue outfits_in_category = mannequin_data.get_outfits_in_category(outfit_category) if outfits_in_category is not None and len(outfits_in_category) >= get_maximum_outfits_for_category(outfit_category): show_mannequin_group_sharing_warning_notification() else: mannequin_data.generate_merged_outfit(self._sim_info_data, mannequin_data.add_outfit(outfit_category, outfit_data), mannequin_data.get_current_outfit(), (outfit_category, outfit_index), preserve_outfit_flags=True) mannequin_group_data.reconcile_mannequin_data() return if self.owner.id: for outfit_category in REGULAR_OUTFIT_CATEGORIES: self._sim_info_data.generate_merged_outfits_for_category(mannequin_data, outfit_category, preserve_outfit_flags=True)
def __init__(self, description='A condition to determine if an object is in a particular state.', **kwargs): super().__init__(subject=TunableEnumEntry(ParticipantType, ParticipantType.Object, description='The subject to check the condition on.'), state=TunableVariant(description='The state to check for.', on_trigger=TunableStateValueReference(description='Satisfy the condition when this state is triggered.'), on_boundary=TunableTuple(description='Satisfy the condition when a boundary of this stat-based state is reached', state=TunableStateValueReference(class_restrictions=CommodityBasedObjectStateValue, description='The state required to satisfy the condition'), boundary=TunableVariant(description='The boundary required to be reached for the condition to be satisfied.', locked_args={'upper': True, 'lower': False}, default='upper')), default='on_trigger'), **kwargs)
class DebugSetupLotInteraction(TerrainImmediateSuperInteraction): INSTANCE_TUNABLES = { 'setup_lot_destroy_old_objects': Tunable(bool, False, description= 'Destroy objects previously created by this interaction.'), 'setup_lot_objects': TunableList( TunableTuple( definition=TunableReference(definition_manager()), position=TunableVector2(Vector2.ZERO()), angle=TunableRange(int, 0, -360, 360), children=TunableList( TunableTuple( definition=TunableReference( definition_manager(), description= 'The child object to create. It will appear in the first available slot in which it fits, subject to additional restrictions specified in the other values of this tuning.' ), part_index=OptionalTunable( Tunable( int, 0, description= 'If specified, restrict slot selection to the given part index.' )), bone_name=OptionalTunable( Tunable( str, '_ctnm_chr_', description= 'If specified, restrict slot selection to one with this exact bone name.' )), slot_type=OptionalTunable( TunableReference( manager=services.get_instance_manager( Types.SLOT_TYPE), description= 'If specified, restrict slot selection to ones that support this type of slot.' )), init_state_values=TunableList( description= '\n List of states the children object will be set to.\n ', tunable=TunableStateValueReference()))), init_state_values=TunableList( description= '\n List of states the created object will be pushed to.\n ', tunable=TunableStateValueReference()))) } _zone_to_cls_to_created_objects = WeakKeyDictionary() @classproperty def destroy_old_objects(cls): return cls.setup_lot_destroy_old_objects @classproperty def created_objects(cls): created_objects = cls._zone_to_cls_to_created_objects.setdefault( services.current_zone(), {}) return setdefault_callable(created_objects, cls, WeakSet) def _run_interaction_gen(self, timeline): with supress_posture_graph_build(): if self.destroy_old_objects: while self.created_objects: obj = self.created_objects.pop() obj.destroy( source=self, cause='Destroying old objects in setup debug lot.') position = self.context.pick.location self.spawn_objects(position) return True yield def _create_object(self, definition_id, position=Vector3.ZERO(), orientation=Quaternion.IDENTITY(), level=0, owner_id=0): obj = objects.system.create_object(definition_id) if obj is not None: transform = Transform(position, orientation) location = Location(transform, self.context.pick.routing_surface) obj.location = location obj.set_household_owner_id(owner_id) self.created_objects.add(obj) return obj def spawn_objects(self, position): root = sims4.math.Vector3(position.x, position.y, position.z) zone = services.current_zone() lot = zone.lot owner_id = lot.owner_household_id if not self.contained_in_lot(lot, root): closest_point = self.find_nearest_point_on_lot(lot, root) if closest_point is None: return False radius = (self.top_right_pos - self.bottom_left_pos).magnitude_2d() / 2 root = closest_point + sims4.math.vector_normalize( sims4.math.vector_flatten(lot.center) - closest_point) * (radius + 1) if not self.contained_in_lot(lot, root): sims4.log.warn( 'Placement', "Placed the lot objects but the entire bounding box isn't inside the lot. This is ok. If you need them to be inside the lot run the interaction again at a diffrent location." ) def _generate_vector(offset_x, offset_z): ground_obj = services.terrain_service.terrain_object() ret_vector = sims4.math.Vector3(root.x + offset_x, root.y, root.z + offset_z) ret_vector.y = ground_obj.get_height_at(ret_vector.x, ret_vector.z) return ret_vector def _generate_quat(rot): return sims4.math.Quaternion.from_axis_angle( rot, sims4.math.Vector3(0, 1, 0)) for info in self.setup_lot_objects: new_pos = _generate_vector(info.position.x, info.position.y) new_rot = _generate_quat(sims4.math.PI / 180 * info.angle) new_obj = self._create_object(info.definition, new_pos, new_rot, owner_id=owner_id) if new_obj is None: sims4.log.error('SetupLot', 'Unable to create object: {}', info) else: for state_value in info.init_state_values: new_obj.set_state(state_value.state, state_value) for child_info in info.children: slot_owner = new_obj if child_info.part_index is not None: for obj_part in new_obj.parts: if obj_part.subroot_index == child_info.part_index: slot_owner = obj_part break bone_name_hash = None if child_info.bone_name is not None: bone_name_hash = hash32(child_info.bone_name) slot_type = None if child_info.slot_type is not None: slot_type = child_info.slot_type for runtime_slot in slot_owner.get_runtime_slots_gen( slot_types={slot_type}, bone_name_hash=bone_name_hash): if runtime_slot.is_valid_for_placement( definition=child_info.definition): break else: sims4.log.error( 'SetupLot', 'Unable to find slot for child object: {}', child_info) child = self._create_object(child_info.definition, owner_id=owner_id) if child is None: sims4.log.error('SetupLot', 'Unable to create child object: {}', child_info) else: runtime_slot.add_child(child) for state_value in child_info.init_state_values: child.set_state(state_value.state, state_value) def contained_in_lot(self, lot, root): self.find_corner_points(root) return True def find_corner_points(self, root): max_x = 0 min_x = 0 max_z = 0 min_z = 0 for info in self.setup_lot_objects: if info.position.x > max_x: max_x = info.position.x if info.position.x < min_x: min_x = info.position.x if info.position.y > max_z: max_z = info.position.y if info.position.y < min_z: min_z = info.position.y self.top_right_pos = sims4.math.Vector3(root.x + max_x, root.y, root.z + max_z) self.bottom_right_pos = sims4.math.Vector3(root.x + max_x, root.y, root.z + min_z) self.top_left_pos = sims4.math.Vector3(root.x + min_x, root.y, root.z + max_z) self.bottom_left_pos = sims4.math.Vector3(root.x + min_x, root.y, root.z + min_z) def find_nearest_point_on_lot(self, lot, root): lot_corners = lot.corners segments = [(lot_corners[0], lot_corners[1]), (lot_corners[1], lot_corners[2]), (lot_corners[2], lot_corners[3]), (lot_corners[3], lot_corners[1])] dist = 0 closest_point = None for segment in segments: new_point = sims4.math.get_closest_point_2D(segment, root) new_distance = (new_point - root).magnitude() if dist == 0: dist = new_distance closest_point = new_point elif new_distance < dist: dist = new_distance closest_point = new_point return closest_point
class SpawnerTuning(HasTunableReference, HasTunableSingletonFactory, AutoFactoryInit, metaclass=TunedInstanceMetaclass, manager=services.get_instance_manager( sims4.resources.Types.RECIPE)): __qualname__ = 'SpawnerTuning' GROUND_SPAWNER = 1 SLOT_SPAWNER = 2 INTERACTION_SPAWNER = 3 INSTANCE_TUNABLES = { 'object_reference': TunableList( description= '\n List of objects the spawner can create. When the random check \n picks this value from the weight calculation it will give all\n the items tuned on this list.\n ', tunable=TunableReference( description= '\n Object reference to the object the spawner will give.\n ', manager=services.definition_manager())), 'spawn_weight': TunableRange( description= '\n Weight that object will have on the probability calculation \n of which object to spawn.\n ', tunable_type=int, default=1, minimum=0), 'spawner_option': TunableVariant( description= '\n Type of spawners to create:\n Ground type - Spawned object will appear on the floor at a tunable \n radius from the spawner object.\n Slot type - Spawned object will appear on an available slot of \n a tunable slot type in the spawner object.\n Interaction type - Spawned objects will appear on the inventory\n when player makes a gather-harvest-scavenge interaction on them. \n ', ground_spawning=TunableTuple( radius=TunableRange( description= '\n Max radius at which the spawned object should appear\n ', tunable_type=int, default=1, minimum=0), force_states=TunableList( description= '\n List of states the created object will be pushed to.\n ', tunable=TunableStateValueReference()), force_initialization_spawn=OptionalTunable( description= '\n If checked, objects with this component will force a \n spawning of objects on initialization. This is mainly used\n for objects on the open street where we want to fake that \n some time has already passed. \n Additionally, if checked, objects will force the states\n on this list instead of the force_states list on the \n general spawner tuning, this way we can add some custom\n states only for the initialization spawn.\n ', tunable=TunableList( description= '\n List of states the created object will have when\n initialized.\n ', tunable=TunableStateValueReference())), location_test=TunableTuple( is_outside=OptionalTunable( description= '\n If checked, will verify if the spawned object is \n located outside. \n If unchecked will test the object is not outside\n ', disabled_name="Don't_Test", tunable=Tunable(bool, True)), is_natural_ground=OptionalTunable( description= '\n If checked, will verify the spawned object is on \n natural ground.\n If unchecked will test the object is not on natural \n ground\n ', disabled_name="Don't_Test", tunable=Tunable(bool, True))), locked_args={'spawn_type': GROUND_SPAWNER}), slot_spawning=TunableTuple( slot_type=TunableReference( description= '\n Slot type where spawned objects should appear\n ', manager=services.get_instance_manager( sims4.resources.Types. SLOT_TYPE)), state_mapping=TunableMapping( description= '\n Mapping of states from the spawner object into the possible\n states that the spawned object may have\n ', key_type=TunableStateValueReference(), value_type=TunableList( description= '\n List of possible children for a parent state\n ', tunable=TunableTuple( description= '\n Pair of weight and possible state that the spawned \n object may have\n ', weight=TunableRange( description= '\n Weight that object will have on the probability calculation \n of which object to spawn.\n ', tunable_type=int, default=1, minimum=0), child_state=TunableStateValueReference()))), locked_args={'spawn_type': SLOT_SPAWNER}), interaction_spawning=TunableTuple( locked_args={'spawn_type': INTERACTION_SPAWNER})), 'spawn_times': OptionalTunable( description= '\n Schedule of when the spawners should trigger.\n If this time is tuned spawners will trigger according to this \n schedule instead of the spawner commodities. \n This should be used for spawners that are on the open neighborhood \n so that those spawners are time based instead of commodity based.\n ', tunable=TunableWeeklyScheduleFactory(), disabled_name='No_custom_spawn_times', enabled_name='Set_custom_spawn_times') } FACTORY_TUNABLES = INSTANCE_TUNABLES @flexmethod def create_spawned_object(cls, inst, spawner_object, definition, loc_type=ItemLocation.ON_LOT): obj = create_object(definition, loc_type=loc_type) if obj is not None: spawner_object.spawner_component._spawned_objects.add(obj) return obj
class SeasonAwareComponent(Component, HasTunableFactory, AutoFactoryInit, component_name=SEASON_AWARE_COMPONENT): FACTORY_TUNABLES = {'seasonal_state_mapping': TunableMapping(description='\n A tunable mapping linking a season to the state the component\n owner should have.\n ', key_type=TunableEnumEntry(description='\n The season we are interested in.\n ', tunable_type=SeasonType, default=None), value_type=TunableList(description='\n A tunable list of states to apply to the owning object of\n this component when it transitions to this season.\n ', tunable=TunableStateValueReference(pack_safe=True)))} @classproperty def required_packs(cls): return (Pack.EP05,) def on_add(self): self.on_season_set(services.season_service().season) def on_finalize_load(self): self.on_season_set(services.season_service().season) def on_season_set(self, season_type): if season_type in self.seasonal_state_mapping: for state_value in self.seasonal_state_mapping[season_type]: self.owner.set_state(state_value.state, state_value)
class WatchSuperInteraction(SuperInteraction): INSTANCE_TUNABLES = { 'off_channel': TunableStateValueReference( description= '\n The off channel. The last Sim using the TV will set the object to\n this state.\n ' ), 'required_channel': TunableStateValueReference( description='\n The channel to watch.\n '), 'remote_animation': TunableAnimationReference( description= '\n The animation for using the TV remote.\n ' ), 'sim_view_discourage_area_width': Tunable( description= '\n The width of the discouragement region placed from a viewing Sim to\n the TV.\n ', tunable_type=float, default=0.4) } CHANGE_CHANNEL_XEVT_ID = 101 def _add_route_goal_suppression_region_to_quadtree(self, *args, **kwargs): if self.target is None: return object_point = self.target.location.transform.translation sim_point = self.sim.intended_location.transform.translation delta = object_point - sim_point delta_length = delta.magnitude() sim_point_offset = self.sim_view_discourage_area_width * 2 if delta_length < sim_point_offset: return start_point = sim_point + delta / (delta_length / sim_point_offset) geo = build_rectangle_from_two_points_and_radius( object_point, start_point, self.sim_view_discourage_area_width) services.sim_quadtree().insert(self.sim, self.id, placement.ItemType.ROUTE_GOAL_PENALIZER, geo, self.sim.routing_surface, False, 0) def _remove_route_goal_suppression_region_from_quadtree(self): services.sim_quadtree().remove(self.id, placement.ItemType.ROUTE_GOAL_PENALIZER, 0) def _refresh_watching_discouragement_stand_region(self, *args, **kwargs): self._remove_route_goal_suppression_region_from_quadtree() self._add_route_goal_suppression_region_to_quadtree() def _start_route_goal_suppression(self, _): self.sim.routing_component.on_intended_location_changed.append( self._refresh_watching_discouragement_stand_region) self._add_route_goal_suppression_region_to_quadtree() def _stop_route_goal_suppression(self, _): self._remove_route_goal_suppression_region_from_quadtree() self.sim.routing_component.on_intended_location_changed.remove( self._refresh_watching_discouragement_stand_region) def ensure_state(self, desired_channel): return conditional_animation(self, desired_channel, self.CHANGE_CHANNEL_XEVT_ID, self.affordance.remote_animation) def _changed_state_callback(self, target, state, old_value, new_value): if new_value is not self.off_channel and new_value.affordance is not None: context = self.context.clone_for_continuation(self) affordance = self.generate_continuation_affordance( new_value.affordance) aop = AffordanceObjectPair(affordance, self.target, affordance, None) aop.test_and_execute(context) self.cancel( FinishingType.OBJECT_CHANGED, cancel_reason_msg= 'state: interaction canceled on state change ({} != {})'.format( new_value.value, self.required_channel.value)) def _run_interaction_gen(self, timeline): result = yield from element_utils.run_child( timeline, build_critical_section_with_finally( self._start_route_goal_suppression, build_critical_section( build_critical_section( self.ensure_state(self.affordance.required_channel), objects.components.state.with_on_state_changed( self.target, self.affordance.required_channel.state, self._changed_state_callback, super()._run_interaction_gen)), maybe( lambda: len(self.target.get_users(sims_only=True)) == 1, self.ensure_state(self.off_channel))), self._stop_route_goal_suppression)) return result yield
class SpawnerTuning(HasTunableReference, HasTunableSingletonFactory, AutoFactoryInit, metaclass=TunedInstanceMetaclass, manager=services.get_instance_manager( sims4.resources.Types.RECIPE)): GROUND_SPAWNER = 1 SLOT_SPAWNER = 2 INTERACTION_SPAWNER = 3 SPAWNER_LOCATION = 0 PORTAL_LOCATION = 1 INSTANCE_TUNABLES = { 'object_reference': TunableList( description= '\n List of objects the spawner can create. When the random check \n picks this value from the weight calculation it will give all\n the items tuned on this list.\n ', tunable=TunableVariant( description= '\n Specify the means by which will the spawner will create the object.\n ', object_definition=ObjectCreator.TunableFactory( get_definition=(True, )), recipe=RecipeCreator.TunableFactory(), default='object_definition')), 'spawn_weight': TunableRange( description= '\n Weight that object will have on the probability calculation \n of which object to spawn.\n ', tunable_type=int, default=1, minimum=0), 'spawn_chance': TunablePercent( description= '\n The chance that the spawned object will actually be created.\n This is in case we want spawned objects to not be created in a \n predictable behavior and the change of "nothing happening" be \n available.\n ', default=100), 'spawner_option': TunableVariant( description= '\n Type of spawners to create:\n Ground type - Spawned object will appear on the floor at a tunable \n radius from the spawner object.\n Slot type - Spawned object will appear on an available slot of \n a tunable slot type in the spawner object.\n Interaction type - Spawned objects will appear on the inventory\n when player makes a gather-harvest-scavenge interaction on them. \n ', ground_spawning=TunableTuple( radius=TunableRange( description= '\n Max radius at which the spawned object should appear\n ', tunable_type=int, default=1, minimum=0), min_radius=TunableRange( description= '\n Minimum distance away from the portal location to\n start looking for a good location.\n ', tunable_type=int, default=0, minimum=0), restrictions=TunableList( description= '\n List of orientation restrictions used by FGL \n when searching for a place to put the object.\n \n Will only apply to off-lot spawners.\n ', tunable=TunableOrientationRestriction()), placement_scoring=TunableList( description= '\n List of scoring functions used by FGL to determine\n best places to put the object.\n\n Will only apply to off-lot spawners.\n ', tunable=TunablePlacementScoringFunction()), force_states=TunableList( description= '\n List of states the created object will be pushed to.\n ', tunable=TunableStateValueReference(pack_safe=True)), force_initialization_spawn=OptionalTunable( description= '\n If checked, objects with this component will force a \n spawning of objects on initialization. This is mainly used\n for objects on the open street where we want to fake that \n some time has already passed. \n Additionally, if checked, objects will force the states\n on this list instead of the force_states list on the \n general spawner tuning, this way we can add some custom\n states only for the initialization spawn.\n ', tunable=TunableList( description= '\n List of states the created object will have when\n initialized.\n ', tunable=TunableStateValueReference())), location_test=TunableTuple( is_outside=OptionalTunable( description= '\n If checked, will verify if the spawned object is \n located outside. \n If unchecked will test the object is not outside\n ', disabled_name="Don't_Test", tunable=Tunable(bool, True)), is_natural_ground=OptionalTunable( description= '\n If checked, will verify the spawned object is on \n natural ground.\n If unchecked will test the object is not on natural \n ground\n ', disabled_name="Don't_Test", tunable=Tunable(bool, True))), starting_location=TunableVariant( description= '\n The location at which we want to start attempting to place\n the object we are creating.\n ', spawner_location=TunableTuple( description= '\n If selected the object will be spawned near the\n location of the spawner object.\n ', consider_source_object_footprint=Tunable( description= "\n If True, then the source object's footprints will\n be considered in the creation of FGL context.\n \n Example: If the source is invisible, then setting\n this to False would allow the spawned object to be\n located at its spawner's location. If the source\n is a visible object, then setting this to True would\n force the spawned object to be offset by any existing\n footprints on the source.\n ", tunable_type=bool, default=False), locked_args={'location_type': SPAWNER_LOCATION}), portal_location=TunableTuple( description= '\n If selected the object will be spanwed near the\n location of the specified portal type and start or end\n location\n ', portal_type=TunableReference( description= '\n A reference to the type of portal to use for the\n starting location.\n ', manager=services.get_instance_manager( sims4.resources.Types.SNIPPET), class_restrictions=('PortalData', )), portal_direction=TunableVariant( description= '\n Choose between the There and Back of the portal.\n This will not work properly if the portal is\n missing a Back and Back is specified here.\n ', locked_args={ 'there': PortalComponent.PORTAL_DIRECTION_THERE, 'back': PortalComponent.PORTAL_DIRECTION_BACK }, default='there'), portal_location=TunableVariant( description= '\n Choose between the entry and exit location of the\n portal.\n ', locked_args={ 'entry': PortalComponent.PORTAL_LOCATION_ENTRY, 'exit': PortalComponent.PORTAL_LOCATION_EXIT }, default='entry'), locked_args={'location_type': PORTAL_LOCATION}), default='spawner_location'), locked_args={'spawn_type': GROUND_SPAWNER}), slot_spawning=TunableTuple( slot_type=TunableReference( description= '\n Slot type where spawned objects should appear\n ', manager=services.get_instance_manager( sims4.resources.Types. SLOT_TYPE)), force_initialization_spawn=OptionalTunable( description= '\n If enabled, objects with this component will force a \n spawning of objects on initialization. This is mainly used\n for objects on the open street where we want to fake that \n some time has already passed.\n ', tunable=TunableRange( description= '\n The number of objects to be created.\n ', tunable_type=int, minimum=1, default=1)), state_mapping=TunableMapping( description= '\n Mapping of states from the spawner object into the possible\n states that the spawned object may have\n ', key_type=TunableStateValueReference(), value_type=TunableList( description= '\n List of possible children for a parent state\n ', tunable=TunableTuple( description= '\n Pair of weight and possible state that the spawned \n object may have\n ', weight=TunableRange( description= '\n Weight that object will have on the probability calculation \n of which object to spawn.\n ', tunable_type=int, default=1, minimum=0), child_state=TunableStateValueReference()))), locked_args={'spawn_type': SLOT_SPAWNER}), interaction_spawning=TunableTuple( locked_args={'spawn_type': INTERACTION_SPAWNER})), 'spawn_times': OptionalTunable( description= '\n Schedule of when the spawners should trigger.\n If this time is tuned spawners will trigger according to this \n schedule instead of the spawner commodities. \n This should be used for spawners that are on the open neighborhood \n so that those spawners are time based instead of commodity based.\n ', tunable=WeeklySchedule.TunableFactory(), disabled_name='No_custom_spawn_times', enabled_name='Set_custom_spawn_times') } FACTORY_TUNABLES = INSTANCE_TUNABLES @flexmethod def create_spawned_object(cls, inst, spawner_object, definition, loc_type=ItemLocation.ON_LOT): try: obj = definition(loc_type=loc_type) except KeyError: logger.exception('Failed to spawn object {} for {}', definition, spawner_object) obj = None if obj is not None: spawner_object.spawner_component.spawned_object_created(obj) return obj
class TimeOfDayComponent(Component, HasTunableFactory, component_name=types.TIME_OF_DAY_COMPONENT): DAILY_REPEAT = date_and_time.create_time_span(hours=24) FACTORY_TUNABLES = { 'state_changes': TunableMapping( description= '\n A mapping from state to times of the day when the state should be \n set to a tuned value.\n ', key_type=TunableStateTypeReference( description= '\n The state to be set.\n '), value_type=TunableList( description= '\n List of times to modify the state at.\n ', tunable=TunableTuple( start_time=TunableRange( description= '\n The start time (24 hour clock time) for the Day_Time state.\n ', tunable_type=float, default=0, minimum=0, maximum=24), value=TunableStateValueReference( description= '\n New state value.\n ' ), loot_list=TunableList( description= '\n A list of loot operations to apply when changing state.\n ', tunable=TunableReference( manager=services.get_instance_manager( sims4.resources.Types.ACTION), class_restrictions=('LootActions', ), pack_safe=True)), chance=SuccessChance.TunableFactory( description= '\n Percent chance that the state change will be considered. \n The chance is evaluated just before running the tests.\n ' ), tests=TunableTestSet( description= '\n Test to decide whether the state change can be applied.\n ' )))) } def __init__(self, owner, *, state_changes): super().__init__(owner) self.state_changes = state_changes self.alarm_handles = [] def _apply_state_change(self, state, change): resolver = SingleObjectResolver(self.owner) chance = change.chance.get_chance(resolver) if random.random() > chance: return if not change.tests.run_tests(resolver): return self.owner.set_state(state, change.value) for loot_action in change.loot_list: loot_action.apply_to_resolver(resolver) def _add_alarm(self, cur_state, state, change): now = services.time_service().sim_now time_to_day = clock.time_until_hour_of_day(now, change.start_time) def alarm_callback(_): self._apply_state_change(state, change) self.alarm_handles.append( alarms.add_alarm(self.owner, time_to_day, alarm_callback, repeating=True, repeating_time_span=self.DAILY_REPEAT)) if cur_state is None or time_to_day > cur_state[0]: return (time_to_day, change) return cur_state def on_add(self): for (state, changes) in self.state_changes.items(): current_state = None for change in changes: current_state = self._add_alarm(current_state, state, change) if current_state is not None: self._apply_state_change(state, current_state[1]) def on_remove(self): for handle in self.alarm_handles: alarms.cancel_alarm(handle)
class CurfewComponent(Component, HasTunableFactory, AutoFactoryInit, component_name=CURFEW_COMPONENT): FACTORY_TUNABLES = { 'curfew_state_reference': TunableStateTypeReference( description= '\n This is a reference to the State type we will be manipulating when\n we change states on this object.\n ' ), 'times_state_reference': TunableStateTypeReference( description= '\n This is a reference to the State type we will be manipulating when\n we change states on this object.\n ' ), 'curfew_not_set': TunableStateValueReference( description= '\n This is the reference to the state to apply on the owning object\n when there is no active curfew setting. Or the setting is UNSET.\n ' ), 'curfew_warning_state': TunableStateValueReference( description= '\n This is the reference to the state to apply to the owning object\n when the curfew is about to start.\n ' ), 'curfew_past_state': TunableStateValueReference( description= '\n This is the reference to the state to apply to the owning object\n when curfew is active.\n ' ), 'curfew_on_state': TunableStateValueReference( description= '\n This is the reference to the state to apply to the owning object\n when the curfew is set but not currently active.\n ' ), 'not_set_state': TunableStateValueReference( description= "\n This is the reference to the state to apply to the owning object\n when there isn't a curfew set at all.\n " ), 'times_set': TunableMapping( description= '\n This is a Mapping of time (in military time) to state to apply to\n the owning object in order to display the correct time that the\n curfew is set for.\n ', key_type=int, value_type=TunableStateValueReference()), 'set_curfew_affordances': TunableList( description= '\n A List of the interactions that will be used to set the curfew\n via this object.\n ', tunable=TunableReference( description= '\n This is the interaction that will be used to "set" the curfew.\n ', manager=services.affordance_manager(), class_restrictions=('SuperInteraction', ))) } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._current_curfew_setting = None def on_add(self): curfew_service = services.get_curfew_service() current_curfew = curfew_service.get_zone_curfew( services.current_zone_id()) if current_curfew not in self.times_set: pass self.apply_state_for_setting(current_curfew) self.apply_warning_state(current_curfew) self._register_for_alarms(curfew_service) def on_remove(self): curfew_service = services.get_curfew_service() self._unregister_for_alarms(curfew_service) def component_super_affordances_gen(self, **kwargs): yield from self.set_curfew_affordances def update_states(self, curfew_setting): if curfew_setting == self._current_curfew_setting: return self.apply_state_for_setting(curfew_setting) self.apply_warning_state(curfew_setting) def apply_state_for_setting(self, setting): if setting is CurfewService.UNSET: self.owner.set_state(self.times_state_reference, self.not_set_state) state_to_apply = self.times_set.get(setting) if state_to_apply is not None: self.owner.set_state(self.times_state_reference, state_to_apply) def apply_warning_state(self, curfew_setting): if curfew_setting is CurfewService.UNSET: self.owner.set_state(self.curfew_state_reference, self.curfew_not_set) return now = services.time_service().sim_now.hour() if now >= CurfewService.CURFEW_END_TIME and now < curfew_setting: self._on_curfew_over_alarm() elif now == curfew_setting - 1: self._on_warning_time_alarm() else: self._on_curfew_started_alarm() def _register_for_alarms(self, curfew_service): curfew_service.register_for_alarm_callbacks( self._on_warning_time_alarm, self._on_curfew_started_alarm, self._on_curfew_over_alarm, self.update_states) def _unregister_for_alarms(self, curfew_service): curfew_service.unregister_for_alarm_callbacks( self._on_warning_time_alarm, self._on_curfew_started_alarm, self._on_curfew_over_alarm, self.update_states) def _on_warning_time_alarm(self): self.owner.set_state(self.curfew_state_reference, self.curfew_warning_state, force_update=True) def _on_curfew_started_alarm(self): self.owner.set_state(self.curfew_state_reference, self.curfew_past_state, force_update=True) def _on_curfew_over_alarm(self): self.owner.set_state(self.curfew_state_reference, self.curfew_on_state, force_update=True)
class CreateCarriedObjectSuperInteraction(SuperInteraction): __qualname__ = 'CreateCarriedObjectSuperInteraction' INSTANCE_TUNABLES = { 'definition': TunableReference( description='\n The object to create.\n ', tuning_group=GroupNames.CREATE_CARRYABLE, manager=services.definition_manager()), 'carry_track_override': OptionalTunable( description= '\n If enabled, specify which carry track the Sim must use to carry the\n created object.\n ', tuning_group=GroupNames.CREATE_CARRYABLE, tunable=TunableEnumEntry( description= '\n Which hand to carry the object in.\n ', tunable_type=PostureTrack, default=PostureTrack.RIGHT)), 'initial_states': TunableList( description= '\n A list of states to apply to the finished object as soon as it is\n created.\n ', tuning_group=GroupNames.CREATE_CARRYABLE, tunable=TunableStateValueReference()), 'continuation': SuperInteraction.TunableReference( description= '\n An interaction to push as a continuation to the carry.\n ' ), 'continuation_with_affordance_overrides': OptionalTunable( description= "\n If enabled, allows you to specify a continuation to the\n carry based on a participant's object definition.\n This continuation will be pushed in addition to the tunable continuation,\n although you will rarely need to tune both at the same time.\n ", tunable=TunableTuple( continuation=TunableContinuation( description= '\n A tunable continuation to push based on the parameters provided.\n ' ), participant=TunableEnumEntry( description= '\n When using the affordance_override mapping, this\n is the participant we will use to get the definition.\n ', tunable_type=ParticipantType, default=ParticipantType.PickedObject), affordance_override=TunableMapping( description= "\n Based on the participants's object definition, you can override\n the affordance on the tunable continuation.\n ", key_type=TunableReference( description= '\n The object definition to look for.\n ', manager=services.definition_manager()), value_type=SuperInteraction.TunableReference())), tuning_group=GroupNames.CREATE_CARRYABLE) } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._object_create_helper = None @property def create_target(self): return self.definition @property def created_target(self): if self._object_create_helper is not None: return self._object_create_helper.object def _get_create_continuation_affordance(self): def create_continuation_affordance(): context = self.context.clone_for_continuation(self) aop = AffordanceObjectPair(self.continuation, self.created_target, self.continuation, None) return (aop, context) if self.continuation is not None: return create_continuation_affordance def build_basic_content(self, sequence, **kwargs): super_build_basic_content = super().build_basic_content def setup_object(obj): for initial_state in reversed(self.initial_states): obj.set_state(initial_state.state, initial_state) obj.set_household_owner_id(self.sim.household.id) self._object_create_helper = CreateObjectHelper( self.sim, self.definition, self, init=setup_object, tag='CreateCarriedObjectSuperInteraction') def claim_object(*_, **__): self._object_create_helper.claim() def set_carry_target(_): if self.carry_track_override: self.track = self.carry_track_override else: self.track = DEFAULT if self.track is None: return False self.context.carry_target = self.created_target def push_tunable_continuation_with_affordance_overrides(_): if self.continuation_with_affordance_overrides is None: return obj = self.get_participant( self.continuation_with_affordance_overrides.participant) if obj is not None: affordance_override = self.continuation_with_affordance_overrides.affordance_override.get( obj.definition) else: affordance_override = None interaction_parameters = {} if 'picked_item_ids' in self.interaction_parameters: interaction_parameters[ 'picked_item_ids'] = self.interaction_parameters[ 'picked_item_ids'] self.push_tunable_continuation( self.continuation_with_affordance_overrides.continuation, affordance_override=affordance_override, **interaction_parameters) def enter_carry(timeline): result = yield element_utils.run_child( timeline, enter_carry_while_holding( self, self.created_target, callback=claim_object, create_si_fn=self._get_create_continuation_affordance(), track=self.track, sequence=build_critical_section( super_build_basic_content(sequence, **kwargs), flush_all_animations))) return result return (self._object_create_helper.create( set_carry_target, enter_carry, push_tunable_continuation_with_affordance_overrides), lambda _: self._object_create_helper.claimed)
class LoudNeighborSituation(SituationComplexCommon): INSTANCE_TUNABLES = {'loud_neighbor_state': _LoudNeighborState.TunableFactory(description='\n The situation state used for when a neighbor starts being loud.\n This will listen for a Sim to bang on the door and complain about\n the noise before transitioning to the complain state.\n ', tuning_group=SituationComplexCommon.SITUATION_STATE_GROUP, display_name='01_loud_neighbor_situation_state'), 'complain_state': _ComplainState.TunableFactory(description="\n The situation state used for when a player Sim has banged on the\n neighbor's door and we are waiting for them to complain to the\n neighbor.\n ", tuning_group=SituationComplexCommon.SITUATION_STATE_GROUP, display_name='02_complain_situation_state'), 'loud_neighbor_filters': TunableList(description='\n Sim filters that fit the description of loud neighbor(s). We run\n through them until we find someone matching the filter.\n ', tunable=TunableVariant(description='\n The filter we want to use to find loud neighbors.\n ', single_filter=TunableSimFilter.TunablePackSafeReference(description='\n A Sim Filter to find a loud neighbor.\n '), aggregate_filter=TunableSimFilter.TunablePackSafeReference(description='\n An aggregate Sim Filter to find a loud neighbor that has\n other Sims.\n ', class_restrictions=filters.tunable.TunableAggregateFilter), default='single_filter')), 'loud_door_state_on': TunableStateValueReference(description='\n State to set on the apartment door of the loud neighbor when the\n situation starts.\n '), 'loud_door_state_off': TunableStateValueReference(description='\n State to set on the apartment door of the loud neighbor when they\n are no longer being loud.\n ')} def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) reader = self._seed.custom_init_params_reader if reader is not None: self._neighbor_sim_id = reader.read_uint64(NEIGHBOR_TOKEN, None) self._neighbor_door_id = reader.read_uint64(DOOR_TOKEN, None) else: self._neighbor_sim_id = None self._neighbor_door_id = None @classproperty def allow_user_facing_goals(cls): return False @classmethod def _get_tuned_job_and_default_role_state_tuples(cls): return [] @classmethod def _states(cls): return (SituationStateData(1, _LoudNeighborState, factory=cls.loud_neighbor_state), SituationStateData(2, _ComplainState, factory=cls.complain_state)) @classmethod def default_job(cls): pass @classmethod def situation_meets_starting_requirements(cls, **kwargs): neighbor_sim_id = cls._get_loud_neighbor() if neighbor_sim_id is None: return False return True def start_situation(self): super().start_situation() neighbor_sim_id = self._get_loud_neighbor() self._set_loud_neighbor_and_door(neighbor_sim_id) if self._neighbor_sim_id is not None: self._change_state(self.loud_neighbor_state()) def _save_custom_situation(self, writer): super()._save_custom_situation(writer) if self._neighbor_sim_id is not None: writer.write_uint64(NEIGHBOR_TOKEN, self._neighbor_sim_id) if self._neighbor_door_id is not None: writer.write_uint64(DOOR_TOKEN, self._neighbor_door_id) def _destroy(self): if self._neighbor_door_id is not None: apartment_door = services.object_manager().get(self._neighbor_door_id) if apartment_door is not None: apartment_door.set_state(self.loud_door_state_off.state, self.loud_door_state_off) services.get_zone_situation_manager().remove_sim_from_auto_fill_blacklist(self._neighbor_sim_id) super()._destroy() @classmethod def _get_loud_neighbor(cls): neighbor_sim_id = None blacklist_sim_ids = {sim_info.sim_id for sim_info in services.active_household()} blacklist_sim_ids.update(set(sim_info.sim_id for sim_info in services.sim_info_manager().instanced_sims_gen())) loud_neighbor_filters = sorted(cls.loud_neighbor_filters, key=lambda *args: random.random()) for neighbor_filter in loud_neighbor_filters: neighbors = services.sim_filter_service().submit_matching_filter(sim_filter=neighbor_filter, allow_yielding=False, blacklist_sim_ids=blacklist_sim_ids, gsi_source_fn=cls.get_sim_filter_gsi_name) neighbor_sim_infos_at_home = [result.sim_info for result in neighbors if result.sim_info.is_at_home] if len(neighbor_sim_infos_at_home) > 1 or len(neighbor_sim_infos_at_home): if neighbor_filter.is_aggregate_filter(): continue neighbor_sim_id = neighbor_sim_infos_at_home[0].sim_id if neighbor_sim_infos_at_home else None if neighbor_sim_id is not None: break return neighbor_sim_id def _set_loud_neighbor_and_door(self, neighbor_sim_id): neighbor_sim_info = services.sim_info_manager().get(neighbor_sim_id) if neighbor_sim_info is None: self._self_destruct() return self._neighbor_sim_id = neighbor_sim_id door_service = services.get_door_service() plex_door_infos = door_service.get_plex_door_infos() object_manager = services.object_manager() for door_info in plex_door_infos: door = object_manager.get(door_info.door_id) if door is not None: if door.household_owner_id == neighbor_sim_info.household_id: self._neighbor_door_id = door_info.door_id break else: logger.error('Could not find door object that belongs to {}', neighbor_sim_info.household.name) self._self_destruct()
class ObjectRelationshipComponent(Component, HasTunableFactory, component_name=types.OBJECT_RELATIONSHIP_COMPONENT, persistence_key=protocols.PersistenceMaster.PersistableData.ObjectRelationshipComponent): __qualname__ = 'ObjectRelationshipComponent' FACTORY_TUNABLES = {'number_of_allowed_relationships': OptionalTunable(description='\n Number of Sims who can have a relationship with this object at one\n time. If not specified, an infinite number of Sims can have a \n relationship with the object.\n ', tunable=TunableRange(tunable_type=int, default=1, minimum=1)), 'relationship_stat': Statistic.TunableReference(description="\n The statistic which will be created for each of this object's\n relationships.\n "), 'relationship_track_visual': OptionalTunable(RelationshipTrack.TunableReference(description='\n The relationship that this track will visually try and imitate in\n regards to static track tack data. If this is None then this\n relationship will not be sent down to the client.\n ')), 'relationship_based_state_change_tuning': OptionalTunable(TunableTuple(description='\n A list of value ranges and associated states. If the active Sim\n has a relationship with this object that falls within one of the\n value ranges specified here, the object will change state to match\n the specified state.\n \n These state changes exist on a per Sim basis, so this tuning will\n effectively make the same object appear different depending on\n which Sim is currently active.\n ', state_changes=TunableList(tunable=TunableTuple(value_threshold=TunableThreshold(description="\n The range that the active Sim's relationship with this\n object must fall within in order for this state change to\n take place.\n "), state=TunableStateValueReference(description="\n The state this object will change to if it's relationship\n with the active Sim falls within the specified range.\n "))), default_state=TunableStateValueReference(description='\n The state this object will change to if there is no other tuned\n relationship based state change for the currently active Sim.\n ')))} def __init__(self, owner, number_of_allowed_relationships, relationship_stat, relationship_track_visual, relationship_based_state_change_tuning): super().__init__(owner) self._number_of_allowed_relationships = number_of_allowed_relationships self._relationship_stat = relationship_stat self._relationship_track_visual = relationship_track_visual self._relationship_based_state_change_tuning = relationship_based_state_change_tuning self._state_changes = None self._default_state = None self._relationships = {} self._relationship_changed_callbacks = defaultdict(CallableList) def _on_active_sim_change(self, _, new_sim): if new_sim is None: return relationship = self._get_relationship(new_sim.id) self._update_state(relationship) def _update_state(self, relationship): if self._default_state is None: return if relationship is None: new_state = self._default_state elif self._state_changes is None: new_state = self._default_state else: for state_change in self._state_changes: while state_change.value_threshold.compare(relationship.get_value()): new_state = state_change.state break new_state = self._default_state self.owner.set_state(new_state.state, new_state) @property def _can_add_new_relationship(self): if self._number_of_allowed_relationships is not None and len(self._relationships) >= self._number_of_allowed_relationships: return False return True def on_add(self): if self._relationship_based_state_change_tuning is None: return self._state_changes = self._relationship_based_state_change_tuning.state_changes self._default_state = self._relationship_based_state_change_tuning.default_state services.current_zone().register_callback(zone_types.ZoneState.CLIENT_CONNECTED, self._register_active_sim_change) services.current_zone().register_callback(zone_types.ZoneState.HOUSEHOLDS_AND_SIM_INFOS_LOADED, self._publish_relationship_data) def on_remove(self): client = services.client_manager().get_first_client() if client is not None: client.unregister_active_sim_changed(self._on_active_sim_change) def _register_active_sim_change(self): client = services.client_manager().get_first_client() if client is not None: client.register_active_sim_changed(self._on_active_sim_change) def _publish_relationship_data(self): if not self._relationship_track_visual: return for sim_id in self._relationships.keys(): self._send_relationship_data(sim_id) def add_relationship_changed_callback_for_sim_id(self, sim_id, callback): self._relationship_changed_callbacks[sim_id].append(callback) def remove_relationship_changed_callback_for_sim_id(self, sim_id, callback): if sim_id in self._relationship_changed_callbacks and callback in self._relationship_changed_callbacks[sim_id]: self._relationship_changed_callbacks[sim_id].remove(callback) def _trigger_relationship_changed_callbacks_for_sim_id(self, sim_id): callbacks = self._relationship_changed_callbacks[sim_id] if callbacks is not None: callbacks() def add_relationship(self, sim_id): if sim_id in self._relationships: return False if not self._can_add_new_relationship: return False stat = self._relationship_stat(None) self._relationships[sim_id] = stat stat.on_add() self._send_relationship_data(sim_id) self._trigger_relationship_changed_callbacks_for_sim_id(sim_id) return True def remove_relationship(self, sim_id): if sim_id not in self._relationships: return del self._relationships[sim_id] self._trigger_relationship_changed_callbacks_for_sim_id(sim_id) def modify_relationship(self, sim_id, value, add=True): if not add: return if not (sim_id not in self._relationships and self.add_relationship(sim_id)): return self._relationships[sim_id].add_value(value) self._send_relationship_data(sim_id) self._trigger_relationship_changed_callbacks_for_sim_id(sim_id) client = services.client_manager().get_first_client() if client is not None and client.active_sim is not None and client.active_sim.sim_id == sim_id: self._update_state(self._relationships[sim_id]) def _get_relationship(self, sim_id): return self._relationships.get(sim_id) def has_relationship(self, sim_id): return sim_id in self._relationships def get_relationship_value(self, sim_id): relationship = self._get_relationship(sim_id) if relationship is not None: return relationship.get_value() return self._relationship_stat.initial_value def get_relationship_initial_value(self): return self._relationship_stat.initial_value def _send_relationship_data(self, sim_id): if self._relationship_track_visual is None: return relationship_to_send = self._get_relationship(sim_id) if not relationship_to_send: return sim_info = services.sim_info_manager().get(sim_id) if sim_info is None: return msg = commodity_protocol.RelationshipUpdate() msg.actor_sim_id = sim_id (msg.target_id.object_id, msg.target_id.manager_id) = self.owner.icon_info msg.target_instance_id = self.owner.id with ProtocolBufferRollback(msg.tracks) as relationship_track_update: relationship_value = relationship_to_send.get_value() relationship_track_update.track_score = relationship_value relationship_track_update.track_bit_id = self._relationship_track_visual.get_bit_at_relationship_value(relationship_value).guid64 relationship_track_update.track_id = self._relationship_track_visual.guid64 relationship_track_update.track_popup_priority = self._relationship_track_visual.display_popup_priority send_relationship_op(sim_info, msg) def save(self, persistence_master_message): if not self._relationships: return persistable_data = protocols.PersistenceMaster.PersistableData() persistable_data.type = protocols.PersistenceMaster.PersistableData.ObjectRelationshipComponent relationship_component_data = persistable_data.Extensions[protocols.PersistableObjectRelationshipComponent.persistable_data] for (key, value) in self._relationships.items(): with ProtocolBufferRollback(relationship_component_data.relationships) as relationship_data: relationship_data.sim_id = key relationship_data.value = value.get_value() persistence_master_message.data.extend([persistable_data]) def load(self, persistable_data): relationship_component_data = persistable_data.Extensions[protocols.PersistableObjectRelationshipComponent.persistable_data] for relationship in relationship_component_data.relationships: self.modify_relationship(relationship.sim_id, relationship.value)