class RelationshipBitTest(HasTunableSingletonFactory, AutoFactoryInit, BaseTest): FACTORY_TUNABLES = { 'subject': TunableEnumFlags( description= '\n Owner(s) of the relationship(s) to be compared with subject_b.\n ', enum_type=ParticipantType, default=ParticipantType.Actor), 'target': TunableEnumFlags( description= '\n Owner(s) of the relationship(s) to be compared with subject_a.\n ', enum_type=ParticipantType, default=ParticipantType.TargetSim), 'relationship_bits': TunableSet( description= '\n Any of these relationship bits will pass the test.\n ', tunable=TunableReference(services.relationship_bit_manager()), minlength=1), 'test_event': TunableVariant( description='\n Event to listen to.\n ', locked_args={ 'Bit Added': TestEvent.AddRelationshipBit, 'Bit Removed': TestEvent.RemoveRelationshipBit }, default='Bit Added') } @property def test_events(self): return (self.test_event, ) def get_expected_args(self): return { 'subject': self.subject, 'target': self.target, 'relationship_bit': event_testing.test_constants.FROM_EVENT_DATA } @cached_test def __call__(self, subject, target, relationship_bit): if relationship_bit not in self.relationship_bits: return TestResult( False, 'Event {} did not trigger for bit {} between Sims {} and {}, bits of interest: {}', relationship_bit, subject, target, self.relationship_bits) return TestResult.TRUE
class WalkstyleBehaviorOverride(HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = { 'walkstyle_behavior_priority': TunableEnumEntry( description= '\n Define the priority of this override relative to other overrides.\n This is meaningful for the non-additive properties of this override.\n ', tunable_type=WalkstyleBehaviorOverridePriority, default=WalkstyleBehaviorOverridePriority.DEFAULT), 'run_required_total_distance': _get_tunable_override(tunable=TunableRange( description= '\n For an entire route, the minimum distance required for Sim to\n run.\n ', tunable_type=float, minimum=0, default=20)), 'run_required_segment_distance': _get_tunable_override(tunable=TunableRange( description= '\n For a specific route segment, the minimum distance required for\n the Sim to run.\n ', tunable_type=float, minimum=0, default=10)), 'run_walkstyle': _get_tunable_override(tunable=TunableWalkstyle( description= '\n Override the walkstyle to use when this Sim is supposed to be running.\n ' )), 'short_walkstyle': _get_tunable_override(tunable=TunableWalkstyle( description= '\n Override walkstyle used when Sims are routing over a short distance.\n ' )), 'wading_walkstyle': _get_tunable_override(tunable=TunableWalkstyle( description= '\n Override walkstyle used when Sims are walking through the water.\n ' )), 'additional_run_flags': TunableEnumFlags( description= '\n Define where the Sim is allowed to run.\n \n NOTE: This is *additive*, meaning the resulting run flags for a Sim\n are defined by the default tuning plus all overrides. However,\n removed_run_flags is applied last.\n ', enum_type=WalkStyleRunAllowedFlags, allow_no_flags=True), 'removed_run_flags': TunableEnumFlags( description= '\n Modify a Sim\'s ability to run.\n \n NOTE: This is *additive* with respect to all overrides, and is\n applied to the combination of the default tuning plus\n additional_run_flags from all overrides.\n \n e.g.: The default behavior for a human is to exclusively run\n outdoors. A hypothetical "excitable" trait might add the ability to\n run indoors (meaning the total result is indoors+outdoors). However,\n the "pregnant" trait might remove the ability to run (both outdoors\n and indoors), effectively preventing the Sim from running at all.\n ', enum_type=WalkStyleRunAllowedFlags, allow_no_flags=True) }
class _BroadcasterEffectTested(_BroadcasterEffect): FACTORY_TUNABLES = { 'tests': TunableTestSet( description= '\n Tests that must pass in order for the broadcaster effect to be\n applied.\n ' ), 'excluded_participants': OptionalTunable( description= '\n If enabled, these participants will be excluded from this broadcaster effect.\n ', tunable=TunableEnumFlags( description= '\n A set of Participants that will be excluded from having this effect\n applied to them. If the broadcaster comes from an interaction,\n these participants will come from that interaction.\n ', enum_type=ParticipantType, default=ParticipantType.Actor | ParticipantType.TargetSim)) } def _should_apply_broadcaster_effect(self, broadcaster, affected_object): if broadcaster.interaction is not None and self.excluded_participants is not None: participants = broadcaster.interaction.get_participants( self.excluded_participants) if affected_object in participants: return False if affected_object.sim_info is not None and affected_object.sim_info in participants: return False resolver = broadcaster.get_resolver(affected_object) if not self.tests.run_tests(resolver): return False return super()._should_apply_broadcaster_effect( broadcaster, affected_object)
def __init__( self, locked_animation_args=DEFAULT, animation_callback=DEFAULT, interaction_asm_type=DEFAULT, override_animation_context=False, participant_enum_override=DEFAULT, description='There is an arbitrary number of possible animation selectors, based on participation in group.', **kwargs): if participant_enum_override is DEFAULT: participant_enum_type = ParticipantTypeAnimation participant_enum_default = ParticipantTypeAnimation.Invalid else: participant_enum_type = participant_enum_override[0] participant_enum_default = participant_enum_override[1] super().__init__(groups=TunableList( TunableTuple( group=TunableEnumFlags( participant_enum_type, participant_enum_default, description='The group the Sim must be a participant of'), animation_ref=TunableAnimationReference( allow_reactionlets=False, override_animation_context=override_animation_context, callback=animation_callback)), description='A list of difficulty to animation mappings.'), description=description, **kwargs)
class DestroySituationsByTagsMixin: FACTORY_TUNABLES = { 'situation_tags': TunableSet( description= '\n A situation must match at least one of the tuned tags in order to\n be destroyed.\n ', tunable=TunableEnumWithFilter(tunable_type=Tag, filter_prefixes=['situation'], default=Tag.INVALID, pack_safe=True)), 'required_participant': TunableEnumFlags( description= '\n If tuned, only situations with this participant will be destroyed.\n ', enum_type=ParticipantType, default=ParticipantType.Invalid) } def _destroy_situations_by_tags(self, resolver): situation_manager = services.get_zone_situation_manager() situations = situation_manager.get_situations_by_tags( self.situation_tags) if situations: participant = None if self.required_participant is not None and self.required_participant != ParticipantType.Invalid: participant = resolver.get_participant( self.required_participant) if participant is None or not participant.is_sim: return False for situation in situations: if participant and not situation.is_sim_info_in_situation( participant.sim_info): continue situation_manager.destroy_situation_by_id(situation.id) return True
class PaintingTexture(metaclass=TunedInstanceMetaclass, manager=services.get_instance_manager( sims4.resources.Types.RECIPE)): INSTANCE_TUNABLES = { 'texture': TunableResourceKey(None, resource_types=[sims4.resources.Types.TGA], allow_none=True), 'tests': TunableTestSet(), 'canvas_types': TunableEnumFlags( CanvasType, CanvasType.NONE, description= '\n The canvas types (generally, aspect ratios) with which this texture\n may be used.\n ' ) } @classmethod def _tuning_loaded_callback(cls): if cls.texture: cls._base_painting_state = PaintingState.from_key(cls.texture) else: cls._base_painting_state = None @classmethod def apply_to_object(cls, obj): obj.canvas_component.painting_state = cls._base_painting_state
def __init__(self, default_participant_type=None, **kwargs): super().__init__(participant_type=TunableEnumFlags( description= "\n We use this participant's holiday tracker to get the icon.\n ", enum_type=ParticipantTypeSingle, default=default_participant_type or ParticipantType.Actor), **kwargs)
class PackSpecificTuning: VENUE_PACK_TUNING = TunableMapping( description= "\n Venue tuning that is needed by UI when that venue's pack is not installed.\n ", key_name='venue_id', key_type=TunableReference( description= '\n Reference to the venue that this data represents\n ', manager=services.get_instance_manager(sims4.resources.Types.VENUE), pack_safe=True), value_name='data', value_type=TunableTuple( description= "\n Venue data that is shown in the UI when this venue's pack is not installed.\n ", gallery_download_venue=OptionalTunable( description= '\n If tuned, the tuned venue tuning will be substituted if this\n venue is downloaded from the gallery by a player who is not\n entitled to it. The default behavior is to substitute the\n generic venue. This tuned venue will also determine download\n compatibility (for instance, only residential venues can be \n downloaded to owned residential venues).\n ', tunable=TunableReference(manager=services.get_instance_manager( sims4.resources.Types.VENUE))), venue_name=TunableLocalizedStringFactory( description= '\n Name that will be displayed for the venue when the pack containing \n that venue is not installed\n ' ), venue_flags=TunableEnumFlags( description= '\n Venue flags used to mark a venue with specific properties.\n ', enum_type=VenueFlags, allow_no_flags=True, default=VenueFlags.NONE), export_class_name='VenueDataTuple'), tuple_name='VenuePackTuning', export_modes=ExportModes.All, verify_tunable_callback=verify_venue_tuning)
class RelationshipBitCountTest(HasTunableSingletonFactory, AutoFactoryInit, BaseTest): FACTORY_TUNABLES = { 'subject': TunableEnumFlags( description= '\n Owner of the relationship.\n ', enum_type=ParticipantTypeSingleSim, default=ParticipantTypeSingleSim.Actor), 'rel_bit': TunablePackSafeReference( description= "\n The type of relationship we're looking for.\n \n In other words, we're looking for any relationship\n with this Rel Bit.\n ", manager=services.get_instance_manager( sims4.resources.Types.RELATIONSHIP_BIT)), 'relationship_count': TunableRange( description= "\n The number of relationships we want to compare against\n the sim's actual number of relationships.\n ", tunable_type=int, minimum=0, default=0), 'comparison_operator': TunableOperator( description= "\n The operator to use to compare the sim's\n actual relationship count vs. the tuned\n Relationship Count.\n ", default=Operator.EQUAL) } def get_expected_args(self): return {'sim_infos': self.subject} @cached_test def __call__(self, sim_infos): if self.rel_bit is None: return TestResult( False, 'Failed relationship bit count test: Rel Bit is not available due to pack-safeness' ) sim_info_manager = services.sim_info_manager() for sim_info in sim_infos: rel_tracker = sim_info.relationship_tracker actual_rel_count = 0 for other_sim_info_id in sim_info.relationship_tracker.target_sim_gen( ): other_sim_info = sim_info_manager.get(other_sim_info_id) if other_sim_info is None: continue if rel_tracker.has_bit(other_sim_info_id, self.rel_bit): actual_rel_count += 1 threshold = sims4.math.Threshold(self.relationship_count, self.comparison_operator) if not threshold.compare(actual_rel_count): operator_symbol = Operator.from_function( self.comparison_operator).symbol return TestResult( False, 'Failed relationship bit count test: Actual Relationship Count ({}) {} Tuned Relationship Count ({})', actual_rel_count, operator_symbol, self.relationship_count) return TestResult.TRUE
class DaycareLiability(Liability, HasTunableFactory, AutoFactoryInit): FACTORY_TUNABLES = {'participants': TunableEnumFlags(description='\n The participants this liability is applied to.\n ', enum_type=ParticipantTypeSim, default=ParticipantTypeSim.Actor)} def __init__(self, interaction, *args, participants=DEFAULT, **kwargs): if participants is DEFAULT: participants = ParticipantTypeSim.Actor super().__init__(*args, participants=participants, **kwargs) self._sim_infos = [] def on_add(self, interaction): super().on_add(interaction) participants = interaction.get_participants(self.participants) for participant in participants: self._sim_infos.append(participant.sim_info) def on_run(self): super().on_run() daycare_service = services.daycare_service() for sim_info in self._sim_infos: if daycare_service is not None: daycare_service.set_sim_globally_unavailable(sim_info) daycare_service.set_sim_unavailable(sim_info) def release(self): super().release() daycare_service = services.daycare_service() for sim_info in self._sim_infos: if daycare_service is not None: daycare_service.set_sim_globally_available(sim_info) daycare_service.set_sim_available(sim_info)
def __init__(self, **kwargs): super().__init__(participant_type=TunableEnumFlags( description= '\n The Participant who owns the lifestyle brand we want to use.\n ', enum_type=ParticipantTypeSingle, default=ParticipantType.Actor), **kwargs)
class LeaveSituationElement(XevtTriggeredElement): FACTORY_TUNABLES = { 'situation_types': TunableList( description= '\n A list of all situations the Sim needs to leave.\n ', tunable=TunableReference(manager=services.get_instance_manager( sims4.resources.Types.SITUATION), pack_safe=True)), 'subject': TunableEnumFlags( description= '\n The participant of who will join the situation.\n ', enum_type=ParticipantType, default=ParticipantType.Actor) } def _do_behavior(self, *args, **kwargs): subject = self.interaction.get_participant(self.subject) if subject is None or not subject.is_sim: logger.error( 'Fail to leave situation since subject {} is not sim for interaction {}', self.subject, self.interaction, owner='cjiang') return False situation_manager = services.get_zone_situation_manager() for situation in situation_manager.running_situations(): if isinstance(situation, self.situation_types): situation_manager.remove_sim_from_situation( subject, situation.id) return True
def participant_type_override(participant_type_enum, participant_type_default): return { 'target_sim': TunableEnumFlags( description= '\n Target(s) of the relationship(s).\n ', enum_type=participant_type_enum, default=participant_type_default) }
def __init__( self, description='A buff that will get added to the subject when running the interaction if the tests succeeds.', **kwargs): super().__init__( subject=TunableEnumFlags(ParticipantType, ParticipantType.Actor, description='Who will receive the buff.'), tests=event_testing.tests.TunableTestSet(), buff_type=TunableBuffReference( description='The buff type to be added to the Sim.'), description=description, **kwargs)
class OutfitGeneratorRandomizationMixin: INSTANCE_TUNABLES = { 'filter_flag': TunableEnumFlags( description= '\n Define how to handle part randomization for the generated outfit.\n ', enum_type=OutfitFilterFlag, default=OutfitFilterFlag.USE_EXISTING_IF_APPROPRIATE | OutfitFilterFlag.USE_VALID_FOR_LIVE_RANDOM, allow_no_flags=True), 'body_type_chance_overrides': TunableMapping( description= '\n Define body type chance overrides for the generate outfit. For\n example, if BODYTYPE_HAT is mapped to 100%, then the outfit is\n guaranteed to have a hat if any hat matches the specified tags.\n \n If used in an appearance modifier, these body types will contribute\n to the flags that determine which body types can be generated,\n regardless of their percent chance.\n ', key_type=BodyType, value_type=TunablePercent( description= '\n The chance that a part is applied to the corresponding body\n type.\n ', default=100)), 'body_type_match_not_found_policy': TunableMapping( description= '\n The policy we should take for a body type that we fail to find a\n match for. Primary example is to use MATCH_NOT_FOUND_KEEP_EXISTING\n for generating a tshirt and making sure a sim wearing full body has\n a lower body cas part.\n \n If used in an appearance modifier, these body types will contribute\n to the flags that determine which body types can be generated.\n ', key_type=BodyType, value_type=MatchNotFoundPolicy) } FACTORY_TUNABLES = INSTANCE_TUNABLES def get_body_type_flags(self): tuned_flags = 0 for body_type in itertools.chain( self.body_type_chance_overrides.keys(), self.body_type_match_not_found_policy.keys()): tuned_flags |= 1 << body_type return tuned_flags or BodyTypeFlag.CLOTHING_ALL def _generate_outfit(self, sim_info, outfit_category, outfit_index=0, tag_list=(), seed=None): sim_info.generate_outfit( outfit_category, outfit_index=outfit_index, tag_list=tag_list, filter_flag=self.filter_flag, body_type_chance_overrides=self.body_type_chance_overrides, body_type_match_not_found_overrides=self. body_type_match_not_found_policy, seed=seed)
def __init__(self, **kwargs): super().__init__( description= '\n A tunable that increments a specified statistic by a specified\n amount, runs a sequence, and then decrements the statistic by the\n same amount.\n ', stat=Statistic.TunableReference( description= '\n The statistic to increment and decrement.\n ' ), subject=TunableEnumFlags( description= '\n The participant of the interaction on which the statistic will\n be incremented and decremented.\n ', enum_type=ParticipantType, default=ParticipantType.Object), transfer_stat=Statistic.TunableReference( description= '\n The statistic whose value to transfer.\n ' ), transfer_subject=TunableEnumFlags( description= '\n The participant of the interaction whose statistic value will\n be transferred.\n ', enum_type=ParticipantType, default=ParticipantType.Actor), **kwargs)
def __init__(self, **kwargs): super().__init__( subject=TunableEnumFlags( ParticipantType, ParticipantType.Actor, description='Object the audio effect will be placed on.'), tag_name=Tunable( str, 'x', description= 'Name of the animation tag this effect will trigger on.'), effect_name=Tunable( str, None, description= 'Name of the audio modification that will be applied'), **kwargs)
def __init__(self, animation_callback=DEFAULT, allow_multi_si_cancel=False, allow_social_animation=False, locked_args=None, **kwargs): import interactions.base.basic animation_ref = TunableAnimationReference(callback=animation_callback, interaction_asm_type=InteractionAsmType.Outcome, description='The one-shot animation ref to play') animation_ref = OptionalTunable(animation_ref) if allow_multi_si_cancel: cancel_si = OptionalTunable(TunableEnumFlags(description='\n Every participant in this list will have the SI\n associated with the interaction owning this outcome\n canceled.\n ', enum_type=ParticipantType)) else: cancel_si = TunableVariant(description="\n When enabled, the outcome will cancel the owning\n interaction's parent SI.\n ", locked_args={'enabled': ParticipantType.Actor, 'disabled': None}, default='disabled') if allow_social_animation: kwargs['social_animation'] = OptionalTunable(description='\n If enabled, specify an animation that will be played once for\n the entire social group.\n ', tunable=TunableAnimationReference()) else: locked_args = {} if locked_args is None else locked_args locked_args['social_animation'] = None if locked_args is None: locked_args = {TunableOutcomeActions.ADD_TARGET_AFFORDANCE_LOOT_KEY: True} elif TunableOutcomeActions.ADD_TARGET_AFFORDANCE_LOOT_KEY not in locked_args: locked_args.update({TunableOutcomeActions.ADD_TARGET_AFFORDANCE_LOOT_KEY: True}) super().__init__(animation_ref=animation_ref, xevt=OptionalTunable(description='\n When specified, the outcome will be associated to this xevent.\n ', tunable=Tunable(tunable_type=int, default=None)), response=OptionalTunable(TunableResponseSelector()), force_outcome_on_exit=Tunable(description='\n If checked outcome will always be given even if\n interaction was canceled. If unchecked, outcome\n will only be given if mixer, one_shot, or\n naturally finishing interaction.\n ', tunable_type=bool, default=False), loot_list=TunableList(description='\n A list of pre-defined loot operations.\n ', tunable=LootActions.TunableReference()), consume_object=Tunable(description="\n If checked, the loot list generated in the target\n object's consumable component will be added to\n this outcome's loot list.\n ", tunable_type=bool, default=False), continuation=TunableContinuation(description='An affordance to be pushed as part of an outcome.'), cancel_si=cancel_si, events_to_send=TunableList(TunableEnumEntry(TestEvent, TestEvent.Invalid, description='events types to send')), display_message=OptionalTunable(description='\n If set, flyaway text will be shown.\n ', tunable=TunableLocalizedString(default=None, description='Localized string that is shown as flyaway text')), loot_offset_override=OptionalTunable(Tunable(int, 0), description="An override to interactions.utils.loot's DEFAULT_LOOT_OFFSET tuning, specifically for this outcome."), parameterized_autonomy=TunableMapping(description='\n Specify parameterized autonomy for the participants of the interaction.\n ', key_type=TunableEnumEntry(description='\n The participant to run parameterized autonomy for.\n ', tunable_type=ParticipantType, default=ParticipantType.Actor), value_type=TunableList(description='\n A list of parameterized autonomy requests to run.\n ', tunable=TunableParameterizedAutonomy())), outcome_result=TunableEnumEntry(description='\n The interaction outcome result to consider this interaction result. This is\n important for testing interactions, such as an aspiration that wants to know\n if a Sim has successfully kissed another Sim. All interactions that a designer\n would consider a success case for such a scenario would be assured here.\n ', tunable_type=OutcomeResult, default=OutcomeResult.SUCCESS), basic_extras=interactions.base.basic.TunableBasicExtrasCore(), locked_args=locked_args, **kwargs)
class SpawnerInteractionTuning(HasTunableSingletonFactory): __qualname__ = 'SpawnerInteractionTuning' FACTORY_TUNABLES = { 'spawner_participant': TunableEnumFlags( description= '\n Subject containing the spawner component the object reward data will \n be read from.\n ', enum_type=ParticipantType, default=ParticipantType.Object), 'locked_args': { 'spawn_type': ObjectRewardsTuning.SPAWNER_REWARD } } def __init__(self, spawner_participant, spawn_type, **kwargs): super().__init__() self.spawner_participant = spawner_participant self.spawn_type = spawn_type
class JoinSituationElement(XevtTriggeredElement): FACTORY_TUNABLES = { 'situation_type': TunableReference( description='\n The situation to join.\n ', manager=services.situation_manager()), 'situation_job': OptionalTunable( description= '\n The situation job that sim will get to while join the situation.\n ', tunable=TunableReference(manager=services.situation_job_manager()), disabled_name='use_default_job', enabled_name='specify_job', disabled_value=DEFAULT), 'subject': TunableEnumFlags( description= '\n The participant of who will join the situation.\n ', enum_type=ParticipantType, default=ParticipantType.Actor) } def _do_behavior(self, *args, **kwargs): situation_manager = services.get_zone_situation_manager() situation = situation_manager.get_situation_by_type( self.situation_type) if situation is None: logger.error( 'Fail to join situation since cannot find running situation {} for interaction {}', self.situation_type, self.interaction, owner='cjiang') return False subject = self.interaction.get_participant(self.subject) if subject is None or not subject.is_sim: logger.error( 'Fail to join situation since subject {} is not sim for interaction {}', self.subject, self.interaction, owner='cjiang') return False situation.invite_sim_to_job(subject, job=self.situation_job) return True
class CanvasComponent(Component, HasTunableFactory, component_name=objects.components.types.CANVAS_COMPONENT, persistence_key=persistence_protocols.PersistenceMaster.PersistableData.CanvasComponent): __qualname__ = 'CanvasComponent' FACTORY_TUNABLES = {'canvas_types': TunableEnumFlags(CanvasType, CanvasType.NONE, description="\n A painting texture must support at least one of these canvas types\n to be applied to this object's painting area.\n ")} def __init__(self, owner, *, canvas_types): super().__init__(owner) self.canvas_types = canvas_types self._painting_state = None def save(self, persistence_master_message): persistable_data = persistence_protocols.PersistenceMaster.PersistableData() persistable_data.type = persistence_protocols.PersistenceMaster.PersistableData.CanvasComponent canvas_data = persistable_data.Extensions[persistence_protocols.PersistableCanvasComponent.persistable_data] canvas_data.texture_id = self.painting_state.texture_id canvas_data.reveal_level = self.painting_state.reveal_level persistence_master_message.data.extend([persistable_data]) def load(self, persistable_data): canvas_data = persistable_data.Extensions[persistence_protocols.PersistableCanvasComponent.persistable_data] self.painting_state = PaintingState(canvas_data.texture_id, canvas_data.reveal_level) @distributor.fields.ComponentField(op=distributor.ops.SetPaintingState, default=None) def painting_state(self) -> PaintingState: return self._painting_state @painting_state.setter def painting_state(self, value): self._painting_state = value @property def painting_reveal_level(self) -> int: if self.painting_state is not None: return self.painting_state.reveal_level @painting_reveal_level.setter def painting_reveal_level(self, reveal_level): if self.painting_state is not None: self.painting_state = self.painting_state.get_at_level(reveal_level) @componentmethod_with_fallback(lambda msg: None) def populate_icon_canvas_texture_info(self, msg): if self.painting_state is not None and msg is not None: msg.texture_id = self.painting_state.texture_id
def __init__(self, **kwargs): super().__init__( description= '\n A tunable that increments a specified statistic by a specified\n amount, runs a sequence, and then decrements the statistic by the\n same amount.\n ', stat=TunableReference( description= '\n The statistic to increment and decrement.\n ', manager=services.statistic_manager(), class_restrictions=(Statistic, Skill)), subject=TunableEnumFlags( description= '\n The participant of the interaction on which the statistic will\n be incremented and decremented.\n ', enum_type=ParticipantType, default=ParticipantType.Object), amount=Tunable( description= '\n The amount that will be incremented and decremented from the\n specified statistic.\n ', tunable_type=float, default=1), **kwargs)
def __init__(self, **kwargs): super().__init__( subject=TunableEnumFlags( description= '\n The participant of the interaction on which the statistic will\n be incremented and decremented.\n ', enum_type=ParticipantType, default=ParticipantType.Actor), rate=TunableRange( description= "\n Units per second to remove of the target's commodities.", tunable_type=float, default=1, minimum=0, maximum=None), categories=TunableList( description= '\n Will reduce all commodities that match any category in this list.', tunable=TunableEnumEntry(StatisticCategory, StatisticCategory.INVALID)), **kwargs)
def __init__( self, description="Configure focus on one or more of an interaction's participants.", **kwargs): super().__init__( subject=TunableEnumFlags(ParticipantType, ParticipantType.Object, description='Who or what to focus on.'), layer=Tunable( int, None, description= 'Layer override: Ambient=0, SuperInteraction=3, Interaction=5.' ), score=Tunable( int, 1, description= 'Focus score. This orders focus elements in the same layer.'), description=description, **kwargs)
class _FilterByPortalFlags(HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = { 'portal_flags': TunableEnumFlags( description= '\n The object must have any of these portal flags in order to\n satisfy the requirement.\n ', enum_type=PortalFlags) } def is_object_valid(self, obj): if getattr(obj, 'parts', None): return any( self.is_object_valid(part.part_definition) for part in obj.parts) surface_portal_constraint = getattr(obj, 'surface_portal_constraint', None) if surface_portal_constraint is None: return False if surface_portal_constraint.required_portal_flags is None: return False return surface_portal_constraint.required_portal_flags & self.portal_flags
class PathPlanContextWrapper(HasTunableFactory, AutoFactoryInit, PathPlanContext): class _AgentShapeCircle(HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = { 'radius': TunableRange( description= "\n The circle's radius, The circle is built around the agent's\n center point.\n ", tunable_type=float, minimum=0, default=0.123) } def get_quadtree_polygon(self, position, orientation): return QtCircle(Vector2(position.x, position.z), self.radius) class _AgentShapeRectangle(HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = { 'length': TunableRange( description= "\n The rectangle's length. This is parallel to the agent's forward\n vector.\n ", tunable_type=float, minimum=0, default=1.5), 'width': TunableRange( description= "\n The rectangle's width. This is perpendicular to the agent's\n forward vector.\n ", tunable_type=float, minimum=0, default=0.5) } def get_quadtree_polygon(self, position, orientation): length_vector = orientation.transform_vector( Vector3.Z_AXIS()) * self.length / 2 return build_rectangle_from_two_points_and_radius( position + length_vector, position - length_vector, self.width) FACTORY_TUNABLES = { '_agent_radius': TunableRange( description= '\n The size of the agent (as a circle radius), when computing a path.\n This determines how much space is required for the agent to route\n through space.\n ', tunable_type=float, minimum=0, default=0.123), '_agent_shape': TunableVariant( description= "\n The shape used to represent the agent's position and intended\n position in the quadtree.\n ", circle=_AgentShapeCircle.TunableFactory(), rectangle=_AgentShapeRectangle.TunableFactory(), default='circle'), '_agent_goal_radius': TunableRange( description= '\n The clearance required (as a circle radius) at the end of a route.\n ', tunable_type=float, minimum=0, default=0.123), '_agent_extra_clearance_modifier': TunableRange( description= "\n The clearance that the agent will try to maintain from other objects\n (and go around them), defined as a multiple of the agent's radius.\n ", tunable_type=float, minimum=0, default=2.5), '_allowed_portal_flags': OptionalTunable( TunableEnumFlags( description= '\n Required portal flags can be set on portals. If these flags are\n also set on the actor, then that actor is allowed to traverse\n that portal type.\n \n e.g. Counter Surface portals have the "Counter" flag set on the\n portals. If the "Counter" flag is set on a routing context, then\n your able to go through counter portals.\n ', enum_type=PortalFlags)), '_discouraged_portal_flags': OptionalTunable( TunableEnumFlags( description= "\n Flags to set on the Sim to match the discouragement flags on\n a portal. If the flags don't match, the Sim will be \n discouraged from routing through this portal. \n \n e.g. Regular doors have a discouragement flag that matches\n only humans, this way pets will try to use pet doors over \n regular doors.\n ", enum_type=PortalFlags)), '_allowed_heights': OptionalTunable( TunableEnumFlags( description= '\n All of the flags that define what this agent is able to route\n under. Each Flag has a specific height assigned to it.\n \n FOOTPRINT_KEY_REQUIRE_SMALL_HEIGHT = 0.5m\n FOOTPRINT_KEY_REQUIRE_TINY_HEIGHT = 0.25m\n FOOTPRINT_KEY_REQUIRE_FLOATING = flying agent\n ', enum_type=FootprintKeyMaskBits, default=FootprintKeyMaskBits.SMALL_HEIGHT)), 'surface_preference_scoring': TunableMapping( description= '\n Surface type and additional score to apply to the cost of goals on \n this surface.\n This allows for geometric constraints with multi surface goals, to \n have preference to specific surface types. i.e Cats always prefer\n to target surface object goals, so we make the ground more \n expensive.\n This will only be applied for multi surface geometric constraints.\n ', key_type=TunableEnumEntry( description= '\n The surface type for scoring to be applied to.\n ', tunable_type=routing.SurfaceType, default=routing.SurfaceType.SURFACETYPE_WORLD, invalid_enums=(routing.SurfaceType.SURFACETYPE_UNKNOWN, )), value_type=TunableRange( description= '\n Additive cost to apply when calculating goals for multi surface \n constraints.\n ', tunable_type=float, default=10, minimum=0)) } def __init__(self, agent, **kwargs): super().__init__(**kwargs) self._agent = agent self.agent_id = agent.id self.agent_radius = self._agent_radius self.agent_extra_clearance_multiplier = self._agent_extra_clearance_modifier self.agent_goal_radius = self._agent_goal_radius self.footprint_key = agent.definition.get_footprint(0) full_keymask = routing.FOOTPRINT_KEY_ON_LOT | routing.FOOTPRINT_KEY_OFF_LOT if self._allowed_heights is not None: full_keymask |= self._allowed_heights self.set_key_mask(full_keymask) full_portal_keymask = PortalFlags.DEFAULT species = getattr(self._agent, 'extended_species', DEFAULT) full_portal_keymask |= SpeciesExtended.get_portal_flag(species) age = getattr(self._agent, 'age', DEFAULT) full_portal_keymask |= Age.get_portal_flag(age) if self._allowed_portal_flags is not None: full_portal_keymask |= self._allowed_portal_flags self.set_portal_key_mask(full_portal_keymask) portal_discouragement_flags = 0 if self._discouraged_portal_flags is not None: portal_discouragement_flags |= self._discouraged_portal_flags self.set_portal_discourage_key_mask(portal_discouragement_flags) def get_quadtree_polygon(self, position=DEFAULT, orientation=DEFAULT): position = self._agent.position if position is DEFAULT else position orientation = self._agent.orientation if orientation is DEFAULT else orientation return self._agent_shape.get_quadtree_polygon(position, orientation) def add_location_to_quadtree(self, placement_type, position=DEFAULT, orientation=DEFAULT, routing_surface=DEFAULT, index=0): position = self._agent.position if position is DEFAULT else position orientation = self._agent.orientation if orientation is DEFAULT else orientation routing_surface = self._agent.routing_surface if routing_surface is DEFAULT else routing_surface if placement_type in (placement.ItemType.SIM_POSITION, placement.ItemType.SIM_INTENDED_POSITION): quadtree_geometry = self.get_quadtree_polygon( position=position, orientation=orientation) else: quadtree_geometry = QtCircle(Vector2(position.x, position.z), self.agent_goal_radius) services.sim_quadtree().insert(self._agent, self._agent.id, placement_type, quadtree_geometry, routing_surface, False, index) def remove_location_from_quadtree(self, placement_type, index=0): services.sim_quadtree().remove(self._agent.id, placement_type, index)
def __init__(self, **kwargs): super().__init__(participant_type=TunableEnumFlags( ParticipantType, ParticipantType.Actor), description="The Sim who's thumbnail will be used.", **kwargs)
class CarryableComponent( Component, HasTunableFactory, AutoFactoryInit, component_name=objects.components.types.CARRYABLE_COMPONENT): class _CarryableAllowedHands(HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = { 'biped_allowed_hands': TunableVariant(locked_args={ 'both': (Hand.LEFT, Hand.RIGHT), 'left_only': (Hand.LEFT, ), 'right_only': (Hand.RIGHT, ) }, default='both'), 'quadruped_allowed_hands': TunableVariant(locked_args={ 'both': (Hand.LEFT, Hand.RIGHT), 'mouth_only': (Hand.RIGHT, ), 'back_only': (Hand.LEFT, ) }, default='mouth_only') } def get_allowed_hands(self, sim): if sim is None: return self.biped_allowed_hands return sim.get_allowed_hands_type(self) class _CarryableTransitionConstraint(HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = { 'constraint_mobile': TunableList( description= '\n The constraint to use when the Sim is in a mobile posture.\n ', tunable=TunableGeometricConstraintVariant( disabled_constraints={ 'spawn_points', 'relative_circle', 'current_position' })), 'constraint_non_mobile': TunableList( description= '\n The constraint to use when the Sim is not in a mobile posture.\n ', tunable=TunableGeometricConstraintVariant( disabled_constraints={ 'spawn_points', 'relative_circle', 'current_position' })) } DEFAULT_GEOMETRIC_TRANSITION_CONSTRAINT = _CarryableTransitionConstraint.TunableFactory( description= '\n Unless specifically overridden, the constraint to use when transitioning\n into and out of a carry for any carryable object.\n ' ) DEFAULT_GEOMETRIC_TRANSITION_LARGE = TunableRange( description= '\n This is a large transition distance. This is used by:\n * TYAE humans picking up any pet\n ', tunable_type=float, default=0.7, minimum=0) DEFAULT_GEOMETRIC_TRANSITION_MEDIUM = TunableRange( description= '\n This is a medium transition distance. This is used by:\n * TYAE humans picking up P humans\n ', tunable_type=float, default=0.6, minimum=0) DEFAULT_GEOMETRIC_TRANSITION_SMALL = TunableRange( description= '\n This is a small transition distance. This is used by:\n * C humans picking up AE cats and AE small dogs\n ', tunable_type=float, default=0.503, minimum=0) DEFAULT_GEOMETRIC_TRANSITION_TINY = TunableRange( description= '\n This is a tiny transition distance. This is used by:\n * C humans picking up C cats and C dogs and small dogs\n ', tunable_type=float, default=0.419, minimum=0) DEFAULT_CARRY_AFFORDANCES = TunableList( description= '\n The list of default carry affordances.\n ', tunable=TunableReference(manager=services.get_instance_manager( sims4.resources.Types.INTERACTION))) PUT_IN_INVENTORY_AFFORDANCE = TunableReference( description= '\n The affordance used by carryable component to put objects in inventory.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION)) PUT_DOWN_HERE_AFFORDANCE = TunableReference( description= '\n The affordance used by carryable component to put down here via the\n PutDownLiability liability.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION)) PUT_DOWN_ANYWHERE_AFFORDANCE = TunableReference( description= '\n The affordance used by carryable component to put down objects anywhere\n via the PutDownLiability liability.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION)) FACTORY_TUNABLES = { 'put_down_tuning': TunableVariant( description= '\n Define how Sims prefer to put this object down.\n ', reference=TunablePutDownStrategySpeciesMapping( description= '\n Define specific costs for all the possible locations this object\n could be put down. Also, define, if necessary, custom\n interactions to put the object down.\n \n If this is not specified, and the default is used, the most\n appropriate put down strategy is used. This allows the object to\n be put down in most places (floor, inventory, slots). Also,\n species might specify their own custom behavior.\n ' ), locked_args={'use_default': None}, default='use_default'), 'state_based_put_down_tuning': TunableMapping( description= '\n A mapping from a state value to a putdownstrategy. If the owning\n object is in any of the states tuned here, it will use that state\'s\n associated putdownstrategy in place of the one putdownstrategy tuned\n in the "put_down_tuning" field. If the object is in multiple states\n listed in this mapping, the behavior is undefined.\n ', key_type=TunableReference( description= '\n The state value this object must be in in order to use the\n associated putdownstrategy.\n ', manager=services.get_instance_manager( sims4.resources.Types.OBJECT_STATE)), value_type= TunableVariant(reference=TunablePutDownStrategySpeciesMapping( description= '\n Tuning for how to score where a Sim might want to set an\n object down.\n ' )), key_name='State', value_name='PutDownStrategy'), 'carry_affordances': OptionalTunable(tunable=TunableList(tunable=TunableReference( description= '\n The versions of the HoldObject affordance that this object\n supports.\n ', manager=services.affordance_manager())), disabled_name='use_default_affordances', enabled_name='use_custom_affordances'), 'provided_affordances': TunableProvidedAffordances( description= '\n Affordances that are generated when a Sim holding this object\n selects another object to interact with. The generated interactions\n target the selected object, and have their carry target set to the\n component\'s owner.\n \n By default, this is applied to Sims. e.g.: The "Drink" interaction\n provides "Make Toast" on other Sims.\n \n Optionally, you can specify a tag to have the interaction appear on\n other objects. e.g.: The "Hold Puppy" interaction might unlock "Put\n Down Here" on sofas.\n \n Target defaults to the object selected (Invalid). Carry Target is\n locked to this object being carried. If is_linked is\n checked, the affordance will be linked to the interaction\n carrying this object.\n ', class_restrictions=('SuperInteraction', ), locked_args={ 'allow_self': False, 'target': ParticipantType.Object, 'carry_target': ParticipantType.CarriedObject }), 'constraint_pick_up': OptionalTunable( description= '\n A list of constraints that must be fulfilled in order to\n interact with this object.\n ', tunable=TunableList(tunable=TunableConstraintVariant( description= '\n A constraint that must be fulfilled in order to\n interact with this object.\n ' ))), 'allowed_hands_data': _CarryableAllowedHands.TunableFactory(), 'holster_while_routing': Tunable( description= '\n If True, the Sim will holster the object before routing and\n unholster when the route is complete.\n ', tunable_type=bool, default=False), 'holster_compatibility': TunableAffordanceFilterSnippet( description= '\n Define interactions for which holstering this object is\n explicitly disallowed.\n \n e.g. The Scythe is tuned to be holster-incompatible with\n sitting, meaning that Sims will holster the Sctyhe when sitting.\n ' ), 'unholster_when_routing': TunableUnholsterWhileRoutingBehaviorVariant(), 'prefer_owning_sim_inventory_when_not_on_home_lot': Tunable( description= "\n If checked, this object will highly prefer to be put into the\n owning Sim's inventory when being put down by the owning Sim on\n a lot other than their home lot.\n \n Certain objects, like consumables, should be exempt from this.\n ", tunable_type=bool, default=True), 'is_valid_posture_graph_object': Tunable( description= '\n If this is checked, this object is allowed to provide postures and\n expand in the posture graph, despite its ability to be carried.\n \n Normally, for the performance reasons, carryable objects are not\n posture providing objects. The preferred way of interacting with a\n carryable object is marking it as a carry requirement in the ASM.\n \n However, there are objects for which this is not possible. For\n example, Nesting Blocks are carryable, but toddlers interact with\n them while in the "Sit On Ground" posture, which must be provided by\n the object\'s parts.\n \n This field should be used with caution, since posture graph\n generation is one of our slowest systems. Do not enable it on\n objects such as food or drinks, since the world is bound to have\n many of them.\n ', tunable_type=bool, default=False), 'portal_key_mask_flags': TunableEnumFlags( description= "\n Any flag tuned here will be kept on the Sim routing context who's\n picking up this object. This will allow a Sim to pickup some\n type of objects and still be allowed to transition through\n some portals while carrying an object.\n ", enum_type=PortalFlags, allow_no_flags=True), 'reslot_plumbbob': OptionalTunable( description= '\n If tuned, the plumbbob will be repositioned when this item is carried.\n Reslot will always go away when sim stops carrying the object.\n ', tunable=TunableReslotPlumbbob()), 'defer_putdown': Tunable( description= '\n If true, the put down will be deferred to the end of the route. \n If false, put down will be done at the start of the route. This\n should be the default behavior. \n ', tunable_type=bool, default=False), 'put_down_height_tolerance': TunableRange( description= '\n Maximum height tolerance on the terrain we will use for the \n placement of this object when asking FGL to find a spot on the\n floor.\n Having a high value here will make it so an object can be placed\n in a terrain spot with a high slope that might result with\n clipping depending on the width of the object. The way this will\n work will be using the object footprint, if the edges are at a\n height higher than the height tolerance, then the location will\n not be valid.\n ', tunable_type=float, default=1.1, minimum=0) } def __init__(self, owner, **kwargs): super().__init__(owner, **kwargs) self._attempted_putdown = False self._attempted_alternative_putdown = False self._cached_put_down_strategy = None @property def attempted_putdown(self): return self._attempted_putdown @property def attempted_alternative_putdown(self): return self._attempted_alternative_putdown @property def ideal_slot_type_set(self): put_down_strategy = self.get_put_down_strategy() return put_down_strategy.ideal_slot_type_set @componentmethod def get_carry_object_posture(self): if self.carry_affordances is None: return CarryPostureStaticTuning.POSTURE_CARRY_OBJECT return self.carry_affordances[0].provided_posture_type @componentmethod_with_fallback(lambda *_, **__: ()) def get_allowed_hands(self, sim): return self.allowed_hands_data.get_allowed_hands(sim) @componentmethod_with_fallback(lambda *_, **__: 0) def get_portal_key_make_for_carry(self): return self.portal_key_mask_flags @componentmethod def should_unholster(self, *args, **kwargs): return self.unholster_when_routing.should_unholster(*args, **kwargs) @componentmethod def get_put_down_strategy(self, parent=DEFAULT): if self._cached_put_down_strategy is None: parent = self.owner.parent if parent is DEFAULT else parent species = parent.species if parent is not None else Species.HUMAN for (state_value, put_down_strategy ) in self.state_based_put_down_tuning.items(): if self.owner.state_value_active(state_value): self._cached_put_down_strategy = put_down_strategy.get( species) break else: if self.put_down_tuning is not None: self._cached_put_down_strategy = self.put_down_tuning.get( species) if self._cached_put_down_strategy is None: put_down_strategy = parent.get_default_put_down_strategy() self._cached_put_down_strategy = put_down_strategy return self._cached_put_down_strategy def _get_carry_transition_distance_for_sim(self, sim): carry_sim = self.owner.sim_info carrying_sim = sim.sim_info if carry_sim.is_toddler: return self.DEFAULT_GEOMETRIC_TRANSITION_MEDIUM if carrying_sim.is_teen_or_older: return self.DEFAULT_GEOMETRIC_TRANSITION_LARGE if carry_sim.is_teen_or_older: return self.DEFAULT_GEOMETRIC_TRANSITION_SMALL return self.DEFAULT_GEOMETRIC_TRANSITION_TINY def _get_adjusted_circle_constraint(self, sim, constraint): if not self.owner.is_sim: return constraint if not isinstance(constraint, TunedCircle): return constraint ideal_radius_override = self._get_carry_transition_distance_for_sim( sim) constraint = copy.copy(constraint) constraint.ideal_radius_width = constraint.ideal_radius_width / constraint.ideal_radius * ideal_radius_override constraint.radius = constraint.radius / constraint.ideal_radius * ideal_radius_override constraint.ideal_radius = ideal_radius_override return constraint @componentmethod def get_carry_transition_constraint(self, sim, position, routing_surface, cost=0, mobile=True): constraints = self.DEFAULT_GEOMETRIC_TRANSITION_CONSTRAINT constraints = constraints.constraint_mobile if mobile else constraints.constraint_non_mobile final_constraint = Anywhere() for constraint in constraints: if mobile: constraint = self._get_adjusted_circle_constraint( sim, constraint) final_constraint = final_constraint.intersect( constraint.create_constraint(None, None, target_position=position, routing_surface=routing_surface)) final_constraint = final_constraint.generate_constraint_with_cost(cost) final_constraint = final_constraint._copy(_multi_surface=True) return final_constraint @componentmethod def get_pick_up_constraint(self, sim): if self.constraint_pick_up is None: return final_constraint = Anywhere() for constraint in self.constraint_pick_up: constraint = self._get_adjusted_circle_constraint(sim, constraint) constraint = constraint.create_constraint(sim, target=self.owner) final_constraint = final_constraint.intersect(constraint) final_constraint = final_constraint._copy(_multi_surface=True) return final_constraint @componentmethod def get_provided_aops_gen(self, target, context, **kwargs): for provided_affordance_data in self.provided_affordances: if not provided_affordance_data.affordance.is_affordance_available( context=context): continue if not provided_affordance_data.object_filter.is_object_valid( target): continue if provided_affordance_data.affordance.is_social and not target.is_sim: affordance = provided_affordance_data.affordance interaction_target = self.owner preferred_objects = (target, ) else: affordance = CarryTargetInteraction.generate( provided_affordance_data.affordance, self.owner) interaction_target = target preferred_objects = () depended_on_si = None parent = self.owner.parent if parent is not None: if parent.is_sim: carry_posture = parent.posture_state.get_carry_posture( self.owner) if carry_posture is not None: if provided_affordance_data.is_linked: depended_on_si = carry_posture.source_interaction yield from affordance.potential_interactions( interaction_target, context, depended_on_si=depended_on_si, preferred_objects=preferred_objects, **kwargs) def component_super_affordances_gen(self, **kwargs): if self.carry_affordances is None: affordances = self.DEFAULT_CARRY_AFFORDANCES else: affordances = self.carry_affordances for affordance in affordances: yield affordance def component_interactable_gen(self): yield self def on_state_changed(self, state, old_value, new_value, from_init): if new_value in self.state_based_put_down_tuning or old_value in self.state_based_put_down_tuning: self._cached_put_down_strategy = None def component_reset(self, reset_reason): self.reset_put_down_count() @componentmethod def get_initial_put_down_position(self, carrying_sim=None): carrying_sim = carrying_sim or self.owner.parent if carrying_sim is None: return (self.owner.position, self.owner.routing_surface) additional_put_down_distance = carrying_sim.posture.additional_put_down_distance position = carrying_sim.position + carrying_sim.forward * ( carrying_sim.object_radius + additional_put_down_distance) sim_los_constraint = carrying_sim.lineofsight_component.constraint if not sims4.geometry.test_point_in_compound_polygon( position, sim_los_constraint.geometry.polygon): position = carrying_sim.position return (position, carrying_sim.routing_surface) @componentmethod def get_put_down_aop(self, interaction, context, alternative_multiplier=1, own_inventory_multiplier=1, object_inventory_multiplier=DEFAULT, in_slot_multiplier=DEFAULT, on_floor_multiplier=1, visibility_override=None, display_name_override=None, additional_post_run_autonomy_commodities=None, add_putdown_liability=False, **kwargs): sim = interaction.sim owner = self.owner if owner.transient: return self._get_destroy_aop(sim, **kwargs) put_down_strategy = self.get_put_down_strategy(parent=sim) if object_inventory_multiplier is DEFAULT: object_inventory_multiplier = sim.get_put_down_object_inventory_cost_override( ) if in_slot_multiplier is DEFAULT: in_slot_multiplier = sim.get_put_down_slot_cost_override() slot_types_and_costs = self.get_slot_types_and_costs( multiplier=in_slot_multiplier) (terrain_transform, terrain_routing_surface) = self._get_terrain_transform(interaction) objects = self._get_objects_with_inventory(interaction) objects = [obj for obj in objects if obj.can_access_for_putdown(sim)] if put_down_strategy.floor_cost is not None and on_floor_multiplier is not None: world_cost = put_down_strategy.floor_cost * on_floor_multiplier else: world_cost = None if put_down_strategy.inventory_cost is not None and own_inventory_multiplier is not None: sim_inventory_cost = put_down_strategy.inventory_cost * own_inventory_multiplier else: sim_inventory_cost = None if put_down_strategy.object_inventory_cost is not None and object_inventory_multiplier is not None: object_inventory_cost = put_down_strategy.object_inventory_cost * object_inventory_multiplier else: object_inventory_cost = None if not put_down_strategy.affordances: self._attempted_alternative_putdown = True if not self._attempted_alternative_putdown or self.owner.is_sim: self._attempted_alternative_putdown = True scored_aops = [] for scored_aop in self._gen_affordance_score_and_aops( interaction, slot_types_and_costs=slot_types_and_costs, world_cost=world_cost, sim_inventory_cost=sim_inventory_cost, object_inventory_cost=object_inventory_cost, terrain_transform=terrain_transform, terrain_routing_surface=terrain_routing_surface, objects_with_inventory=objects, visibility_override=visibility_override, display_name_override=display_name_override, additional_post_run_autonomy_commodities= additional_post_run_autonomy_commodities, multiplier=alternative_multiplier, add_putdown_liability=add_putdown_liability): if scored_aop.aop.test(context): scored_aops.append(scored_aop) if scored_aops: scored_aops.sort(key=operator.itemgetter(0)) return scored_aops[-1].aop affordance = CarryableComponent.PUT_DOWN_ANYWHERE_AFFORDANCE if add_putdown_liability: liabilities = ((PutDownLiability.LIABILITY_TOKEN, PutDownLiability(self.owner)), ) else: liabilities = () aop = AffordanceObjectPair( affordance, self.owner, affordance, None, slot_types_and_costs=slot_types_and_costs, world_cost=world_cost, sim_inventory_cost=sim_inventory_cost, object_inventory_cost=object_inventory_cost, terrain_transform=terrain_transform, terrain_routing_surface=terrain_routing_surface, objects_with_inventory=objects, visibility_override=visibility_override, display_name_override=display_name_override, additional_post_run_autonomy_commodities= additional_post_run_autonomy_commodities, liabilities=liabilities, **kwargs) self._attempted_putdown = True return aop def _gen_affordance_score_and_aops( self, interaction, slot_types_and_costs, world_cost, sim_inventory_cost, object_inventory_cost, terrain_transform, terrain_routing_surface, objects_with_inventory, visibility_override, display_name_override, additional_post_run_autonomy_commodities, multiplier=1, add_putdown_liability=False): put_down_strategy = self.get_put_down_strategy() for affordance in put_down_strategy.affordances: if add_putdown_liability: liabilities = ((PutDownLiability.LIABILITY_TOKEN, PutDownLiability(self.owner)), ) else: liabilities = () aop = AffordanceObjectPair( affordance, self.owner, affordance, None, slot_types_and_costs=slot_types_and_costs, world_cost=world_cost, sim_inventory_cost=sim_inventory_cost, object_inventory_cost=object_inventory_cost, terrain_transform=terrain_transform, terrain_routing_surface=terrain_routing_surface, objects_with_inventory=objects, visibility_override=visibility_override, display_name_override=display_name_override, additional_post_run_autonomy_commodities= additional_post_run_autonomy_commodities, liabilities=liabilities) yield ScoredAOP(multiplier, aop) def _get_cost_for_slot_type(self, slot_type): put_down_strategy = self.get_put_down_strategy() if slot_type in self.owner.ideal_slot_types: return put_down_strategy.preferred_slot_cost return put_down_strategy.normal_slot_cost def get_slot_types_and_costs(self, multiplier=1): slot_types_and_costs = [] for slot_type in self.owner.all_valid_slot_types: cost = self._get_cost_for_slot_type(slot_type) if cost is not None and multiplier is not None: cost *= multiplier else: cost = None slot_types_and_costs.append((slot_type, cost)) return slot_types_and_costs def _get_terrain_transform(self, interaction): if not self.owner.is_sim and self.owner.footprint_component is None: return (None, None) else: sim = interaction.sim put_down_position = interaction.interaction_parameters.get( 'put_down_position') put_down_routing_surface = interaction.interaction_parameters.get( 'put_down_routing_surface') if put_down_position is None: (starting_position, starting_routing_surface ) = self.get_initial_put_down_position(carrying_sim=sim) else: starting_position = put_down_position starting_routing_surface = put_down_routing_surface starting_location = placement.create_starting_location( position=starting_position, orientation=sim.orientation, routing_surface=starting_routing_surface) if self.owner.is_sim: search_flags = FGLSearchFlagsDefaultForSim | FGLSearchFlag.STAY_IN_CURRENT_BLOCK fgl_context_fn = functools.partial( placement.create_fgl_context_for_sim, search_flags=search_flags) else: search_flags = FGLSearchFlag.STAY_IN_CURRENT_BLOCK | FGLSearchFlag.SHOULD_TEST_ROUTING | FGLSearchFlag.CALCULATE_RESULT_TERRAIN_HEIGHTS | FGLSearchFlag.DONE_ON_MAX_RESULTS | FGLSearchFlag.SHOULD_TEST_BUILDBUY fgl_context_fn = functools.partial( placement.create_fgl_context_for_object, search_flags=search_flags) MAX_PUTDOWN_STEPS = 8 MAX_PUTDOWN_DISTANCE = 10 fgl_context = fgl_context_fn( starting_location, self.owner, max_steps=MAX_PUTDOWN_STEPS, max_distance=MAX_PUTDOWN_DISTANCE, height_tolerance=self.put_down_height_tolerance) (position, orientation) = placement.find_good_location(fgl_context) if position is not None: put_down_transform = sims4.math.Transform( position, orientation) return (put_down_transform, starting_routing_surface) return (None, None) def _get_objects_with_inventory(self, interaction): objects = [] inventory_item = self.owner.inventoryitem_component if inventory_item is not None: if CarryableComponent.PUT_IN_INVENTORY_AFFORDANCE is not None: for obj in inventory_item.valid_object_inventory_gen(): objects.append(obj) return objects def _get_destroy_aop(self, sim, **kwargs): affordance = CarryableComponent.PUT_DOWN_HERE_AFFORDANCE return AffordanceObjectPair(affordance, self.owner, affordance, None, put_down_transform=None, **kwargs) def reset_put_down_count(self): self._attempted_alternative_putdown = False self._attempted_putdown = False self._cached_put_down_strategy = None def on_object_carry(self, actor, *_, **__): if self.reslot_plumbbob is not None: reslot_plumbbob(actor, self.reslot_plumbbob) def on_object_uncarry(self, actor, *_, **__): if self.reslot_plumbbob is not None: unslot_plumbbob(actor)
class WalksStyleBehavior(HasTunableSingletonFactory, AutoFactoryInit): SWIMMING_WALKSTYLES = TunableList( description= '\n The exhaustive list of walkstyles allowed while Sims are swimming. If a\n Sim has a request for a walkstyle that is not supported, the first\n element is used as a replacement.\n ', tunable=TunableWalkstyle(pack_safe=True)) WALKSTYLE_COST = TunableMapping( description= '\n Associate a specific walkstyle to a statistic cost before the walkstyle\n can be activated.\n ', key_type=TunableWalkstyle( description= '\n The walkstyle that should have a specified cost when triggered.\n ', pack_safe=True), value_type=TunableTuple( description= '\n Cost data of the specified walkstyle.\n ', walkstyle_cost_statistic=TunableReference( description= '\n The statistic we are operating on when the walkstyle is\n triggered.\n ', manager=services.get_instance_manager( sims4.resources.Types.STATISTIC), pack_safe=True), cost=TunableRange( description= '\n When the walkstyle is triggered during a route, this is the\n cost that will be substracted from the specified statistic. \n ', tunable_type=int, default=1, minimum=0))) WALKSTYLES_OVERRIDE_TELEPORT = TunableList( description= '\n Any walkstyles found here will be able to override the teleport styles\n if they are specified.\n ', tunable=TunableWalkstyle(pack_safe=True)) FACTORY_TUNABLES = { 'carry_walkstyle_behavior': OptionalTunable( description= '\n Define the walkstyle the Sim plays whenever they are being carried\n by another Sim.\n \n If this is set to "no behavior", the Sim will not react to the\n parent\'s walkstyle at all. They will play no walkstyle, and rely on\n posture idles to animate.\n \n If this is set, Sims have the ability to modify their walkstyle\n whenever their parent is routing.\n ', tunable=TunableTuple( description= '\n Specify how this Sim behaves when being carried by another Sim.\n ', default_carry_walkstyle=TunableWalkstyle( description= '\n Unless an override is specified, this is the walkstyle\n applied to the Sim whenever they are being carried.\n ' ), carry_walkstyle_overrides=TunableMapping( description= '\n Define carry walkstyle overrides. For instance, we might\n want to specify a different carry walkstyle if the parent is\n frantically running, for example.\n ', key_type=TunableWalkstyle( description= '\n The walkstyle that this carry walkstyle override applies to.\n ', pack_safe=True), value_type=TunableWalkstyle( description= '\n The carry walkstyle override.\n ', pack_safe=True))), enabled_name='Apply_Carry_Walkstyle', disabled_name='No_Behavior'), 'combo_walkstyle_replacements': TunableList( description= '\n The prioritized list of the combo walkstyle replacement rules. We\n use this list to decide if a Sim should use a combo walk style based\n on the the highest priority walkstyle request, and other walkstyles\n that might affect the replacement based on the key combo rules.\n ', tunable=TunableTuple( description= '\n The n->1 mapping of walkstyle replacement. \n ', key_combo_list=TunableList( description= '\n The list of the walkstyles used as key combos. If the\n current highest priority walkstyle exists in this list, and\n the Sim has every other walkstyle in the key list, then we\n replace this with the result walkstyle tuned in the tuple.\n ', tunable=TunableWalkstyle(pack_safe=True)), result=TunableWalkstyle( description= '\n The mapped combo walkstyle.\n ', pack_safe=True))), 'default_walkstyle': TunableWalkstyle( description= '\n The underlying walkstyle for this Sim. This is most likely going to\n be overridden by the CAS walkstyle, emotional walkstyles, buff\n walkstyles, etc...\n ' ), 'run_allowed_flags': TunableEnumFlags( description= "\n Define where the Sim is allowed to run. Certain buffs might suppress\n a Sim's ability to run.\n ", enum_type=WalkStyleRunAllowedFlags, default=WalkStyleRunAllowedFlags.RUN_ALLOWED_OUTDOORS, allow_no_flags=True), 'run_disallowed_walkstyles': TunableList( description= "\n A set of walkstyles that would never allow a Sim to run, i.e., if\n the Sim's requested walkstyle is in this set, they will not run,\n even to cover great distances.\n ", tunable=TunableWalkstyle(pack_safe=True)), 'run_required_total_distance': TunableRange( description= '\n For an entire route, the minimum distance required for Sim to run.\n ', tunable_type=float, minimum=0, default=20), 'run_required_segment_distance': TunableRange( description= '\n For a specific route segment, the minimum distance required for the\n Sim to run.\n ', tunable_type=float, minimum=0, default=10), 'run_walkstyle': TunableWalkstyle( description= '\n The walkstyle to use when this Sim is supposed to be running.\n ' ), 'wading_walkstyle': OptionalTunable( description= '\n If enabled, the routing agent will play a different walkstyle when\n walking through water.\n ', tunable=TunableWalkstyle( description= '\n The walkstyle to use when wading through water.\n ' )), 'wading_walkstyle_buff': OptionalTunable( description= '\n A buff which, if tuned, will be on the sim if the sim is currently\n in wading level water.\n ', tunable=TunableReference(manager=services.get_instance_manager( sims4.resources.Types.BUFF))), 'short_walkstyle': TunableWalkstyle( description= '\n The walkstyle to use when Sims are routing over a distance shorter\n than the one defined in "Short Walkstyle Distance" or any of the\n overrides.\n \n This value is used if no override is tuned in "Short Walkstyle Map".\n ' ), 'short_walkstyle_distance': TunableRange( description= "\n Any route whose distance is less than this value will request the\n short version of the Sim's current walkstyle.\n ", tunable_type=float, minimum=0, default=7), 'short_walkstyle_distance_override_map': TunableMapping( description= "\n If a Sim's current walkstyle is any of the ones specified in here,\n use the associated value to determine if the short version of the\n walkstyle is to be requested.\n ", key_type=TunableWalkstyle( description= '\n The walkstyle that this distance override applies to.\n ', pack_safe=True), value_type=TunableRange( description= "\n Any route whose distance is less than this value will request\n the short version of the Sim's current walkstyle, provided the\n Sim's current walkstyle is the associated walkstyle.\n ", tunable_type=float, minimum=0, default=7)), 'short_walkstyle_map': TunableMapping( description= '\n Associate a specific short version of a walkstyle to walkstyles.\n ', key_type=TunableWalkstyle( description= '\n The walkstyle that this short walkstyle mapping applies to.\n ', pack_safe=True), value_type=TunableWalkstyle( description= '\n The short version of the associated walkstyle.\n ', pack_safe=True)) } def _get_walkstyle_overrides(self, actor): if actor.is_sim: return tuple(buff.walkstyle_behavior_override for buff in actor.get_active_buff_types() if buff.walkstyle_behavior_override is not None) return () def _apply_run_walkstyle_to_path(self, actor, path, walkstyle_overrides, time_offset=None): run_allowed_flags = self.run_allowed_flags for walkstyle_override in walkstyle_overrides: run_allowed_flags |= walkstyle_override.additional_run_flags for walkstyle_override in walkstyle_overrides: run_allowed_flags &= ~walkstyle_override.removed_run_flags if not run_allowed_flags: return run_required_total_distance = sims4.math.safe_max( (override for override in walkstyle_overrides if override.run_required_total_distance is not None), key=operator.attrgetter('walkstyle_behavior_priority'), default=self).run_required_total_distance if path.length() < run_required_total_distance: return run_required_segment_distance = sims4.math.safe_max( (override for override in walkstyle_overrides if override.run_required_segment_distance is not None), key=operator.attrgetter('walkstyle_behavior_priority'), default=self).run_required_segment_distance path_nodes = list(path.nodes) all_path_node_data = [] for (start_node, end_node) in zip(path_nodes, path_nodes[1:]): switch_routing_surface = start_node.routing_surface_id != end_node.routing_surface_id is_outside = start_node.portal_id == 0 and get_block_id_for_node( start_node) == 0 route_key = (switch_routing_surface, is_outside) all_path_node_data.append((route_key, start_node, end_node)) run_walkstyle = self.get_run_walkstyle(actor) for ((_, is_outside), path_node_data) in itertools.groupby(all_path_node_data, key=operator.itemgetter(0)): if is_outside and not run_allowed_flags & WalkStyleRunAllowedFlags.RUN_ALLOWED_OUTDOORS: continue if not is_outside and not run_allowed_flags & WalkStyleRunAllowedFlags.RUN_ALLOWED_INDOORS: continue path_node_data = list(path_node_data) segment_length = sum( (sims4.math.Vector3(*start_node.position) - sims4.math.Vector3(*end_node.position)).magnitude_2d() for (_, start_node, end_node) in path_node_data) if segment_length < run_required_segment_distance: continue for (_, path_node, _) in path_node_data: if not time_offset is None: if path_node.time >= time_offset: path_node.walkstyle = run_walkstyle path_node.walkstyle = run_walkstyle def check_for_wading(self, sim, *_, **__): routing_component = sim.routing_component if not routing_component.last_route_has_wading_nodes and not routing_component.wading_buff_handle: return wading_interval = OceanTuning.get_actor_wading_interval(sim) if wading_interval is None: return water_height = get_water_depth_at_location(sim.location) if water_height in wading_interval: if routing_component.wading_buff_handle is None: routing_component.wading_buff_handle = sim.add_buff( self.wading_walkstyle_buff) elif routing_component.wading_buff_handle is not None: sim.remove_buff(routing_component.wading_buff_handle) routing_component.wading_buff_handle = None def _apply_wading_walkstyle_to_path(self, actor, path, default_walkstyle, time_offset=None): if actor.is_sim and actor.sim_info.is_ghost: return False wading_interval = OceanTuning.get_actor_wading_interval(actor) if wading_interval is None: return False wading_walkstyle = self.get_wading_walkstyle(actor) if wading_walkstyle is None: return False def get_node_water_height(path_node): return get_water_depth(path_node.position[0], path_node.position[2], path_node.routing_surface_id.secondary_id) path_nodes = list(path.nodes) start_wading = get_node_water_height(path_nodes[0]) in wading_interval end_wading = get_node_water_height(path_nodes[-1]) in wading_interval if not start_wading and not end_wading: return False path_contains_wading = False for (start_node, end_node) in zip(path_nodes, path_nodes[1:]): if time_offset is not None and end_node.time < time_offset: continue if start_node.routing_surface_id.type == SurfaceType.SURFACETYPE_POOL: continue if start_node.portal_object_id != 0: continue start_wading = get_node_water_height(start_node) in wading_interval end_wading = get_node_water_height(end_node) in wading_interval if not start_wading and not end_wading: continue is_wading = start_wading if is_wading: start_node.walkstyle = wading_walkstyle path_contains_wading = True nodes_to_add = [] for (transform, routing_surface, time) in path.get_location_data_along_segment_gen( start_node.index, end_node.index, time_step=0.3): should_wade = get_water_depth( transform.translation[0], transform.translation[2]) in wading_interval if is_wading and not should_wade: is_wading = False nodes_to_add.append( (Location(transform.translation, transform.orientation, routing_surface), time, 0, default_walkstyle, 0, 0, end_node.index)) elif not is_wading: if should_wade: is_wading = True nodes_to_add.append( (Location(transform.translation, transform.orientation, routing_surface), time, 0, wading_walkstyle, 0, 0, end_node.index)) path_contains_wading = True running_index_offset = 0 for (loc, time, node_type, walkstyle, portal_obj_id, portal_id, index) in nodes_to_add: node_index = index + running_index_offset path.nodes.add_node(loc, time, node_type, walkstyle, portal_obj_id, portal_id, node_index) node = path.nodes[node_index] node.is_procedural = False running_index_offset += 1 return path_contains_wading def apply_walkstyle_to_path(self, actor, path, time_offset=None): gsi_archiver = None can_archive = gsi_handlers.walkstyle_handlers.archiver.enabled and actor.is_sim if can_archive: gsi_archiver = WalkstyleGSIArchiver(actor) walkstyle = self.get_walkstyle_for_path(actor, path, gsi_archiver) if can_archive: gsi_archiver.gsi_archive_entry() path_nodes = list(path.nodes) for path_node in path_nodes: if not time_offset is None: if path_node.time >= time_offset: path_node.walkstyle = walkstyle path_node.walkstyle = walkstyle walkstyle_overrides = self._get_walkstyle_overrides(actor) if walkstyle not in self.run_disallowed_walkstyles: self._apply_run_walkstyle_to_path(actor, path, walkstyle_overrides, time_offset=time_offset) if actor.is_sim: actor.routing_component.last_route_has_wading_nodes = self._apply_wading_walkstyle_to_path( actor, path, walkstyle, time_offset=time_offset) return walkstyle def get_combo_replacement(self, highest_priority_walkstyle, walkstyle_list): for combo_tuple in self.combo_walkstyle_replacements: key_combo_list = combo_tuple.key_combo_list if highest_priority_walkstyle in key_combo_list: if all(ws in walkstyle_list for ws in key_combo_list): return combo_tuple def get_combo_replaced_walkstyle(self, highest_priority_walkstyle, walkstyle_list): combo_tuple = self.get_combo_replacement(highest_priority_walkstyle, walkstyle_list) if combo_tuple is not None: return combo_tuple.result def get_default_walkstyle(self, actor, gsi_archiver=None): walkstyle = actor.get_cost_valid_walkstyle( WalksStyleBehavior.WALKSTYLE_COST) walkstyle_list = actor.get_walkstyle_list() replaced_walkstyle = self.get_combo_replaced_walkstyle( walkstyle, walkstyle_list) if gsi_archiver is not None: gsi_archiver.default_walkstyle = walkstyle gsi_archiver.combo_replacement_walkstyle_found = replaced_walkstyle if replaced_walkstyle is not None: walkstyle = replaced_walkstyle return walkstyle def get_short_walkstyle(self, walkstyle, actor): short_walkstyle = self._get_property_override(actor, 'short_walkstyle') return self.short_walkstyle_map.get(walkstyle, short_walkstyle) def get_run_walkstyle(self, actor): run_walkstyle = self._get_property_override(actor, 'run_walkstyle') return run_walkstyle def get_wading_walkstyle(self, actor): wading_walkstyle = self._get_property_override(actor, 'wading_walkstyle') return wading_walkstyle def supports_wading_walkstyle_buff(self, actor): return self.wading_walkstyle_buff is not None and OceanTuning.get_actor_wading_interval( actor) def _get_property_override(self, actor, property_name): overrides = self._get_walkstyle_overrides(actor) override = sims4.math.safe_max( (override for override in overrides if getattr(override, property_name) is not None), key=operator.attrgetter('walkstyle_behavior_priority'), default=self) property_value = getattr(override, property_name) return property_value def _apply_walkstyle_cost(self, actor, walkstyle): walkstyle_cost = WalksStyleBehavior.WALKSTYLE_COST.get(walkstyle, None) if walkstyle_cost is not None: stat_instance = actor.get_stat_instance( walkstyle_cost.walkstyle_cost_statistic) if stat_instance is None: logger.error( 'Statistic {}, not found on Sim {} for walkstyle cost', walkstyle_cost.walkstyle_cost_statistic, actor, owner='camilogarcia') return stat_instance.add_value(-walkstyle_cost.cost) def get_walkstyle_for_path(self, actor, path, gsi_archiver=None): walkstyle = self.get_default_walkstyle(actor, gsi_archiver) if gsi_archiver is not None: gsi_archiver.walkstyle_requests = actor.routing_component.get_walkstyle_requests( ) short_walk_distance = self.short_walkstyle_distance_override_map.get( walkstyle, self.short_walkstyle_distance) if path.length() < short_walk_distance: walkstyle = self.get_short_walkstyle(walkstyle, actor) if gsi_archiver is not None: gsi_archiver.default_walkstyle_replaced_by_short_walkstyle = walkstyle if actor.is_sim: if actor.in_pool and walkstyle not in self.SWIMMING_WALKSTYLES: walkstyle = self.SWIMMING_WALKSTYLES[0] if gsi_archiver is not None: gsi_archiver.default_walkstyle_replaced_by_swimming_walkstyle = walkstyle return walkstyle else: posture = actor.posture if posture.mobile and posture.compatible_walkstyles and walkstyle not in posture.compatible_walkstyles: walkstyle = posture.compatible_walkstyles[0] if gsi_archiver is not None: gsi_archiver.default_walkstyle_replaced_by_posture_walkstyle = walkstyle return walkstyle return walkstyle
class DoorService(Service): FRONT_DOOR_ALLOWED_PORTAL_FLAGS = TunableEnumFlags( description= "\n Door Service does a routability check to all doors from the lot's\n arrival spawn point to find doors that are reachable without crossing\n other doors.\n \n These flags are supplied to the routability check's PathPlanContext, to\n tell it what portals are usable. For example, stair portals should be\n allowed (e.g. for front doors off the ground level, or house is on a\n foundation).\n ", enum_type=PortalFlags) FRONT_DOOR_ALLOWED_APARTMENT_PORTAL_FLAGS = TunableEnumFlags( description= '\n Additional portal flags to be used if needing to choose between\n multiple external doors in an apartment.\n \n e.g. Elevator.\n ', enum_type=PortalFlags) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._front_door_id = None self._plex_door_infos = EMPTY_SET @classproperty def save_error_code(cls): return persistence_error_types.ErrorCodes.SERVICE_SAVE_FAILED_DOOR_SERVICE def has_front_door(self): return self._front_door_id is not None def get_front_door(self): return services.object_manager().get(self._front_door_id) def on_door_removed(self, door): if door.id == self._front_door_id: self._front_door_id = None def fix_up_doors(self, force_refresh=False): building_type = services.get_plex_service().get_plex_building_type( services.current_zone_id()) if building_type == PlexBuildingType.DEFAULT or building_type == PlexBuildingType.PENTHOUSE_PLEX or building_type == PlexBuildingType.COASTAL: self._fix_up(force_refresh=force_refresh) elif building_type == PlexBuildingType.FULLY_CONTAINED_PLEX: self._fix_up_for_apartments() services.object_manager().on_front_door_candidates_changed() def _fix_up(self, force_refresh): (exterior_door_infos, interior_doors) = self._get_exterior_and_interior_doors() backward_doors = set(info.door for info in exterior_door_infos if info.is_backwards) self._flip_backward_doors(backward_doors) self._set_front_door_availabilities(exterior_door_infos, interior_doors) preferred_door_id = self._front_door_id if not force_refresh else None new_front_door = self._choose_front_door( exterior_door_infos, preferred_door_id=preferred_door_id) self.set_as_front_door(new_front_door) def set_as_front_door(self, door): if door is None and self._front_door_id is None: return if door is not None and self._front_door_id == door.id: return old_door = services.object_manager().get(self._front_door_id) if old_door is not None: old_door.set_front_door_status(False) self._front_door_id = None if door is not None: door.set_front_door_status(True) self._front_door_id = door.id def _flip_backward_doors(self, doors): for door in doors: door.swap_there_and_back() def _set_front_door_availabilities(self, exterior_door_infos, interior_doors): zone_requires_front_door = self._zone_requires_front_door() for info in exterior_door_infos: info.door.set_front_door_availability(zone_requires_front_door) for door in interior_doors: door.set_front_door_availability(False) def _zone_requires_front_door(self): zone = services.current_zone() venue = zone.venue_service.active_venue requires_front_door = venue.venue_requires_front_door if requires_front_door == VenueFrontdoorRequirement.NEVER: return False if requires_front_door == VenueFrontdoorRequirement.ALWAYS: return True if requires_front_door == VenueFrontdoorRequirement.OWNED_OR_RENTED: if services.travel_group_manager().is_current_zone_rented(): return True return zone.lot.owner_household_id != 0 logger.error( 'Current venue {} at Zone {} has front door requirement set to invalid value: {}', venue, zone, requires_front_door, owner='trevor') return False def _choose_front_door(self, exterior_door_infos, preferred_door_id=None): if not self._zone_requires_front_door(): return if preferred_door_id is not None: for info in exterior_door_infos: if info.door.id == preferred_door_id: return info.door if not exterior_door_infos: return info = min(exterior_door_infos, key=operator.attrgetter('distance')) return info.door def _object_is_door(self, obj): if not isinstance(obj, Door): return False elif not obj.is_door_portal: return False return True def _get_doors(self): doors = frozenset(obj for obj in services.object_manager().values() if self._object_is_door(obj)) return doors def _get_arrival_point(self): zone = services.current_zone() spawn_point = zone.active_lot_arrival_spawn_point if spawn_point is not None: return spawn_point.get_approximate_center() logger.error( 'Active lot missing lot arrival spawn points. This will cause incorrect front door behavior.', zone.lot.lot_id) return zone.lot.corners[1] def _get_exterior_and_interior_doors(self): doors = self._get_doors() connections = self._get_door_connections_from_arrival(doors) exterior_door_to_infos = {} for (_, handle, distance) in connections: old_info = exterior_door_to_infos.get(handle.door) if old_info is not None and not handle.is_front: continue is_backwards = not handle.is_front info = ExteriorDoorInfo(door=handle.door, distance=distance, is_backwards=is_backwards) exterior_door_to_infos[handle.door] = info interior_doors = frozenset(door for door in doors if door not in exterior_door_to_infos) return (frozenset(exterior_door_to_infos.values()), interior_doors) def _get_door_connections_from_arrival(self, doors, is_apartment=False): zone = services.current_zone() source_point = self._get_arrival_point() source_handles = set() source_handle = Handle( source_point, routing.SurfaceIdentifier(zone.id, 0, routing.SurfaceType.SURFACETYPE_WORLD)) source_handles.add(source_handle) routing_context = routing.PathPlanContext() for door in doors: for portal_handle in door.get_portal_pairs(): routing_context.lock_portal(portal_handle.there) routing_context.lock_portal(portal_handle.back) routing_context.set_key_mask(routing.FOOTPRINT_KEY_ON_LOT | routing.FOOTPRINT_KEY_OFF_LOT) if is_apartment: routing_context.set_portal_key_mask( DoorService.FRONT_DOOR_ALLOWED_PORTAL_FLAGS | DoorService.FRONT_DOOR_ALLOWED_APARTMENT_PORTAL_FLAGS) else: routing_context.set_portal_key_mask( DoorService.FRONT_DOOR_ALLOWED_PORTAL_FLAGS) dest_handles = set() for door in doors: (front_position, back_position) = door.get_door_positions() if not front_position is None: if back_position is None: continue dest_handles.add( DoorConnectivityHandle(front_position, door.routing_surface, door=door, is_front=True)) dest_handles.add( DoorConnectivityHandle(back_position, door.routing_surface, door=door, is_front=False)) connections = () if dest_handles: connections = routing.estimate_path_batch( source_handles, dest_handles, routing_context=routing_context) if connections is None: connections = () return connections def _fix_up_for_apartments(self): plex_door_infos = self.get_plex_door_infos(force_refresh=True) backward_doors = set() active_zone_id = services.current_zone_id() object_manager = services.object_manager() plex_doors = [] for info in plex_door_infos: household_id = services.get_persistence_service( ).get_household_id_from_zone_id(info.zone_id) door = object_manager.get(info.door_id) if door is None: logger.error('Plex Door {} does not exist.', info.door_id, owner='rmccord') else: if info.is_backwards: backward_doors.add(door) current_zone = services.current_zone() lot = current_zone.lot world_description_id = services.get_world_description_id( current_zone.world_id) lot_description_id = services.get_lot_description_id( lot.lot_id, world_description_id) neighborhood_id = current_zone.neighborhood_id neighborhood_data = services.get_persistence_service( ).get_neighborhood_proto_buff(neighborhood_id) logger.error( 'For WB: An apartment door facing the common area needs to be flipped. Lot desc id: {}, World desc id: {}. Neighborhood id: {}, Neighborhood Name: {}', lot_description_id, world_description_id, neighborhood_id, neighborhood_data.name) door.set_household_owner_id(household_id) if info.zone_id == active_zone_id: plex_doors.append(door) else: door.set_inactive_apartment_door_status(True) self._flip_backward_doors(backward_doors) if not plex_doors: return if len(plex_doors) == 1: self.set_as_front_door(plex_doors[0]) return logger.warn( "plex zone_id: {} has multiple potential front doors: {}, can lead to sims able to access areas they shouldn't", active_zone_id, plex_doors) best_door = None best_distance = None connections = self._get_door_connections_from_arrival( plex_doors, is_apartment=True) for (_, handle, distance) in connections: if not best_distance is None: if best_distance < distance: best_door = handle.door best_distance = distance best_door = handle.door best_distance = distance if best_door is None: logger.error( 'Unable to route to plex doors in zone_id: {} potential doors: {}', active_zone_id, plex_doors) self.set_as_front_door(plex_doors[0]) return self.set_as_front_door(best_door) def unlock_all_doors(self): doors = self._get_doors() for door in doors: door.remove_locks() def get_plex_door_infos(self, force_refresh=False): if self._plex_door_infos and not force_refresh: return self._plex_door_infos plex_service = services.get_plex_service() doors = self._get_doors() plex_door_infos = set() for door in doors: (front_position, back_position) = door.get_door_positions() if front_position is None or back_position is None: logger.error("Door '{}' has broken portals.", door) else: front_zone_id = plex_service.get_plex_zone_at_position( front_position, door.level) back_zone_id = plex_service.get_plex_zone_at_position( back_position, door.level) if front_zone_id is None and back_zone_id is None: current_zone = services.current_zone() lot = current_zone.lot world_description_id = services.get_world_description_id( current_zone.world_id) lot_description_id = services.get_lot_description_id( lot.lot_id, world_description_id) neighborhood_id = current_zone.neighborhood_id neighborhood_data = services.get_persistence_service( ).get_neighborhood_proto_buff(neighborhood_id) if False and EXEMPT_DOOR_WORLD_DESCRIPTION_ID == world_description_id and EXEMPT_DOOR_LOT_DESCRIPTION_ID == lot_description_id: continue logger.error( "Door isn't part of any plex. This will require WB fix. Door: {}, Lot desc id: {}, World desc id: {}. Neighborhood id: {}, Neighborhood Name: {}", door, lot_description_id, world_description_id, neighborhood_id, neighborhood_data.name) else: if front_zone_id == back_zone_id: continue zone_id = front_zone_id or back_zone_id is_backwards = front_zone_id is not None info = PlexDoorInfo(door_id=door.id, zone_id=zone_id, is_backwards=is_backwards) plex_door_infos.add(info) self._plex_door_infos = frozenset(plex_door_infos) return self._plex_door_infos def save(self, zone_data=None, **kwargs): if zone_data is not None: if self._front_door_id is not None: zone_data.front_door_id = self._front_door_id def load(self, zone_data=None): for door in self._get_doors(): door.set_front_door_status(False) door.set_front_door_availability(False) door.set_inactive_apartment_door_status(False) if zone_data is not None and zone_data.HasField('front_door_id'): door = services.object_manager().get(zone_data.front_door_id) if door is not None: self.set_as_front_door(door)