class LightingLiability(Liability, HasTunableFactory, AutoFactoryInit): LIABILITY_TOKEN = 'LightingLiability' FACTORY_TUNABLES = {'radius_squared': TunableDistanceSquared(description='\n The distance away from the specified participant that lights will\n be turned off.\n ', default=1, display_name='Radius'), 'participant': TunableEnumEntry(description='\n The participant of the interaction that we will be used as the\n center of the radius to turn lights off.\n ', tunable_type=ParticipantType, default=ParticipantType.Actor)} def __init__(self, interaction, **kwargs): super().__init__(**kwargs) self._interaction = interaction self._lights = WeakSet() self._automated_lights = WeakSet() def on_run(self): if self._lights: return participant = self._interaction.get_participant(self.participant) position = participant.position for obj in services.object_manager().get_all_objects_with_component_gen(objects.components.types.LIGHTING_COMPONENT): if get_object_has_tag(obj.definition.id, LightingComponent.MANUAL_LIGHT_TAG): continue distance_from_pos = obj.position - position if distance_from_pos.magnitude_squared() > self.radius_squared: continue if obj.get_light_dimmer_value() == LightingComponent.LIGHT_AUTOMATION_DIMMER_VALUE: self._automated_lights.add(obj) else: self._lights.add(obj) obj.set_light_dimmer_value(LightingComponent.LIGHT_DIMMER_VALUE_OFF) def release(self): for obj in self._lights: obj.set_light_dimmer_value(LightingComponent.LIGHT_DIMMER_VALUE_MAX_INTENSITY) self._lights.clear() for obj in self._automated_lights: obj.set_light_dimmer_value(LightingComponent.LIGHT_AUTOMATION_DIMMER_VALUE) self._automated_lights.clear()
class Subject: def __init__(self, parent): self.parent = parent self._observers_lock = RLock() self._observers = WeakSet() def addObserver(self, observer): with self._observers_lock: self._observers.add(observer) logger.debug("%s is being observed by %s", stringFor(self.parent), stringFor(observer)) def removeObserver(self, observer): with self._observers_lock: try: self._observers.remove(observer) except KeyError: logger.error("Tried to remove observer %s twice from %s", stringFor(observer), stringFor(self.parent)) def clearObservers(self): with self._observers_lock: self._observers.clear() logger.debug("%s observers were cleaned.", stringFor(self.parent)) def notify(self, event, *args): with self._observers_lock: observers = list(self._observers) for obs in observers: logger.debug("%s is about to notify %s to %s", stringFor(self.parent), event, stringFor(obs)) obs.onNotify(self.parent, event, args)
class Subject(object): def __init__(self, parent, loggingLevel=logging.INFO): super(Subject, self).__init__() self._logger = logging.getLogger("[OBSERVER {} ({})]".format( parent.__class__.__name__.upper(), id(parent))) self._logger.setLevel(loggingLevel) self.parent = parent self._observers_lock = RLock() self._observers = WeakSet() def addObserver(self, observer): with self._observers_lock: self._observers.add(observer) self._logger.debug("%s is being observed by %s", stringFor(self.parent), stringFor(observer)) def removeObserver(self, observer): with self._observers_lock: try: self._observers.remove(observer) except KeyError: self._logger.error("Tried to remove observer %s twice from %s", stringFor(observer), stringFor(self.parent)) def hasObserver(self, observer): with self._observers_lock: return observer in self._observers def clearObservers(self): with self._observers_lock: self._observers.clear() self._logger.debug("%s observers were cleaned.", stringFor(self.parent)) def notify(self, event, *args): with self._observers_lock: observers = list(self._observers) for obs in observers: self._logger.debug("%s is about to notify %s to %s", stringFor(self.parent), event, stringFor(obs)) try: obs.onNotify(self.parent, event, args) except Exception as e: self._logger.error( "Catched exception trying to notify %s to %s with arguments: %s", str(event), str(obs), str(args)) self._logger.exception(e)
class Privacy(LineOfSight): __qualname__ = 'Privacy' _PRIVACY_FOOTPRINT_TYPE = 5 _PRIVACY_DISCOURAGEMENT_COST = routing.get_default_discouragement_cost() _SHOO_CONSTRAINT_RADIUS = Tunable( description= '\n The radius of the constraint a Shooed Sim will attempt to route to.\n ', tunable_type=float, default=2.5) _UNAVAILABLE_TOOLTIP = TunableLocalizedStringFactory( description= '\n Tooltip displayed when an object is not accessible due to being inside\n a privacy region.\n ' ) _EMBARRASSED_AFFORDANCE = TunableReference( description= '\n The affordance a Sim will play when getting embarrassed by walking in\n on a privacy situation.\n ', manager=services.affordance_manager()) def __init__(self, interaction, tests, max_line_of_sight_radius, map_divisions, simplification_ratio, boundary_epsilon, facing_offset): super().__init__(max_line_of_sight_radius, map_divisions, simplification_ratio, boundary_epsilon) self._max_line_of_sight_radius = max_line_of_sight_radius self._interaction = interaction self._tests = tests self._privacy_constraints = [] self._allowed_sims = WeakSet() self._disallowed_sims = WeakSet() self._violators = WeakSet() self._late_violators = WeakSet() self.is_active = False self.has_shooed = False self.central_object = None self._pushed_interactions = [] services.privacy_service().add_instance(self) @property def unavailable_tooltip(self): return self._UNAVAILABLE_TOOLTIP @property def interaction(self): return self._interaction @property def is_active(self) -> bool: return self._is_active @is_active.setter def is_active(self, value): self._is_active = value def _is_sim_allowed(self, sim): if self._tests: resolver = self._interaction.get_resolver(target=sim) if self._tests and self._tests.run_tests(resolver): return True if self._interaction.can_sim_violate_privacy(sim): return True return False def evaluate_sim(self, sim): if self._is_sim_allowed(sim): self._allowed_sims.add(sim) return True self._disallowed_sims.add(sim) return False def build_privacy(self, target=None): self.is_active = True target_object = self._interaction.get_participant( ParticipantType.Object) target_object = None if target_object.is_sim else target_object self.central_object = target_object or (target or self._interaction.sim) self.generate(self.central_object.position, self.central_object.routing_surface) for poly in self.constraint.geometry.polygon: self._privacy_constraints.append( PolygonFootprint( poly, routing_surface=self._interaction.sim.routing_surface, cost=self._PRIVACY_DISCOURAGEMENT_COST, footprint_type=self._PRIVACY_FOOTPRINT_TYPE, enabled=True)) self._allowed_sims.update( self._interaction.get_participants(ParticipantType.AllSims)) for sim in services.sim_info_manager().instanced_sims_gen(): while sim not in self._allowed_sims: self.evaluate_sim(sim) violating_sims = self.find_violating_sims() self._cancel_unavailable_interactions(violating_sims) self._add_overrides_and_constraints_if_needed(violating_sims) def cleanup_privacy_instance(self): if self.is_active: self.is_active = False for sim in self._allowed_sims: self.remove_override_for_sim(sim) for sim in self._late_violators: self.remove_override_for_sim(sim) del self._privacy_constraints[:] self._allowed_sims.clear() self._disallowed_sims.clear() self._violators.clear() self._late_violators.clear() self._cancel_pushed_interactions() def remove_privacy(self): self.cleanup_privacy_instance() services.privacy_service().remove_instance(self) def intersects_with_object(self, obj): if obj.routing_surface != self.central_object.routing_surface: return False delta = obj.position - self.central_object.position distance = delta.magnitude_2d_squared() if distance > self.max_line_of_sight_radius * self.max_line_of_sight_radius: return False object_footprint = obj.footprint_polygon if object_footprint is None: object_footprint = sims4.geometry.Polygon([obj.position]) for poly in self.constraint.geometry.polygon: intersection = poly.intersect(object_footprint) while intersection is not None and intersection.has_enough_vertices: return True return False def find_violating_sims(self): if not self.is_active: return [] nearby_sims = placement.get_nearby_sims( self.central_object.position, self.central_object.routing_surface.secondary_id, radius=self.max_line_of_sight_radius, exclude=self._allowed_sims, only_sim_position=True) violators = [] for sim in nearby_sims: if any(sim_primitive.is_traversing_portal() for sim_primitive in sim.primitives if isinstance(sim_primitive, FollowPath)): pass if sim not in self._disallowed_sims and self.evaluate_sim(sim): pass while sims4.geometry.test_point_in_compound_polygon( sim.position, self.constraint.geometry.polygon): violators.append(sim) return violators def _add_overrides_and_constraints_if_needed(self, violating_sims): for sim in self._allowed_sims: self.add_override_for_sim(sim) for sim in violating_sims: self._violators.add(sim) liabilities = ((SHOO_LIABILITY, ShooLiability(self, sim)), ) result = self._route_sim_away(sim, liabilities=liabilities) while result: self._pushed_interactions.append(result.interaction) def _cancel_unavailable_interactions(self, violating_sims): for sim in violating_sims: interactions_to_cancel = set() if sim.queue.running is not None: interactions_to_cancel.add(sim.queue.running) for interaction in sim.si_state: while interaction.is_super and interaction.target is not None and sim.locked_from_obj_by_privacy( interaction.target): interactions_to_cancel.add(interaction) for interaction in sim.queue: if interaction.target is not None and sim.locked_from_obj_by_privacy( interaction.target): interactions_to_cancel.add(interaction) else: while interaction.target is not None: break for interaction in interactions_to_cancel: interaction.cancel( FinishingType.INTERACTION_INCOMPATIBILITY, cancel_reason_msg= 'Canceled due to incompatibility with privacy instance.') def _route_sim_away(self, sim, liabilities=()): context = InteractionContext(sim, InteractionContext.SOURCE_SCRIPT, Priority.High, insert_strategy=QueueInsertStrategy.NEXT) from interactions.utils.satisfy_constraint_interaction import BuildAndForceSatisfyShooConstraintInteraction result = sim.push_super_affordance( BuildAndForceSatisfyShooConstraintInteraction, None, context, liabilities=liabilities, privacy_inst=self, name_override='BuildShooFromPrivacy') if not result: logger.debug( 'Failed to push BuildAndForceSatisfyShooConstraintInteraction on Sim {} to route them out of a privacy area. Result: {}', sim, result, owner='tastle') self.interaction.cancel( FinishingType.TRANSITION_FAILURE, cancel_reason_msg='Failed to shoo Sims away.') return result def _cancel_pushed_interactions(self): for interaction in self._pushed_interactions: interaction.cancel( FinishingType.AUTO_EXIT, cancel_reason_msg='Privacy finished and is cleaning up.') self._pushed_interactions.clear() def handle_late_violator(self, sim): self._cancel_unavailable_interactions((sim, )) self.add_override_for_sim(sim) liabilities = ((LATE_SHOO_LIABILITY, LateShooLiability(self, sim)), ) result = self._route_sim_away(sim, liabilities=liabilities) if not result: return if not self._violators: context = InteractionContext( sim, InteractionContext.SOURCE_SCRIPT, Priority.High, insert_strategy=QueueInsertStrategy.NEXT) result = sim.push_super_affordance( self._EMBARRASSED_AFFORDANCE, self.interaction.get_participant(ParticipantType.Actor), context) if not result: logger.error( 'Failed to push the embarrassed affordance on Sim {}. Interaction {}. Result {}. Context {} ', sim, self.interaction, result, context, owner='tastle') return self._late_violators.add(sim) def add_override_for_sim(self, sim): for footprint in self._privacy_constraints: sim.routing_context.ignore_footprint_contour( footprint.footprint_id) def remove_override_for_sim(self, sim): for footprint in self._privacy_constraints: sim.routing_context.remove_footprint_contour_override( footprint.footprint_id) @property def allowed_sims(self): return self._allowed_sims @property def disallowed_sims(self): return self._disallowed_sims @property def violators(self): return self._violators def remove_violator(self, sim): self.remove_override_for_sim(sim) self._violators.discard(sim) @property def late_violators(self): return self._late_violators def remove_late_violator(self, sim): self.remove_override_for_sim(sim) self._late_violators.discard(sim)
class Privacy(LineOfSight): __qualname__ = 'Privacy' _PRIVACY_FOOTPRINT_TYPE = 5 _PRIVACY_DISCOURAGEMENT_COST = routing.get_default_discouragement_cost() _SHOO_CONSTRAINT_RADIUS = Tunable(description='\n The radius of the constraint a Shooed Sim will attempt to route to.\n ', tunable_type=float, default=2.5) _UNAVAILABLE_TOOLTIP = TunableLocalizedStringFactory(description='\n Tooltip displayed when an object is not accessible due to being inside\n a privacy region.\n ') _EMBARRASSED_AFFORDANCE = TunableReference(description='\n The affordance a Sim will play when getting embarrassed by walking in\n on a privacy situation.\n ', manager=services.affordance_manager()) def __init__(self, interaction, tests, max_line_of_sight_radius, map_divisions, simplification_ratio, boundary_epsilon, facing_offset): super().__init__(max_line_of_sight_radius, map_divisions, simplification_ratio, boundary_epsilon) self._max_line_of_sight_radius = max_line_of_sight_radius self._interaction = interaction self._tests = tests self._privacy_constraints = [] self._allowed_sims = WeakSet() self._disallowed_sims = WeakSet() self._violators = WeakSet() self._late_violators = WeakSet() self.is_active = False self.has_shooed = False self.central_object = None self._pushed_interactions = [] services.privacy_service().add_instance(self) @property def unavailable_tooltip(self): return self._UNAVAILABLE_TOOLTIP @property def interaction(self): return self._interaction @property def is_active(self) -> bool: return self._is_active @is_active.setter def is_active(self, value): self._is_active = value def _is_sim_allowed(self, sim): if self._tests: resolver = self._interaction.get_resolver(target=sim) if self._tests and self._tests.run_tests(resolver): return True if self._interaction.can_sim_violate_privacy(sim): return True return False def evaluate_sim(self, sim): if self._is_sim_allowed(sim): self._allowed_sims.add(sim) return True self._disallowed_sims.add(sim) return False def build_privacy(self, target=None): self.is_active = True target_object = self._interaction.get_participant(ParticipantType.Object) target_object = None if target_object.is_sim else target_object self.central_object = target_object or (target or self._interaction.sim) self.generate(self.central_object.position, self.central_object.routing_surface) for poly in self.constraint.geometry.polygon: self._privacy_constraints.append(PolygonFootprint(poly, routing_surface=self._interaction.sim.routing_surface, cost=self._PRIVACY_DISCOURAGEMENT_COST, footprint_type=self._PRIVACY_FOOTPRINT_TYPE, enabled=True)) self._allowed_sims.update(self._interaction.get_participants(ParticipantType.AllSims)) for sim in services.sim_info_manager().instanced_sims_gen(): while sim not in self._allowed_sims: self.evaluate_sim(sim) violating_sims = self.find_violating_sims() self._cancel_unavailable_interactions(violating_sims) self._add_overrides_and_constraints_if_needed(violating_sims) def cleanup_privacy_instance(self): if self.is_active: self.is_active = False for sim in self._allowed_sims: self.remove_override_for_sim(sim) for sim in self._late_violators: self.remove_override_for_sim(sim) del self._privacy_constraints[:] self._allowed_sims.clear() self._disallowed_sims.clear() self._violators.clear() self._late_violators.clear() self._cancel_pushed_interactions() def remove_privacy(self): self.cleanup_privacy_instance() services.privacy_service().remove_instance(self) def intersects_with_object(self, obj): if obj.routing_surface != self.central_object.routing_surface: return False delta = obj.position - self.central_object.position distance = delta.magnitude_2d_squared() if distance > self.max_line_of_sight_radius*self.max_line_of_sight_radius: return False object_footprint = obj.footprint_polygon if object_footprint is None: object_footprint = sims4.geometry.Polygon([obj.position]) for poly in self.constraint.geometry.polygon: intersection = poly.intersect(object_footprint) while intersection is not None and intersection.has_enough_vertices: return True return False def find_violating_sims(self): if not self.is_active: return [] nearby_sims = placement.get_nearby_sims(self.central_object.position, self.central_object.routing_surface.secondary_id, radius=self.max_line_of_sight_radius, exclude=self._allowed_sims, only_sim_position=True) violators = [] for sim in nearby_sims: if any(sim_primitive.is_traversing_portal() for sim_primitive in sim.primitives if isinstance(sim_primitive, FollowPath)): pass if sim not in self._disallowed_sims and self.evaluate_sim(sim): pass while sims4.geometry.test_point_in_compound_polygon(sim.position, self.constraint.geometry.polygon): violators.append(sim) return violators def _add_overrides_and_constraints_if_needed(self, violating_sims): for sim in self._allowed_sims: self.add_override_for_sim(sim) for sim in violating_sims: self._violators.add(sim) liabilities = ((SHOO_LIABILITY, ShooLiability(self, sim)),) result = self._route_sim_away(sim, liabilities=liabilities) while result: self._pushed_interactions.append(result.interaction) def _cancel_unavailable_interactions(self, violating_sims): for sim in violating_sims: interactions_to_cancel = set() if sim.queue.running is not None: interactions_to_cancel.add(sim.queue.running) for interaction in sim.si_state: while interaction.is_super and interaction.target is not None and sim.locked_from_obj_by_privacy(interaction.target): interactions_to_cancel.add(interaction) for interaction in sim.queue: if interaction.target is not None and sim.locked_from_obj_by_privacy(interaction.target): interactions_to_cancel.add(interaction) else: while interaction.target is not None: break for interaction in interactions_to_cancel: interaction.cancel(FinishingType.INTERACTION_INCOMPATIBILITY, cancel_reason_msg='Canceled due to incompatibility with privacy instance.') def _route_sim_away(self, sim, liabilities=()): context = InteractionContext(sim, InteractionContext.SOURCE_SCRIPT, Priority.High, insert_strategy=QueueInsertStrategy.NEXT) from interactions.utils.satisfy_constraint_interaction import BuildAndForceSatisfyShooConstraintInteraction result = sim.push_super_affordance(BuildAndForceSatisfyShooConstraintInteraction, None, context, liabilities=liabilities, privacy_inst=self, name_override='BuildShooFromPrivacy') if not result: logger.debug('Failed to push BuildAndForceSatisfyShooConstraintInteraction on Sim {} to route them out of a privacy area. Result: {}', sim, result, owner='tastle') self.interaction.cancel(FinishingType.TRANSITION_FAILURE, cancel_reason_msg='Failed to shoo Sims away.') return result def _cancel_pushed_interactions(self): for interaction in self._pushed_interactions: interaction.cancel(FinishingType.AUTO_EXIT, cancel_reason_msg='Privacy finished and is cleaning up.') self._pushed_interactions.clear() def handle_late_violator(self, sim): self._cancel_unavailable_interactions((sim,)) self.add_override_for_sim(sim) liabilities = ((LATE_SHOO_LIABILITY, LateShooLiability(self, sim)),) result = self._route_sim_away(sim, liabilities=liabilities) if not result: return if not self._violators: context = InteractionContext(sim, InteractionContext.SOURCE_SCRIPT, Priority.High, insert_strategy=QueueInsertStrategy.NEXT) result = sim.push_super_affordance(self._EMBARRASSED_AFFORDANCE, self.interaction.get_participant(ParticipantType.Actor), context) if not result: logger.error('Failed to push the embarrassed affordance on Sim {}. Interaction {}. Result {}. Context {} ', sim, self.interaction, result, context, owner='tastle') return self._late_violators.add(sim) def add_override_for_sim(self, sim): for footprint in self._privacy_constraints: sim.routing_context.ignore_footprint_contour(footprint.footprint_id) def remove_override_for_sim(self, sim): for footprint in self._privacy_constraints: sim.routing_context.remove_footprint_contour_override(footprint.footprint_id) @property def allowed_sims(self): return self._allowed_sims @property def disallowed_sims(self): return self._disallowed_sims @property def violators(self): return self._violators def remove_violator(self, sim): self.remove_override_for_sim(sim) self._violators.discard(sim) @property def late_violators(self): return self._late_violators def remove_late_violator(self, sim): self.remove_override_for_sim(sim) self._late_violators.discard(sim)
class StoryProgressionActionCareer(_StoryProgressionFilterAction): FACTORY_TUNABLES = { 'employment_rate': TunableInterval( description= '\n The ideal employment rates. If the rate of employed Sims fall\n outside this interval, Sims will be hired/fired as necessary.\n ', tunable_type=float, default_lower=0.6, default_upper=0.9, minimum=0, maximum=1) } def __init__(self, **kwargs): super().__init__(**kwargs) self._employed = WeakSet() self._unemployed = WeakSet() self._workforce_count = 0 def _allow_instanced_sims(self): return True def _is_valid_candidate(self, sim_info): if not sim_info.is_npc: return False if sim_info.lod == SimInfoLODLevel.MINIMUM: return False if sim_info.is_instanced(allow_hidden_flags=ALL_HIDDEN_REASONS): return False elif sim_info.career_tracker.currently_during_work_hours: return False return True def _apply_action(self, sim_info): if sim_info.career_tracker.has_quittable_career(): self._employed.add(sim_info) elif not sim_info.career_tracker.has_work_career(): self._unemployed.add(sim_info) self._workforce_count += 1 def _post_apply_action(self): lower_bound = math.floor(self._workforce_count * self.employment_rate.lower_bound) upper_bound = math.ceil(self._workforce_count * self.employment_rate.upper_bound) num_employed = len(self._employed) if num_employed < lower_bound: self._try_employ_sim() elif num_employed > upper_bound: self._try_unemploy_sim() self._employed.clear() self._unemployed.clear() self._workforce_count = 0 def _get_ideal_candidate_for_employment(self): def _get_weight(candidate, career): if not career.is_valid_career(sim_info=candidate): return 0 if candidate.career_tracker.has_career_by_uid(career.guid64): return 0 return career.career_story_progression.joining.get_multiplier( SingleSimResolver(candidate)) career_service = services.get_career_service() weights = [ (_get_weight(candidate, career), candidate, career) for (candidate, career) in itertools.product(( candidate for candidate in self._unemployed if self._is_valid_candidate(candidate)), ( career for career in career_service.get_career_list() if career.career_story_progression.joining is not None)) ] if not weights: return selected_candidate_index = weighted_random_index(weights) if selected_candidate_index is None: return selected_candidate = weights[selected_candidate_index] return (selected_candidate[1], selected_candidate[2]) def _get_ideal_candidate_for_unemployment(self, get_unemployment_multiplier): def _get_weight(candidate, career): subaction_multiplier = get_unemployment_multiplier(career) return subaction_multiplier.get_multiplier( SingleSimResolver(candidate)) weights = list( itertools.chain.from_iterable( ((_get_weight(candidate, career), candidate, career) for career in candidate.career_tracker if career.can_quit if get_unemployment_multiplier(career) is not None) for candidate in self._employed if self._is_valid_candidate(candidate))) if not weights: return selected_candidate_index = weighted_random_index(weights) if selected_candidate_index is None: return selected_candidate = weights[selected_candidate_index] return (selected_candidate[1], selected_candidate[2]) def _try_employ_sim(self): selected_candidate = self._get_ideal_candidate_for_employment() if selected_candidate is None: return False (sim_info, career_type) = selected_candidate max_user_level = career_type.get_max_user_level() user_level = random.randint(1, max_user_level) if gsi_handlers.story_progression_handlers.story_progression_archiver.enabled: gsi_handlers.story_progression_handlers.archive_story_progression( self, 'Add Career to {}: {} ({}/{})', sim_info, career_type, user_level, max_user_level) sim_info.career_tracker.add_career(career_type(sim_info), user_level_override=user_level, give_skipped_rewards=False) return True def _try_retire_sim(self): selected_candidate = self._get_ideal_candidate_for_unemployment( lambda career: career.career_story_progression.retiring) if selected_candidate is None: return False (sim_info, career_type) = selected_candidate if gsi_handlers.story_progression_handlers.story_progression_archiver.enabled: gsi_handlers.story_progression_handlers.archive_story_progression( self, 'Retiring {} from {}', sim_info, career_type) sim_info.career_tracker.retire_career(career_type.guid64) return True def _try_quit_sim(self): selected_candidate = self._get_ideal_candidate_for_unemployment( lambda career: career.career_story_progression.quitting) if selected_candidate is None: return False (sim_info, career_type) = selected_candidate if gsi_handlers.story_progression_handlers.story_progression_archiver.enabled: gsi_handlers.story_progression_handlers.archive_story_progression( self, 'Having {} quit from {}', sim_info, career_type) sim_info.career_tracker.quit_quittable_careers() return True def _try_unemploy_sim(self): if self._try_retire_sim(): return True elif self._try_quit_sim(): return True return False
class LinkedObjectComponent( AutoFactoryInit, objects.components.Component, HasTunableFactory, allow_dynamic=True, component_name=objects.components.types.LINKED_OBJECT_COMPONENT): FACTORY_TUNABLES = { '_parent_state_value': OptionalTunable( description= "\n When enabled, this state will be applied to the parent when\n it has children.\n \n For example, the default link state for the console is unlinked.\n If you set this to the linked state, then when it becomes the\n parent to a T.V. it'll change the console to the linked state.\n When the T.V. is unlinked, the console will revert back to \n the unlinked state.\n ", tunable=TunablePackSafeReference( description= '\n state value to apply to parent objects.\n Behaves as disabled if state not in installed data.\n ', manager=services.get_instance_manager( sims4.resources.Types.OBJECT_STATE), class_restrictions=('ObjectStateValue', ))), '_child_state_value': OptionalTunable( description= "\n When enabled, this state will be applied to the children.\n\n For example, the default link state for a T.V is unlinked.\n If you set this to the linked state, then when it becomes the\n child of a console. it'll change the T.V. to the linked state.\n When the T.V. is unlinked, the T.V. will revert back to \n the unlinked state.\n ", tunable=TunablePackSafeReference( description= '\n state value to apply to child objects.\n Behaves as disabled if state not in installed data.\n ', manager=services.get_instance_manager( sims4.resources.Types.OBJECT_STATE), class_restrictions=('ObjectStateValue', ))), '_child_tag': TunableEnumWithFilter( description= '\n Tag that determines which objects can be linked.\n ', tunable_type=tag.Tag, filter_prefixes=['func'], default=tag.Tag.INVALID, invalid_enums=(tag.Tag.INVALID, )), '_distance': TunableDistanceSquared( description= '\n Max distance from component owner and still be\n linkable.\n ', default=3), '_count': TunableRange( description= '\n Max number of children to link.\n ', tunable_type=int, default=1, minimum=1) } def __init__(self, *args, parent=True, **kwargs): super().__init__(*args, **kwargs) self._children = WeakSet() self._parent = self.owner self._return_state_value = None self._is_parent = parent def on_add(self): self._start() def on_remove(self): self._stop() def on_added_to_inventory(self): self._stop() def on_removed_from_inventory(self): self._start() if self._is_parent: self._add_all() def component_reset(self, reset_reason): if reset_reason != ResetReason.BEING_DESTROYED: self._relink(from_reset=True) elif self._is_parent: self.unlink_all_children() def on_finalize_load(self): self._relink() def on_post_load(self): if self._is_parent and self._children: self._return_state_value = None self.link(None, self._parent_state_value) def on_location_changed(self, old_location): if not services.current_zone( ).is_zone_loading and self._parent is not self.owner: self._relink( update_others=not services.current_zone().is_in_build_buy) def on_reset_component_get_interdependent_reset_records( self, reset_reason, reset_records): owner_users = self.owner.get_users() for obj in self.get_linked_objects_gen(): if self._has_active_link(owner_users, obj): reset_records.append( ResetRecord(obj, ResetReason.RESET_EXPECTED, self, 'Linked object reset')) def _start(self): self._parent = None if self._is_parent: build_buy.register_build_buy_exit_callback(self._relink) def _add_all(self): self._children = self._get_nearby_objects() for child in self._children: self._link_child(child) if self._children: self.link(None, self._parent_state_value) def _stop(self): if self._parent is self.owner: return old_parent = self._parent self._parent = self.owner if self._is_parent: build_buy.unregister_build_buy_exit_callback(self._relink) self.unlink_all_children() elif old_parent is not None: old_parent.linked_object_component.child_unlinked(self.owner) def unlink_all_children(self, update_others=False): for child in self._children: self._unlink(child) if update_others: self._update_others(self._children) self._children.clear() self.unlink_self() def _has_active_link(self, owner_users, severed_object): return owner_users & severed_object.get_users() def _relink(self, update_others=False, from_reset=False): if self._is_parent: if not self._children: self._add_all() return new_children = self._get_nearby_objects() removed_children = self._children - new_children if not from_reset: owner_users = self.owner.get_users() for child in removed_children: if self._has_active_link(owner_users, child): self.owner.reset(ResetReason.RESET_EXPECTED, None, 'Unlinking child') return if not new_children: self.unlink_all_children(update_others=update_others) return for child in removed_children: self._unlink(child) for child in new_children - self._children: self._link_child(child) self._children = new_children if removed_children and update_others: self._update_others(removed_children) elif self._parent is not None: self._parent.linked_object_component.refresh(self.owner) def refresh(self, child): if self._is_parent: if child not in self._children: logger.error( "Refreshing linked child: {} that isn't in parent {}", child, self.owner) return child.linked_object_component.link(self.owner, self._child_state_value) def unlink_self(self): if self._parent is not self.owner: self._parent = None if self._return_state_value is not None: self.owner.state_component.reset_state_to_default( self._return_state_value) self._return_state_value = None def _unlink(self, child): if child not in self._children: logger.error("Removing linked child: {} that isn't in parent {}", child, self.owner) return child.reset(ResetReason.RESET_EXPECTED, self.owner, 'Unlinking from parent') child.linked_object_component.unlink_self() child.remove_component( objects.components.types.LINKED_OBJECT_COMPONENT) def child_unlinked(self, child): if child not in self._children: logger.error("Removing linked child: {} that isn't in parent {}", child, self.owner) return self._relink() def _link_child(self, child): if child.linked_object_component is None: child.add_dynamic_component( objects.components.types.LINKED_OBJECT_COMPONENT, parent=False, _parent_state_value=None, _child_tag=None, _child_state_value=None, _count=None, _distance=None) child.linked_object_component.link(self.owner, self._child_state_value) def link(self, parent, state_value): self._parent = parent if state_value is not None: state_component = self.owner.state_component if state_component is not None: state = state_value.state if self._return_state_value is None: if state_component.has_state(state): self._return_state_value = state_component.get_state( state) state_component.set_state(state_value.state, state_value) @componentmethod_with_fallback(lambda: []) def get_linked_objects_gen(self): if self._is_parent: yield from self._children elif self._parent is not None: if self._parent is not self.owner: yield self._parent for child in self._parent.linked_object_component.get_linked_objects_gen( ): if child is not self.owner: yield child def _get_nearby_objects(self): if self.owner.is_hidden(): return () filtered_near_objects = [] nearby_objects = services.object_manager().get_objects_with_tag_gen( self._child_tag) for test_object in nearby_objects: if self._is_valid_child(test_object): dist_square = (self.owner.position - test_object.position).magnitude_2d_squared() if dist_square < self._distance: filtered_near_objects.append((dist_square, test_object)) filtered_near_objects.sort(key=operator.itemgetter(0)) return_list = set([x[1] for x in filtered_near_objects[:self._count]]) return return_list def _is_valid_child(self, test_object): linked_object_component = test_object.linked_object_component if linked_object_component is not None: if linked_object_component._is_parent: return False if linked_object_component._parent is not self.owner and test_object.linked_object_component._parent is not None: return False if test_object.level != self.owner.level: return False elif test_object.is_hidden(): return False return True def _update_others(self, new_children): owner = self.owner for obj in services.object_manager().get_valid_objects_gen(): if obj.linked_object_component is not None: if obj is not owner: new_children = obj.linked_object_component._try_add_links( new_children) if not new_children: break def _try_add_links(self, new_children): if self.owner is self._parent: return new_children if len(self._children) == self._count: return new_children else: filtered_near_objects = [] for test_object in new_children: if test_object.has_tag(self._child_tag): if test_object.level == self.owner.level: dist_square = ( self.owner.position - test_object.position).magnitude_2d_squared() if dist_square < self._distance: filtered_near_objects.append( (dist_square, test_object)) if filtered_near_objects: filtered_near_objects.sort(key=operator.itemgetter(0)) new_set = set([ x[1] for x in filtered_near_objects[:self._count - len(self._children)] ]) for child in new_set: self._link_child(child) if not self._children: self.link(None, self._parent_state_value) self._children |= new_set return new_children - new_set return new_children
class Ensemble(metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(sims4.resources.Types.ENSEMBLE)): ENSEMBLE_PRIORITIES = TunableList(description='\n A list of ensembles by priority. Those with higher guids will be\n considered more important than those with lower guids.\n \n IMPORTANT: All ensemble types must be referenced in this list.\n ', tunable=TunableReference(description='\n A single ensemble.\n ', manager=services.get_instance_manager(sims4.resources.Types.ENSEMBLE), pack_safe=True)) @staticmethod def get_ensemble_priority(ensemble_type): index = 0 for ensemble in Ensemble.ENSEMBLE_PRIORITIES: if ensemble is ensemble_type: return index index += 1 logger.error('Ensemble of type {} not found in Ensemble Priorities. Please add the ensemble to ensemble.ensemble.', ensemble_type) INSTANCE_TUNABLES = {'max_ensemble_radius': TunableDistanceSquared(description="\n The maximum distance away from the center of mass that Sims will\n receive an autonomy bonus for.\n \n If Sims are beyond this distance from the ensemble's center of mass,\n then they will autonomously consider to run any interaction from\n ensemble_autonomous_interactions.\n \n Any such interaction will have an additional constraint that is a\n circle whose radius is this value.\n ", default=1.0), 'ensemble_autonomy_bonus_multiplier': TunableRange(description='\n The autonomy multiplier that will be applied for objects within the\n autonomy center of mass.\n ', tunable_type=float, default=2.0, minimum=1.0), 'ensemble_autonomous_interactions': TunableSet(description="\n This is a set of self interactions that are generated for Sims part \n of this ensemble.\n \n The interactions don't target anything and have an additional\n constraint equivalent to the circle defined by the ensemble's center\n of mass and radius.\n ", tunable=TunableReference(manager=services.get_instance_manager(sims4.resources.Types.INTERACTION), pack_safe=True)), 'visible': Tunable(description='\n If this ensemble is visible and displays to the UI.\n ', tunable_type=bool, default=True), 'rally': Tunable(description='\n If this is True then this ensemble will offer rallying behavior.\n ', tunable_type=bool, default=True), 'center_of_mass_multiplier': TunableMultiplier.TunableFactory(description="\n Define multipliers that control the weight that a Sim has when\n determining the ensemble's center of mass.\n "), 'max_limit': OptionalTunable(description='\n If enabled this ensemble will have a maximum number of Sims that\n can be a part of it.\n ', tunable=TunableRange(description='\n The maximum number of Sims that can be in this ensemble.\n ', tunable_type=int, default=8, minimum=2)), 'prohibited_species': TunableSet(description='\n A set of species that cannot be added to this type of ensemble.\n ', tunable=TunableEnumEntry(description='\n A species that cannot be added to this type of ensemble.\n ', tunable_type=Species, default=Species.HUMAN, invalid_enums=(Species.INVALID,)))} def __init__(self): self._guid = None self._sims = WeakSet() def __iter__(self): yield from self._sims def __len__(self): return len(self._sims) @property def guid(self): return self._guid @classmethod def can_add_sim_to_ensemble(cls, sim): if sim.species in cls.prohibited_species: return False return True def add_sim_to_ensemble(self, sim): if sim in self._sims: return self._sims.add(sim) if self.ensemble_autonomous_interactions: sim_info_utils.apply_super_affordance_commodity_flags(sim, self, self.ensemble_autonomous_interactions) if self.visible: op = UpdateEnsemble(self._guid, sim.id, True) Distributor.instance().add_op_with_no_owner(op) def remove_sim_from_ensemble(self, sim): self._sims.remove(sim) if self.ensemble_autonomous_interactions: sim_info_utils.remove_super_affordance_commodity_flags(sim, self) if self.visible: op = UpdateEnsemble(self._guid, sim.id, False) Distributor.instance().add_op_with_no_owner(op) def is_sim_in_ensemble(self, sim): return sim in self._sims def start_ensemble(self): self._guid = id_generator.generate_object_id() if self.visible: op = StartEnsemble(self._guid) Distributor.instance().add_op_with_no_owner(op) def end_ensemble(self): if self.ensemble_autonomous_interactions: for sim in self._sims: sim_info_utils.remove_super_affordance_commodity_flags(sim, self) self._sims.clear() if self.visible: op = EndEnsemble(self._guid) Distributor.instance().add_op_with_no_owner(op) @cached def _get_sim_weight(self, sim): return self.center_of_mass_multiplier.get_multiplier(SingleSimResolver(sim.sim_info)) @cached def calculate_level_and_center_of_mass(self): sims_per_level = defaultdict(list) for sim in self._sims: sims_per_level[sim.level].append(sim) best_level = max(sims_per_level, key=lambda level: (len(sims_per_level[level]), -level)) best_sims = sims_per_level[best_level] center_of_mass = sum((sim.position*self._get_sim_weight(sim) for sim in best_sims), sims4.math.Vector3.ZERO())/sum(self._get_sim_weight(sim) for sim in best_sims) return (best_level, center_of_mass) def is_within_ensemble_radius(self, obj): (level, center_of_mass) = self.calculate_level_and_center_of_mass() if obj.level != level: return False else: distance = (obj.position - center_of_mass).magnitude_squared() if distance > self.max_ensemble_radius: return False return True @cached def get_ensemble_multiplier(self, target): if self.is_within_ensemble_radius(target): return self.ensemble_autonomy_bonus_multiplier return 1 def get_center_of_mass_constraint(self): if not self: logger.warn('No Sims in ensemble when trying to construct constraint.') return ANYWHERE (level, position) = self.calculate_level_and_center_of_mass() routing_surface = routing.SurfaceIdentifier(services.current_zone_id(), level, routing.SurfaceType.SURFACETYPE_WORLD) return Circle(position, sqrt(self.max_ensemble_radius), routing_surface) def get_ensemble_autonomous_interactions_gen(self, context, **interaction_parameters): if self.is_within_ensemble_radius(context.sim): return for ensemble_affordance in self.ensemble_autonomous_interactions: affordance = EnsembleConstraintProxyInteraction.generate(ensemble_affordance, self) yield from affordance.potential_interactions(context.sim, context, **interaction_parameters)