def get_constraint(self, sim, **kwargs): carryable = self._obj.get_component(CARRYABLE_COMPONENT) if carryable is not None and carryable.constraint_pick_up is not None: constraint_total = Anywhere() for constraint_factory in carryable.constraint_pick_up: constraint = constraint_factory.create_constraint(sim, target=self._obj, routing_surface=self._obj.routing_surface) constraint_total = constraint_total.intersect(constraint) return constraint_total return super().get_constraint(sim)
def constraint_intersection(self): if self._constraint_intersection_dirty: intersection = Anywhere() for constraint in set(self._constraints.values()): new_intersection = intersection.intersect(constraint) if not self._invalid_expected and not new_intersection.valid: logger.error('Invalid constraint intersection for PostureState:{}', self) intersection = new_intersection break intersection = new_intersection self._constraint_intersection_dirty = False self._constraint_intersection = intersection return self._constraint_intersection
class Broadcaster(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager( sims4.resources.Types.BROADCASTER)): __qualname__ = 'Broadcaster' FREQUENCY_ENTER = 0 FREQUENCY_PULSE = 1 INSTANCE_TUNABLES = { 'constraints': TunableList( description= '\n A list of constraints that define the area of influence of this\n broadcaster. It is required that at least one constraint be defined.\n ', tunable=TunableGeometricConstraintVariant()), 'effects': TunableList( description= '\n A list of effects that are applied to Sims and objects affected by\n this broadcaster.\n ', tunable=TunableBroadcasterEffectVariant()), 'frequency': TunableVariant( description= '\n Define in what instances and how often this broadcaster affects Sims\n and objects in its area of influence.\n ', on_enter=TunableTuple( description= '\n Sims and objects are affected by this broadcaster when they\n enter in its area of influence, or when the broadcaster is\n created.\n ', locked_args={'frequency_type': FREQUENCY_ENTER}, allow_multiple=Tunable( description= "\n If checked, then Sims may react multiple times if they re-\n enter the broadcaster's area of influence. If unchecked,\n then Sims will only react to the broadcaster once.\n ", tunable_type=bool, needs_tuning=True, default=False)), on_pulse=TunableTuple( description= '\n Sims and objects are constantly affected by this broadcaster\n while they are in its area of influence.\n ', locked_args={'frequency_type': FREQUENCY_PULSE}, cooldown_time=TunableSimMinute( description= '\n The time interval between broadcaster pulses. Sims would not\n react to the broadcaster for at least this amount of time\n while in its area of influence.\n ', default=8)), default='on_pulse'), 'clustering': OptionalTunable( description= '\n If set, then similar broadcasters, i.e. broadcasters of the same\n instance, will be clustered together if their broadcasting objects\n are close by. This improves performance and avoids having Sims react\n multiple times to similar broadcasters. When broadcasters are\n clustered together, there is no guarantee as to what object will be\n used for testing purposes.\n \n e.g. Stinky food reactions are clustered together. A test on the\n broadcaster should not, for example, differentiate between a stinky\n lobster and a stinky steak, because the broadcasting object is\n arbitrary and undefined.\n \n e.g. Jealousy reactions are not clustered together. A test on the\n broadcaster considers the relationship between two Sims. Therefore,\n it would be unwise to consider an arbitrary Sim if two jealousy\n broadcasters are close to each other.\n ', tunable=ObjectClusterRequest.TunableFactory( description= '\n Specify how clusters for this particular broadcaster are formed.\n ', locked_args={'minimum_size': 1}), enabled_by_default=True), 'allow_objects': Tunable( description= '\n If checked, then in addition to all instantiated Sims, all objects\n will be affected by this broadcaster. Some tuned effects might still\n only apply to Sims (e.g. affordance pushing).\n \n Checking this tuning field has performance repercussions, as it\n means we have to process a lot more data during broadcaster pings.\n Please use this sporadically.\n ', tunable_type=bool, default=False) } @classmethod def _verify_tuning_callback(cls): if not cls.constraints: logger.error('Broadcaster {} does not define any constraints.', cls) @classmethod def register_static_callbacks(cls, *args, **kwargs): for broadcaster_effect in cls.effects: broadcaster_effect.register_static_callbacks(*args, **kwargs) def __init__(self, *args, broadcasting_object, interaction=None, **kwargs): super().__init__(*args, **kwargs) self._broadcasting_object_ref = weakref.ref( broadcasting_object, self._on_broadcasting_object_deleted) self._interaction = interaction self._constraint = None self._affected_objects = weakref.WeakKeyDictionary() self._current_objects = weakref.WeakSet() self._linked_broadcasters = weakref.WeakSet() broadcasting_object.register_on_location_changed( self._on_broadcasting_object_moved) self._quadtree = None self._cluster_request = None @property def broadcasting_object(self): if self._broadcasting_object_ref is not None: return self._broadcasting_object_ref() @property def interaction(self): return self._interaction @property def quadtree(self): return self._quadtree @property def cluster_request(self): return self._cluster_request def _on_broadcasting_object_deleted(self, _): current_zone = services.current_zone() if current_zone is not None: broadcaster_service = current_zone.broadcaster_service if broadcaster_service is not None: broadcaster_service.remove_broadcaster(self) def _on_broadcasting_object_moved(self, *_, **__): self.regenerate_constraint() current_zone = services.current_zone() if current_zone is not None: broadcaster_service = current_zone.broadcaster_service if broadcaster_service is not None: broadcaster_service.update_cluster_request(self) def on_processed(self): for affected_object in self._affected_objects: while affected_object not in self._current_objects: self.remove_broadcaster_effect(affected_object) self._current_objects.clear() def on_removed(self): for affected_object in self._affected_objects: self.remove_broadcaster_effect(affected_object) broadcasting_object = self.broadcasting_object if broadcasting_object is not None: broadcasting_object.unregister_on_location_changed( self._on_broadcasting_object_moved) def on_added_to_quadtree_and_cluster_request(self, quadtree, cluster_request): self._quadtree = quadtree self._cluster_request = cluster_request def can_affect(self, obj): if not self.allow_objects and not obj.is_sim: return False broadcasting_object = self.broadcasting_object if broadcasting_object is None: return False routing_surface = broadcasting_object.routing_surface if routing_surface is None or obj.routing_surface != routing_surface: return False if obj is broadcasting_object: return False linked_broadcasters = list(self._linked_broadcasters) if any(obj is linked_broadcaster.broadcasting_object for linked_broadcaster in linked_broadcasters): return False return True def apply_broadcaster_effect(self, affected_object): self._current_objects.add(affected_object) if self._should_apply_broadcaster_effect(affected_object): self._affected_objects[affected_object] = ( services.time_service().sim_now, True) for broadcaster_effect in self.effects: broadcaster_effect.apply_broadcaster_effect( self, affected_object) for linked_broadcaster in self._linked_broadcasters: linked_broadcaster._apply_linked_broadcaster_effect( affected_object, self._affected_objects[affected_object]) def _apply_linked_broadcaster_effect(self, affected_object, data): self._apply_linked_broadcaster_data(affected_object, data) for broadcaster_effect in self.effects: while broadcaster_effect.apply_when_linked: broadcaster_effect.apply_broadcaster_effect( self, affected_object) def _apply_linked_broadcaster_data(self, affected_object, data): if affected_object in self._affected_objects: was_in_area = self._affected_objects[affected_object][1] is_in_area = data[1] if was_in_area and not is_in_area: self.remove_broadcaster_effect(affected_object) self._affected_objects[affected_object] = data def remove_broadcaster_effect(self, affected_object, is_linked=False): if not self._affected_objects[affected_object][1]: return self._affected_objects[affected_object] = ( self._affected_objects[affected_object][0], False) for broadcaster_effect in self.effects: while broadcaster_effect.apply_when_linked or not is_linked: broadcaster_effect.remove_broadcaster_effect( self, affected_object) if not is_linked: for linked_broadcaster in self._linked_broadcasters: linked_broadcaster.remove_broadcaster_effect(affected_object, is_linked=True) def _should_apply_broadcaster_effect(self, affected_object): if self.frequency.frequency_type == self.FREQUENCY_ENTER: if affected_object not in self._affected_objects: return True if self.frequency.allow_multiple and not self._affected_objects[ affected_object][1]: return True return False if self.frequency.frequency_type == self.FREQUENCY_PULSE: last_reaction = self._affected_objects.get(affected_object, None) if last_reaction is None: return True time_since_last_reaction = services.time_service( ).sim_now - last_reaction[0] if time_since_last_reaction.in_minutes( ) > self.frequency.cooldown_time: return True return False def clear_linked_broadcasters(self): self._linked_broadcasters.clear() def set_linked_broadcasters(self, broadcasters): self.clear_linked_broadcasters() self._linked_broadcasters.update(broadcasters) for linked_broadcaster in self._linked_broadcasters: linked_broadcaster.clear_linked_broadcasters() for (obj, data) in self._affected_objects.items(): linked_broadcaster._apply_linked_broadcaster_data(obj, data) @property def has_linked_broadcasters(self): if self._linked_broadcasters: return True return False def get_linked_broadcasters_gen(self): yield self._linked_broadcasters def regenerate_constraint(self, *_, **__): self._constraint = None def get_constraint(self): if self._constraint is None or not self._constraint.valid: self._constraint = Anywhere() for tuned_constraint in self.constraints: self._constraint = self._constraint.intersect( tuned_constraint.create_constraint( None, target=self.broadcasting_object, target_position=self.broadcasting_object.position)) return self._constraint def get_resolver(self, affected_object): return DoubleObjectResolver(affected_object, self.broadcasting_object) def get_clustering(self): broadcasting_object = self.broadcasting_object if broadcasting_object is None: return if broadcasting_object.is_sim: return if broadcasting_object.is_in_inventory(): return if broadcasting_object.routing_surface is None: return return self.clustering def should_cluster(self): return self.get_clustering() is not None def get_affected_object_count(self): return sum(1 for data in self._affected_objects.values() if data[1]) @property def id(self): return self.broadcaster_id @property def lineofsight_component(self): return _BroadcasterLosComponent(self) @property def position(self): return self.broadcasting_object.position @property def routing_surface(self): return self.broadcasting_object.routing_surface @property def parts(self): return self.broadcasting_object.parts
class Broadcaster(HasTunableReference, RouteEventProviderMixin, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager( sims4.resources.Types.BROADCASTER)): class _BroadcasterObjectFilter(HasTunableSingletonFactory, AutoFactoryInit): def is_affecting_objects(self): raise NotImplementedError def can_affect_object(self, obj): raise NotImplementedError class _BroadcasterObjectFilterNone(HasTunableSingletonFactory): def __str__(self): return 'Nothing' def is_affecting_objects(self): return (False, None) def can_affect_object(self, obj): return False class _BroadcasterObjectFilterFlammable(HasTunableSingletonFactory): def __str__(self): return 'Flammable Objects' def is_affecting_objects(self): return (True, {FireTuning.FLAMMABLE_TAG}) def can_affect_object(self, obj): target_object_tags = obj.get_tags() if FireTuning.FLAMMABLE_TAG in target_object_tags: return True return False class _BroadcasterObjectFilterTags(HasTunableSingletonFactory, AutoFactoryInit): def __str__(self): return '{}'.format(', '.join(str(tag) for tag in self.tags)) FACTORY_TUNABLES = { 'tags': TunableSet( description= '\n An object with any tag in this set can be affected by the\n broadcaster.\n ', tunable=TunableEnumEntry( description= '\n A tag.\n ', tunable_type=Tag, default=Tag.INVALID, pack_safe=True)) } def is_affecting_objects(self): return (True, self.tags) def can_affect_object(self, obj): target_object_tags = obj.get_tags() if self.tags & target_object_tags: return True return False FREQUENCY_ENTER = 0 FREQUENCY_PULSE = 1 INSTANCE_TUNABLES = { 'clock_type': TunableEnumEntry( description= "\n Denotes whether broadcasters of this type are managed in real time\n or game time.\n \n Most broadcasters should be managed in Game Time because they will\n update with the speed of the game, including performance dips, and\n speed changes. However, Real Time broadcasters are more performant\n because they will only update based on the frequency of real time.\n You should use real time updates if the broadcaster is enabled for\n the lifetime of the object, there are a lot of that type, or timing\n doesn't matter as much.\n \n One Shot Interaction broadcasters should always be Game Time.\n Environment Score broadcasters should be in Real Time. Consult an\n engineer if you have questions.\n ", tunable_type=BroadcasterClockType, default=BroadcasterClockType.GAME_TIME), 'constraints': TunableList( description= '\n A list of constraints that define the area of influence of this\n broadcaster. It is required that at least one constraint be defined.\n ', tunable=TunableGeometricConstraintVariant( constraint_locked_args={'multi_surface': True}, circle_locked_args={'require_los': False}, disabled_constraints={'spawn_points', 'current_position'}), minlength=1), 'effects': TunableList( description= '\n A list of effects that are applied to Sims and objects affected by\n this broadcaster.\n ', tunable=TunableBroadcasterEffectVariant()), 'route_events': TunableList( description= "\n Specify any route events that are triggered when the Sim follows a\n path that has points within this broadcaster's constraints.\n ", tunable=RouteEvent.TunableReference( description= '\n A Route Event that is to be played when a Sim plans a route\n through this broadcaster.\n ', pack_safe=True)), 'frequency': TunableVariant( description= '\n Define in what instances and how often this broadcaster affects Sims\n and objects in its area of influence.\n ', on_enter=TunableTuple( description= '\n Sims and objects are affected by this broadcaster when they\n enter in its area of influence, or when the broadcaster is\n created.\n ', locked_args={'frequency_type': FREQUENCY_ENTER}, allow_multiple=Tunable( description= "\n If checked, then Sims may react multiple times if they re-\n enter the broadcaster's area of influence. If unchecked,\n then Sims will only react to the broadcaster once.\n ", tunable_type=bool, default=False)), on_pulse=TunableTuple( description= '\n Sims and objects are constantly affected by this broadcaster\n while they are in its area of influence.\n ', locked_args={'frequency_type': FREQUENCY_PULSE}, cooldown_time=TunableSimMinute( description= '\n The time interval between broadcaster pulses. Sims would not\n react to the broadcaster for at least this amount of time\n while in its area of influence.\n ', default=8)), default='on_pulse'), 'clustering': OptionalTunable( description= '\n If set, then similar broadcasters, i.e. broadcasters of the same\n instance, will be clustered together if their broadcasting objects\n are close by. This improves performance and avoids having Sims react\n multiple times to similar broadcasters. When broadcasters are\n clustered together, there is no guarantee as to what object will be\n used for testing purposes.\n \n e.g. Stinky food reactions are clustered together. A test on the\n broadcaster should not, for example, differentiate between a stinky\n lobster and a stinky steak, because the broadcasting object is\n arbitrary and undefined.\n \n e.g. Jealousy reactions are not clustered together. A test on the\n broadcaster considers the relationship between two Sims. Therefore,\n it would be unwise to consider an arbitrary Sim if two jealousy\n broadcasters are close to each other.\n ', tunable=ObjectClusterRequest.TunableFactory( description= '\n Specify how clusters for this particular broadcaster are formed.\n ', locked_args={'minimum_size': 1}), enabled_by_default=True), 'allow_objects': TunableVariant( description= '\n If enabled, then in addition to all instantiated Sims, some objects\n will be affected by this broadcaster. Some tuned effects might still\n only apply to Sims (e.g. affordance pushing).\n \n Setting this tuning field has serious performance repercussions. Its\n indiscriminate use could undermine our ability to meet Minspec\n goals. Please use this sparingly.\n ', disallow=_BroadcasterObjectFilterNone.TunableFactory(), from_tags=_BroadcasterObjectFilterTags.TunableFactory(), from_flammable=_BroadcasterObjectFilterFlammable.TunableFactory(), default='disallow'), 'allow_sims': Tunable( description= '\n If checked then this broadcaster will consider Sims. This is on by\n default. \n \n If neither allow_objects or allow_sims is checked that will result\n in a tuning error.\n ', tunable_type=bool, default=True), 'allow_sim_test': OptionalTunable( description= '\n If enabled, allows for a top level test set to determine which\n sims can be affected by the broadcaster at all.\n ', tunable=TunableTestSet()), 'immediate': Tunable( description= '\n If checked, this broadcaster will trigger a broadcaster update when\n added to the service. This adds a performance cost so please use\n this sparingly. This can be used for one-shot interactions that\n generate broadcasters because the update interval might be excluded\n in the interaction duration.\n ', tunable_type=bool, default=False) } @classmethod def _verify_tuning_callback(cls): (allow_objects, _) = cls.allow_objects.is_affecting_objects() if not cls.allow_sims and not allow_objects: logger.error( 'Broadcaster {} is tuned to not allow any objects as targets.', cls) @classmethod def register_static_callbacks(cls, *args, **kwargs): for broadcaster_effect in cls.effects: broadcaster_effect.register_static_callbacks(*args, **kwargs) @classmethod def get_broadcaster_service(cls): current_zone = services.current_zone() if current_zone is not None: if cls.clock_type == BroadcasterClockType.GAME_TIME: return current_zone.broadcaster_service if cls.clock_type == BroadcasterClockType.REAL_TIME: return current_zone.broadcaster_real_time_service raise NotImplementedError def __init__(self, *args, broadcasting_object, interaction=None, **kwargs): super().__init__(*args, **kwargs) self._broadcasting_object_ref = weakref.ref( broadcasting_object, self._on_broadcasting_object_deleted) self._interaction = interaction self._constraint = None self._affected_objects = weakref.WeakKeyDictionary() self._current_objects = weakref.WeakSet() self._linked_broadcasters = weakref.WeakSet() broadcasting_object.register_on_location_changed( self._on_broadcasting_object_moved) self._quadtree = None self._cluster_request = None @property def broadcasting_object(self): if self._broadcasting_object_ref is not None: return self._broadcasting_object_ref() @property def interaction(self): return self._interaction @property def quadtree(self): return self._quadtree @property def cluster_request(self): return self._cluster_request def _on_broadcasting_object_deleted(self, _): broadcaster_service = self.get_broadcaster_service() if broadcaster_service is not None: broadcaster_service.remove_broadcaster(self) def _on_broadcasting_object_moved(self, *_, **__): self.regenerate_constraint() broadcaster_service = self.get_broadcaster_service() if broadcaster_service is not None: broadcaster_service.update_cluster_request(self) def on_processed(self): for affected_object in self._affected_objects: if affected_object not in self._current_objects: self.remove_broadcaster_effect(affected_object) self._current_objects.clear() def on_removed(self): for affected_object in self._affected_objects: self.remove_broadcaster_effect(affected_object) broadcasting_object = self.broadcasting_object if broadcasting_object is not None: for broadcaster_effect in self.effects: if broadcaster_effect.apply_when_removed: broadcaster_effect.apply_broadcaster_loot(self) broadcasting_object.unregister_on_location_changed( self._on_broadcasting_object_moved) def on_added_to_quadtree_and_cluster_request(self, quadtree, cluster_request): self._quadtree = quadtree self._cluster_request = cluster_request def can_affect(self, obj): if obj.is_sim: if not self.allow_sims: return False elif not self.allow_objects.can_affect_object(obj): return False broadcasting_object = self.broadcasting_object if broadcasting_object is None: return False if obj is broadcasting_object: return False elif any(obj is linked_broadcaster.broadcasting_object for linked_broadcaster in self._linked_broadcasters): return False return True def on_event_executed(self, route_event, sim): super().on_event_executed(route_event, sim) if self.can_affect(sim): self.apply_broadcaster_effect(sim) def is_route_event_valid(self, route_event, time, sim, path): if not self.can_affect(sim): return False constraint = self.get_constraint() if not constraint.valid or constraint.geometry is None: return False else: (transform, routing_surface) = path.get_location_data_at_time(time) if not (constraint.geometry.test_transform(transform) and constraint.is_routing_surface_valid(routing_surface)): return False return True def apply_broadcaster_effect(self, affected_object): if affected_object.is_sim and self.allow_sim_test and not self.allow_sim_test.run_tests( SingleSimResolver(affected_object.sim_info)): return self._current_objects.add(affected_object) if self._should_apply_broadcaster_effect(affected_object): self._affected_objects[affected_object] = ( services.time_service().sim_now, True) for broadcaster_effect in self.effects: if affected_object in self._affected_objects: broadcaster_effect.apply_broadcaster_effect( self, affected_object) for linked_broadcaster in self._linked_broadcasters: if affected_object in self._affected_objects: linked_broadcaster._apply_linked_broadcaster_effect( affected_object, self._affected_objects[affected_object]) def _apply_linked_broadcaster_effect(self, affected_object, data): self._apply_linked_broadcaster_data(affected_object, data) for broadcaster_effect in self.effects: if broadcaster_effect.apply_when_linked: broadcaster_effect.apply_broadcaster_effect( self, affected_object) def _apply_linked_broadcaster_data(self, affected_object, data): if affected_object in self._affected_objects: was_in_area = self._affected_objects[affected_object][1] is_in_area = data[1] if was_in_area and not is_in_area: self.remove_broadcaster_effect(affected_object) self._affected_objects[affected_object] = data def remove_broadcaster_effect(self, affected_object, is_linked=False): affected_object_data = self._affected_objects.get(affected_object) if affected_object_data is None: return if not affected_object_data[1]: return self._affected_objects[affected_object] = (affected_object_data[0], False) self._current_objects.discard(affected_object) for broadcaster_effect in self.effects: if not broadcaster_effect.apply_when_linked: if not is_linked: broadcaster_effect.remove_broadcaster_effect( self, affected_object) broadcaster_effect.remove_broadcaster_effect(self, affected_object) if not is_linked: for linked_broadcaster in self._linked_broadcasters: linked_broadcaster.remove_broadcaster_effect(affected_object, is_linked=True) def _should_apply_broadcaster_effect(self, affected_object): if self.frequency.frequency_type == self.FREQUENCY_ENTER: if affected_object not in self._affected_objects: return True elif self.frequency.allow_multiple and not self._affected_objects[ affected_object][1]: return True return False if self.frequency.frequency_type == self.FREQUENCY_PULSE: last_reaction = self._affected_objects.get(affected_object, None) if last_reaction is None: return True else: time_since_last_reaction = services.time_service( ).sim_now - last_reaction[0] if time_since_last_reaction.in_minutes( ) > self.frequency.cooldown_time: return True else: return False return False def clear_linked_broadcasters(self): self._linked_broadcasters.clear() def set_linked_broadcasters(self, broadcasters): self.clear_linked_broadcasters() self._linked_broadcasters.update(broadcasters) for linked_broadcaster in self._linked_broadcasters: linked_broadcaster.clear_linked_broadcasters() for (obj, data) in linked_broadcaster._affected_objects.items(): if obj not in self._affected_objects: self._affected_objects[obj] = data for linked_broadcaster in self._linked_broadcasters: for (obj, data) in self._affected_objects.items(): linked_broadcaster._apply_linked_broadcaster_data(obj, data) def get_linked_broadcasters_gen(self): yield from self._linked_broadcasters def regenerate_constraint(self, *_, **__): self._constraint = None def get_constraint(self): if not (self._constraint is None or not self._constraint.valid): self._constraint = Anywhere() for tuned_constraint in self.constraints: self._constraint = self._constraint.intersect( tuned_constraint.create_constraint( None, target=self.broadcasting_object, target_position=self.broadcasting_object.position)) return self._constraint def get_resolver(self, affected_object): return DoubleObjectResolver(affected_object, self.broadcasting_object) def get_clustering(self): broadcasting_object = self.broadcasting_object if broadcasting_object is None: return if broadcasting_object.is_sim: return if broadcasting_object.is_in_inventory(): return if broadcasting_object.routing_surface is None: return return self.clustering def should_cluster(self): return self.get_clustering() is not None def get_affected_object_count(self): return sum(1 for data in self._affected_objects.values() if data[1]) @property def id(self): return self.broadcaster_id @property def lineofsight_component(self): return _BroadcasterLosComponent(self) @property def position(self): return self.broadcasting_object.position @property def routing_surface(self): return self.broadcasting_object.routing_surface @property def parts(self): return self.broadcasting_object.parts
class ReactionTriggerElement(XevtTriggeredElement): __qualname__ = 'ReactionTriggerElement' FACTORY_TUNABLES = { 'description': 'At the specified timing, push an affordance on other Sims on the lot.', 'reaction_affordance': TunableReference(services.affordance_manager(), description='The affordance to push on other Sims.'), 'reaction_target': TunableEnumEntry( ParticipantType, ParticipantType.Actor, description= 'The subject of this interaction that will be set as the target of the pushed reaction_affordance.' ), 'reaction_constraints': TunableList( TunableGeometricConstraintVariant(), description= 'The constraints that Sims on the lot have to satisfy such that reaction_affordance is pushed on them.' ), 'trigger_on_late_arrivals': Tunable( bool, False, needs_tuning=True, description= '\n If checked, Sims entering the reaction area after the reaction is first triggered will\n also react, up until when the interaction is canceled.\n ' ) } def __init__(self, interaction, *args, sequence=(), **kwargs): super().__init__(interaction, sequence=sequence, *args, **kwargs) self._reaction_target_sim = self.interaction.get_participant( self.reaction_target) self._reaction_constraint = None self._triggered_sims = _instances = weakref.WeakSet() @classmethod def on_affordance_loaded_callback(cls, affordance, reaction_trigger_element): def sim_can_execute_affordance(interaction, sim): context = InteractionContext(sim, InteractionContext.SOURCE_SCRIPT, Priority.High) return sim.test_super_affordance( reaction_trigger_element.reaction_affordance, interaction.target, context) affordance.register_sim_can_violate_privacy_callback( sim_can_execute_affordance) def _build_outer_elements(self, sequence): if self.trigger_on_late_arrivals: return build_critical_section_with_finally( sequence, self._remove_constraints) return sequence def _do_behavior(self): self._reaction_constraint = Anywhere() for tuned_reaction_constraint in self.reaction_constraints: self._reaction_constraint = self._reaction_constraint.intersect( tuned_reaction_constraint.create_constraint( None, target=self._reaction_target_sim)) if self.trigger_on_late_arrivals: self._reaction_target_sim.reaction_triggers[ self.interaction] = self for sim in services.sim_info_manager().instanced_sims_gen(): self.intersect_and_execute(sim) def intersect_and_execute(self, sim): if sim in self._triggered_sims: return participants = self.interaction.get_participants( ParticipantType.AllSims) if sim not in participants: sim_constraint = interactions.constraints.Transform( sim.transform, routing_surface=sim.routing_surface) context = InteractionContext(sim, InteractionContext.SOURCE_SCRIPT, Priority.High) if sim_constraint.intersect(self._reaction_constraint).valid: result = sim.push_super_affordance(self.reaction_affordance, self._reaction_target_sim, context) if result: self.interaction.add_liability( JOIN_INTERACTION_LIABILITY, JoinInteractionLiability(result.interaction)) self._triggered_sims.add(sim) def _remove_constraints(self, *_, **__): self._reaction_target_sim.reaction_triggers.pop(self.interaction, None)
def get_combined_constraint(self, existing_constraint=None, priority=None, group_id=None, to_exclude=None, include_inertial_sis=False, force_inertial_sis=False, existing_si=None, posture_state=DEFAULT, allow_posture_providers=True, include_existing_constraint=True, participant_type=ParticipantType.Actor): included_sis = set() if include_inertial_sis: if existing_si is not None and any(si.id == existing_si.continuation_id for si in self): sis_must_include = set() else: sis_must_include = self._get_must_include_sis(priority, group_id, existing_si=existing_si) if force_inertial_sis: for si in self._super_interactions: if si in sis_must_include: pass if not allow_posture_providers and self.sim.posture_state.is_source_interaction(si): pass if not self._common_included_si_tests(si): pass sis_must_include.add(si) to_consider = set() for non_guaranteed_si in self._super_interactions: if non_guaranteed_si in sis_must_include: pass if not allow_posture_providers and self.sim.posture_state.is_source_interaction(non_guaranteed_si): pass to_consider.add(non_guaranteed_si) else: sis_must_include = self._get_must_include_sis(priority, group_id, existing_si=existing_si) to_consider = set() if allow_posture_providers: additional_posture_sis = set() for si in sis_must_include: owned_posture = self.sim.posture_state.get_source_or_owned_posture_for_si(si) if owned_posture is None: pass if owned_posture.track != postures.PostureTrack.BODY: pass if owned_posture.source_interaction.is_finishing: pass additional_posture_sis.add(owned_posture.source_interaction) sis_must_include.update(additional_posture_sis) additional_posture_sis.clear() total_constraint = Anywhere() included_carryables = set() for si_must_include in sis_must_include: if si_must_include.is_finishing: pass while not si_must_include is to_exclude: if si_must_include is existing_si: pass if existing_si is not None and existing_si.group_id == si_must_include.group_id: pass my_role = si_must_include.get_participant_type(self.sim) if existing_si is not None: existing_participant_type = existing_si.get_participant_type(self.sim) if not self.are_sis_compatible(si_must_include, existing_si, my_role, existing_participant_type, ignore_geometry=True): return (Nowhere(), sis_must_include) si_constraint = si_must_include.constraint_intersection(participant_type=my_role, posture_state=posture_state) if existing_si is not None: if (existing_si.should_rally or existing_si.relocate_main_group) and (si_must_include.is_social and si_must_include.social_group is not None) and si_must_include.social_group is si_must_include.sim.get_main_group(): si_constraint = si_constraint.generate_posture_only_constraint() si_constraint = si_constraint.apply_posture_state(None, existing_si.get_constraint_resolver(None, participant_type=participant_type)) if existing_constraint is not None: si_constraint = si_constraint.apply(existing_constraint) test_constraint = total_constraint.intersect(si_constraint) if not test_constraint.valid: break carry_target = si_must_include.targeted_carryable if carry_target is not None: if len(included_carryables) == 2 and carry_target not in included_carryables: pass included_carryables.add(carry_target) total_constraint = test_constraint included_sis.add(si_must_include) if len(included_carryables) == 2 and existing_si is not None: existing_carry_target = existing_si.carry_target or existing_si.target if existing_carry_target is not None and existing_carry_target.carryable_component is not None: if existing_carry_target not in included_carryables: total_constraint = Nowhere() if included_sis != sis_must_include: total_constraint = Nowhere() if not total_constraint.valid: return (total_constraint, included_sis) if total_constraint.tentative or existing_constraint is not None and existing_constraint.tentative: return (total_constraint, included_sis) if to_consider: for si in self._sis_sorted(to_consider): if si is to_exclude: pass if existing_si is not None and existing_si.group_id == si.group_id: pass if not self._common_included_si_tests(si): pass my_role = si.get_participant_type(self.sim) if existing_si is not None: existing_participant_type = existing_si.get_participant_type(self.sim) if not self.are_sis_compatible(si, existing_si, my_role, existing_participant_type, ignore_geometry=True): pass si_constraint = si.constraint_intersection(participant_type=my_role, posture_state=posture_state) if existing_si is not None: si_constraint = si_constraint.apply_posture_state(None, existing_si.get_constraint_resolver(None, participant_type=participant_type)) if si_constraint.tentative: si_constraint = si.constraint_intersection(participant_type=my_role, posture_state=DEFAULT) test_constraint = total_constraint.intersect(si_constraint) if existing_constraint is not None: test_constraint_plus_existing = test_constraint.intersect(existing_constraint) while not not test_constraint_plus_existing.valid: if test_constraint_plus_existing.tentative: pass test_constraint = test_constraint.apply(existing_constraint) if test_constraint.valid: total_constraint = test_constraint included_sis.add(si) while total_constraint.tentative: break if test_constraint.valid: total_constraint = test_constraint included_sis.add(si) while total_constraint.tentative: break if allow_posture_providers: additional_posture_sis = set() for si in included_sis: owned_posture = self.sim.posture_state.get_source_or_owned_posture_for_si(si) while owned_posture is not None and owned_posture.source_interaction not in included_sis and owned_posture.track == postures.PostureTrack.BODY: additional_posture_sis.add(owned_posture.source_interaction) included_sis.update(additional_posture_sis) if include_existing_constraint and existing_constraint is not None: total_constraint = total_constraint.intersect(existing_constraint) return (total_constraint, included_sis)
class Broadcaster(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(sims4.resources.Types.BROADCASTER)): __qualname__ = 'Broadcaster' FREQUENCY_ENTER = 0 FREQUENCY_PULSE = 1 INSTANCE_TUNABLES = {'constraints': TunableList(description='\n A list of constraints that define the area of influence of this\n broadcaster. It is required that at least one constraint be defined.\n ', tunable=TunableGeometricConstraintVariant()), 'effects': TunableList(description='\n A list of effects that are applied to Sims and objects affected by\n this broadcaster.\n ', tunable=TunableBroadcasterEffectVariant()), 'frequency': TunableVariant(description='\n Define in what instances and how often this broadcaster affects Sims\n and objects in its area of influence.\n ', on_enter=TunableTuple(description='\n Sims and objects are affected by this broadcaster when they\n enter in its area of influence, or when the broadcaster is\n created.\n ', locked_args={'frequency_type': FREQUENCY_ENTER}, allow_multiple=Tunable(description="\n If checked, then Sims may react multiple times if they re-\n enter the broadcaster's area of influence. If unchecked,\n then Sims will only react to the broadcaster once.\n ", tunable_type=bool, needs_tuning=True, default=False)), on_pulse=TunableTuple(description='\n Sims and objects are constantly affected by this broadcaster\n while they are in its area of influence.\n ', locked_args={'frequency_type': FREQUENCY_PULSE}, cooldown_time=TunableSimMinute(description='\n The time interval between broadcaster pulses. Sims would not\n react to the broadcaster for at least this amount of time\n while in its area of influence.\n ', default=8)), default='on_pulse'), 'clustering': OptionalTunable(description='\n If set, then similar broadcasters, i.e. broadcasters of the same\n instance, will be clustered together if their broadcasting objects\n are close by. This improves performance and avoids having Sims react\n multiple times to similar broadcasters. When broadcasters are\n clustered together, there is no guarantee as to what object will be\n used for testing purposes.\n \n e.g. Stinky food reactions are clustered together. A test on the\n broadcaster should not, for example, differentiate between a stinky\n lobster and a stinky steak, because the broadcasting object is\n arbitrary and undefined.\n \n e.g. Jealousy reactions are not clustered together. A test on the\n broadcaster considers the relationship between two Sims. Therefore,\n it would be unwise to consider an arbitrary Sim if two jealousy\n broadcasters are close to each other.\n ', tunable=ObjectClusterRequest.TunableFactory(description='\n Specify how clusters for this particular broadcaster are formed.\n ', locked_args={'minimum_size': 1}), enabled_by_default=True), 'allow_objects': Tunable(description='\n If checked, then in addition to all instantiated Sims, all objects\n will be affected by this broadcaster. Some tuned effects might still\n only apply to Sims (e.g. affordance pushing).\n \n Checking this tuning field has performance repercussions, as it\n means we have to process a lot more data during broadcaster pings.\n Please use this sporadically.\n ', tunable_type=bool, default=False)} @classmethod def _verify_tuning_callback(cls): if not cls.constraints: logger.error('Broadcaster {} does not define any constraints.', cls) @classmethod def register_static_callbacks(cls, *args, **kwargs): for broadcaster_effect in cls.effects: broadcaster_effect.register_static_callbacks(*args, **kwargs) def __init__(self, *args, broadcasting_object, interaction=None, **kwargs): super().__init__(*args, **kwargs) self._broadcasting_object_ref = weakref.ref(broadcasting_object, self._on_broadcasting_object_deleted) self._interaction = interaction self._constraint = None self._affected_objects = weakref.WeakKeyDictionary() self._current_objects = weakref.WeakSet() self._linked_broadcasters = weakref.WeakSet() broadcasting_object.register_on_location_changed(self._on_broadcasting_object_moved) self._quadtree = None self._cluster_request = None @property def broadcasting_object(self): if self._broadcasting_object_ref is not None: return self._broadcasting_object_ref() @property def interaction(self): return self._interaction @property def quadtree(self): return self._quadtree @property def cluster_request(self): return self._cluster_request def _on_broadcasting_object_deleted(self, _): current_zone = services.current_zone() if current_zone is not None: broadcaster_service = current_zone.broadcaster_service if broadcaster_service is not None: broadcaster_service.remove_broadcaster(self) def _on_broadcasting_object_moved(self, *_, **__): self.regenerate_constraint() current_zone = services.current_zone() if current_zone is not None: broadcaster_service = current_zone.broadcaster_service if broadcaster_service is not None: broadcaster_service.update_cluster_request(self) def on_processed(self): for affected_object in self._affected_objects: while affected_object not in self._current_objects: self.remove_broadcaster_effect(affected_object) self._current_objects.clear() def on_removed(self): for affected_object in self._affected_objects: self.remove_broadcaster_effect(affected_object) broadcasting_object = self.broadcasting_object if broadcasting_object is not None: broadcasting_object.unregister_on_location_changed(self._on_broadcasting_object_moved) def on_added_to_quadtree_and_cluster_request(self, quadtree, cluster_request): self._quadtree = quadtree self._cluster_request = cluster_request def can_affect(self, obj): if not self.allow_objects and not obj.is_sim: return False broadcasting_object = self.broadcasting_object if broadcasting_object is None: return False routing_surface = broadcasting_object.routing_surface if routing_surface is None or obj.routing_surface != routing_surface: return False if obj is broadcasting_object: return False linked_broadcasters = list(self._linked_broadcasters) if any(obj is linked_broadcaster.broadcasting_object for linked_broadcaster in linked_broadcasters): return False return True def apply_broadcaster_effect(self, affected_object): self._current_objects.add(affected_object) if self._should_apply_broadcaster_effect(affected_object): self._affected_objects[affected_object] = (services.time_service().sim_now, True) for broadcaster_effect in self.effects: broadcaster_effect.apply_broadcaster_effect(self, affected_object) for linked_broadcaster in self._linked_broadcasters: linked_broadcaster._apply_linked_broadcaster_effect(affected_object, self._affected_objects[affected_object]) def _apply_linked_broadcaster_effect(self, affected_object, data): self._apply_linked_broadcaster_data(affected_object, data) for broadcaster_effect in self.effects: while broadcaster_effect.apply_when_linked: broadcaster_effect.apply_broadcaster_effect(self, affected_object) def _apply_linked_broadcaster_data(self, affected_object, data): if affected_object in self._affected_objects: was_in_area = self._affected_objects[affected_object][1] is_in_area = data[1] if was_in_area and not is_in_area: self.remove_broadcaster_effect(affected_object) self._affected_objects[affected_object] = data def remove_broadcaster_effect(self, affected_object, is_linked=False): if not self._affected_objects[affected_object][1]: return self._affected_objects[affected_object] = (self._affected_objects[affected_object][0], False) for broadcaster_effect in self.effects: while broadcaster_effect.apply_when_linked or not is_linked: broadcaster_effect.remove_broadcaster_effect(self, affected_object) if not is_linked: for linked_broadcaster in self._linked_broadcasters: linked_broadcaster.remove_broadcaster_effect(affected_object, is_linked=True) def _should_apply_broadcaster_effect(self, affected_object): if self.frequency.frequency_type == self.FREQUENCY_ENTER: if affected_object not in self._affected_objects: return True if self.frequency.allow_multiple and not self._affected_objects[affected_object][1]: return True return False if self.frequency.frequency_type == self.FREQUENCY_PULSE: last_reaction = self._affected_objects.get(affected_object, None) if last_reaction is None: return True time_since_last_reaction = services.time_service().sim_now - last_reaction[0] if time_since_last_reaction.in_minutes() > self.frequency.cooldown_time: return True return False def clear_linked_broadcasters(self): self._linked_broadcasters.clear() def set_linked_broadcasters(self, broadcasters): self.clear_linked_broadcasters() self._linked_broadcasters.update(broadcasters) for linked_broadcaster in self._linked_broadcasters: linked_broadcaster.clear_linked_broadcasters() for (obj, data) in self._affected_objects.items(): linked_broadcaster._apply_linked_broadcaster_data(obj, data) @property def has_linked_broadcasters(self): if self._linked_broadcasters: return True return False def get_linked_broadcasters_gen(self): yield self._linked_broadcasters def regenerate_constraint(self, *_, **__): self._constraint = None def get_constraint(self): if self._constraint is None or not self._constraint.valid: self._constraint = Anywhere() for tuned_constraint in self.constraints: self._constraint = self._constraint.intersect(tuned_constraint.create_constraint(None, target=self.broadcasting_object, target_position=self.broadcasting_object.position)) return self._constraint def get_resolver(self, affected_object): return DoubleObjectResolver(affected_object, self.broadcasting_object) def get_clustering(self): broadcasting_object = self.broadcasting_object if broadcasting_object is None: return if broadcasting_object.is_sim: return if broadcasting_object.is_in_inventory(): return if broadcasting_object.routing_surface is None: return return self.clustering def should_cluster(self): return self.get_clustering() is not None def get_affected_object_count(self): return sum(1 for data in self._affected_objects.values() if data[1]) @property def id(self): return self.broadcaster_id @property def lineofsight_component(self): return _BroadcasterLosComponent(self) @property def position(self): return self.broadcasting_object.position @property def routing_surface(self): return self.broadcasting_object.routing_surface @property def parts(self): return self.broadcasting_object.parts