class AttractorManagerMixin: ATTRACTOR_OBJECT_TAGS = TunableSet(description='\n One or more tags that indicate an object is a type of attractor point.\n We use attractor points to push Sims near things and reference specific\n geography in the world.\n ', tunable=TunableEnumWithFilter(description='\n A specific tag.\n ', tunable_type=tag.Tag, default=tag.Tag.INVALID, invalid_enums=(tag.Tag.INVALID,), filter_prefixes=('AtPo',))) SPAWN_POINT_ATTRACTORS = TunableMapping(description='\n Mapping from spawn point tags to attractor objects so we can create\n attractor points at spawn points.\n ', key_type=TunableEnumEntry(description='\n The tag on the spawn point.\n ', tunable_type=tag.Tag, default=tag.Tag.INVALID, invalid_enums=(tag.Tag.INVALID,)), key_name='spawn point tag', value_type=TunableReference(description='\n The object we want to create on the Spawn Point.\n ', manager=services.definition_manager(), pack_safe=True), value_name='attractor point definition') def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._dynamic_attractor_ids = WeakSet() def create_dynamic_attractor_object(self, definition_id, location, tags_to_add=None): tags_to_add = set() if tags_to_add is None else tags_to_add def setup_obj(obj): obj.append_tags(tags_to_add) obj.location = location obj.persistence_group = PersistenceGroups.NONE created_obj = objects.system.create_object(definition_id, init=setup_obj) self._dynamic_attractor_ids.add(created_obj) if not self.ATTRACTOR_OBJECT_TAGS.intersection(created_obj.get_tags()): logger.warn('Attractor object does not have any tags in the ATTRACTOR OBJECT TAGS list. We need to be able to locate attractor objects and keep track of them.') return created_obj def destroy_dynamic_attractor_object(self, object_id): obj_to_destroy = self.get(object_id) if obj_to_destroy is None: logger.error('Object {} is not a dynamic attractor point.', object_id) return self._dynamic_attractor_ids.discard(obj_to_destroy) obj_to_destroy.destroy(obj_to_destroy, cause='Destroying Dynamic Attractor Point') def get_attractor_objects(self): return self.get_objects_matching_tags(AttractorManagerMixin.ATTRACTOR_OBJECT_TAGS) def create_spawn_point_attractor(self, spawn_point): obj_ids = set() for (spawn_point_tag, attractor_definition) in self.SPAWN_POINT_ATTRACTORS.items(): tags = spawn_point.get_tags() if spawn_point_tag in tags: location = sims4.math.Location(transform=spawn_point.get_approximate_transform(), routing_surface=spawn_point.routing_surface) obj = self.create_dynamic_attractor_object(attractor_definition, location, tags_to_add={spawn_point_tag}) obj_ids.add(obj.id) return frozenset(obj_ids)
class Client: _interaction_source = interactions.context.InteractionContext.SOURCE_PIE_MENU _interaction_priority = interactions.priority.Priority.High def __init__(self, session_id, account, household_id): self.id = session_id self.manager = None self._account = account self._household_id = household_id self._choice_menu = None self._interaction_parameters = {} self.active = True self.zone_id = services.current_zone_id() self._selectable_sims = SelectableSims(self) self._active_sim_info = None self._active_sim_changed = CallableList() self.ui_objects = weakref.WeakSet() self.primitives = () self._live_drag_objects = [] self._live_drag_start_system = LiveDragLocation.INVALID self._live_drag_is_stack = False self._live_drag_sell_dialog_active = False self.objects_moved_via_live_drag = WeakSet() def __repr__(self): return '<Client {0:#x}>'.format(self.id) @property def account(self): return self._account @distributor.fields.Field(op=distributor.ops.UpdateClientActiveSim) def active_sim_info(self): return self._active_sim_info resend_active_sim_info = active_sim_info.get_resend() @active_sim_info.setter def active_sim_info(self, sim_info): self._set_active_sim_without_field_distribution(sim_info) @property def active_sim(self): if self.active_sim_info is not None: return self.active_sim_info.get_sim_instance( allow_hidden_flags=ALL_HIDDEN_REASONS) @active_sim.setter def active_sim(self, sim): self.active_sim_info = sim.sim_info def _set_active_sim_without_field_distribution(self, sim_info): if self._active_sim_info is not None and self._active_sim_info is sim_info: return current_sim = self._active_sim_info.get_sim_instance( ) if self._active_sim_info is not None else None if sim_info is not None: self._active_sim_info = sim_info if sim_info.household is not None: sim_info.household.on_active_sim_changed(sim_info) else: self._active_sim_info = None self.notify_active_sim_changed(current_sim, new_sim_info=sim_info) @property def choice_menu(self): return self._choice_menu @property def interaction_source(self): return self._interaction_source @interaction_source.setter def interaction_source(self, value): if value is None: if self._interaction_source is not Client._interaction_source: del self._interaction_source else: self._interaction_source = value @property def interaction_priority(self): return self._interaction_priority @interaction_priority.setter def interaction_priority(self, value): if value is None: if self._interaction_priority is not Client._interaction_priority: del self._interaction_priority else: self._interaction_priority = value @property def household_id(self): return self._household_id @property def household(self): household_manager = services.household_manager() if household_manager is not None: return household_manager.get(self._household_id) @property def selectable_sims(self): return self._selectable_sims def create_interaction_context(self, sim, **kwargs): context = interactions.context.InteractionContext( sim, self.interaction_source, self.interaction_priority, client=self, **kwargs) return context @property def live_drag_objects(self): return self._live_drag_objects def get_interaction_parameters(self): return self._interaction_parameters def set_interaction_parameters(self, **kwargs): self._interaction_parameters = kwargs def set_choices(self, new_choices): self._choice_menu = new_choices def select_interaction(self, choice_id, revision): if self.choice_menu is not None: if revision == self.choice_menu.revision: choice_menu = self.choice_menu self._choice_menu = None self.set_interaction_parameters() try: return choice_menu.select(choice_id) except: if choice_menu.simref is not None: sim = choice_menu.simref() if sim is not None: sim.reset( ResetReason.RESET_ON_ERROR, cause= 'Exception while selecting interaction from the pie menu.' ) raise def get_create_op(self, *args, **kwargs): return distributor.ops.ClientCreate(self, *args, is_active=True, **kwargs) def get_delete_op(self): return distributor.ops.ClientDelete() def get_create_after_objs(self): active = self.active_sim if active is not None: yield active household = self.household if household is not None: yield household @property def valid_for_distribution(self): return True def refresh_achievement_data(self): active_sim_info = None if self.active_sim is not None: active_sim_info = self.active_sim.sim_info self.account.achievement_tracker.refresh_progress(active_sim_info) def send_message(self, msg_id, msg): if self.active: omega.send(self.id, msg_id, msg.SerializeToString()) else: logger.warn( 'Message sent to client {} after it has already disconnected.', self) def validate_selectable_sim(self): if self._active_sim_info is None or not self._active_sim_info.is_enabled_in_skewer: self.set_next_sim() def set_next_sim(self): sim_info = self._selectable_sims.get_next_selectable( self._active_sim_info) if sim_info is self.active_sim_info: self.resend_active_sim_info() return False return self.set_active_sim_info(sim_info) def set_next_sim_or_none(self, only_if_this_active_sim_info=None): if only_if_this_active_sim_info is not None and self._active_sim_info is not only_if_this_active_sim_info: return sim_info = self._selectable_sims.get_next_selectable( self._active_sim_info) if sim_info is None: return self.set_active_sim_info(None) if sim_info is self._active_sim_info: return self.set_active_sim_info(None) return self.set_active_sim_info(sim_info) def set_active_sim_by_id(self, sim_id): if self.active_sim_info is not None and self.active_sim_info.id == sim_id: return False for sim_info in self._selectable_sims: if sim_info.sim_id == sim_id: if not sim_info.is_enabled_in_skewer: return False return self.set_active_sim_info(sim_info) return False def set_active_sim(self, sim): return self.set_active_sim_info(sim.sim_info) def set_active_sim_info(self, sim_info): with telemetry_helper.begin_hook(writer, TELEMETRY_HOOK_ACTIVE_SIM_CHANGED, sim_info=sim_info): pass self.active_sim_info = sim_info return self._active_sim_info is not None def add_selectable_sim_info(self, sim_info, send_relationship_update=True): self._selectable_sims.add_selectable_sim_info( sim_info, send_relationship_update=send_relationship_update) if self.active_sim_info is None: self.set_next_sim() self.household.refresh_aging_updates(sim_info) def add_selectable_sim_by_id(self, sim_id): sim_info = services.sim_info_manager().get(sim_id) if sim_info is not None: self.add_selectable_sim_info(sim_info) def remove_selectable_sim_info(self, sim_info): self._selectable_sims.remove_selectable_sim_info(sim_info) if self.active_sim_info is None: self.set_next_sim() self.household.refresh_aging_updates(sim_info) def remove_selectable_sim_by_id(self, sim_id): if len(self._selectable_sims) <= 1: return False sim_info = services.sim_info_manager().get(sim_id) if sim_info is not None: self.remove_selectable_sim_info(sim_info) return True def make_all_sims_selectable(self): self.clear_selectable_sims() for sim_info in services.sim_info_manager().objects: self._selectable_sims.add_selectable_sim_info(sim_info) self.set_next_sim() def clear_selectable_sims(self): self.active_sim_info = None self._selectable_sims.clear_selectable_sims() def register_active_sim_changed(self, callback): if callback not in self._active_sim_changed: self._active_sim_changed.append(callback) def unregister_active_sim_changed(self, callback): if callback in self._active_sim_changed: self._active_sim_changed.remove(callback) def on_sim_added_to_skewer(self, sim_info, send_relationship_update=True): if send_relationship_update: sim_info.relationship_tracker.send_relationship_info() is_zone_running = services.current_zone().is_zone_running sim_info.on_sim_added_to_skewer() if not is_zone_running: sim_info.commodity_tracker.start_low_level_simulation() else: services.active_household().distribute_household_data() sim_info.commodity_tracker.send_commodity_progress_update( from_add=True) sim_info.career_tracker.on_sim_added_to_skewer() if sim_info.degree_tracker is not None: sim_info.degree_tracker.on_sim_added_to_skewer() sim_info.send_whim_bucks_update(SetWhimBucks.LOAD) sim_info.resend_trait_ids() sim = sim_info.get_sim_instance(allow_hidden_flags=ALL_HIDDEN_REASONS) if sim is not None: if is_zone_running: sim.inventory_component.visible_storage.allow_ui = True sim.inventory_component.publish_inventory_items() sim.ui_manager.refresh_ui_data() services.autonomy_service().logging_sims.add(sim) sim_info.start_aspiration_tracker_on_instantiation( force_ui_update=True) if sim_info.whim_tracker is not None: sim_info.whim_tracker.start_whims_tracker() zone_director = services.venue_service().get_zone_director() if zone_director is not None: zone_director.on_sim_added_to_skewer(sim_info) sim_info.trait_tracker.sort_and_send_commodity_list() def on_sim_removed_from_skewer(self, sim_info): sim = sim_info.get_sim_instance() if sim is not None: autonomy_service = services.autonomy_service() if autonomy_service is not None: autonomy_service.logging_sims.discard(sim) def clean_and_send_remaining_relationship_info(self): relationship_service = services.relationship_service() for sim_info in self.selectable_sims: sim_info.relationship_tracker.clean_and_send_remaining_relationship_info( ) relationship_service.clean_and_send_remaining_object_relationships( sim_info.id) def cancel_live_drag_on_objects(self): for obj in self._live_drag_objects: obj.live_drag_component.cancel_live_dragging() self._live_drag_objects = [] def _get_stack_items_from_drag_object(self, drag_object, remove=False, is_stack=False): if drag_object.inventoryitem_component is None: return (False, None) previous_inventory = drag_object.inventoryitem_component.get_inventory( ) if previous_inventory is None: return (False, None) stack_id = drag_object.inventoryitem_component.get_stack_id() stack_items = previous_inventory.get_stack_items(stack_id) if remove: if is_stack: for item in stack_items: success = previous_inventory.try_remove_object_by_id( item.id, count=item.stack_count()) else: success = previous_inventory.try_remove_object_by_id( drag_object.id, count=1) stack_items = previous_inventory.get_stack_items(stack_id) else: success = previous_inventory.try_remove_object_by_id( drag_object.id, count=1) stack_items = previous_inventory.get_stack_items(stack_id) else: success = True return (success, stack_items) def remove_drag_object_and_get_next_item(self, drag_object): next_object_id = None (success, stack_items) = self._get_stack_items_from_drag_object(drag_object, remove=True) if success: if stack_items: next_object_id = stack_items[0].id return (success, next_object_id) def get_live_drag_object_value(self, drag_object, is_stack=False): (_, stack_items) = self._get_stack_items_from_drag_object( drag_object, remove=False, is_stack=is_stack) value = 0 if is_stack and stack_items: for item in stack_items: value += item.current_value * item.stack_count() else: value = drag_object.current_value return value def start_live_drag(self, live_drag_object, start_system, is_stack, should_send_start_message: bool = True): self._live_drag_start_system = start_system success = True if is_stack: inventoryitem_component = live_drag_object.inventoryitem_component stack_id = inventoryitem_component.get_stack_id() current_inventory = inventoryitem_component.get_inventory() stack_items = current_inventory.get_stack_items(stack_id) else: stack_items = [live_drag_object] for item in stack_items: live_drag_component = live_drag_object.live_drag_component live_drag_component = item.live_drag_component if live_drag_component is None: logger_live_drag.error( 'Live Drag Start called on an object with no Live Drag Component. Object: {}' .format(item)) self.send_live_drag_cancel(live_drag_object.id) return if not ((not item.in_use or item.in_use_by(self)) and live_drag_component.can_live_drag): logger_live_drag.warn( 'Live Drag Start called on an object that is in use. Object: {}' .format(item)) self.send_live_drag_cancel(item.id) return success = live_drag_component.start_live_dragging( self, start_system) if not success: break self._live_drag_objects.append(item) if not success: self.cancel_live_drag_on_objects() self.send_live_drag_cancel(live_drag_object.id, LiveDragLocation.INVALID) self._live_drag_is_stack = is_stack if gsi_handlers.live_drag_handlers.live_drag_archiver.enabled: gsi_handlers.live_drag_handlers.archive_live_drag( 'Start', 'Operation', LiveDragLocation.GAMEPLAY_SCRIPT, start_system, live_drag_object_id=live_drag_object.id) if live_drag_object.live_drag_component.active_household_has_sell_permission: sell_value = self.get_live_drag_object_value( live_drag_object, self._live_drag_is_stack ) if live_drag_object.definition.get_is_deletable() else -1 for child_object in live_drag_object.get_all_children_gen(): sell_value += self.get_live_drag_object_value( child_object) if child_object.definition.get_is_deletable( ) else 0 else: sell_value = -1 (valid_drop_object_ids, valid_stack_id) = live_drag_component.get_valid_drop_object_ids() icon_info = create_icon_info_msg(live_drag_object.get_icon_info_data()) if should_send_start_message: op = distributor.ops.LiveDragStart(live_drag_object.id, start_system, valid_drop_object_ids, valid_stack_id, sell_value, icon_info) distributor_system = Distributor.instance() distributor_system.add_op_with_no_owner(op) def end_live_drag(self, source_object, target_object=None, end_system=LiveDragLocation.INVALID, location=None): live_drag_component = source_object.live_drag_component if live_drag_component is None: logger_live_drag.error( 'Live Drag End called on an object with no Live Drag Component. Object: {}' .format(source_object)) self.send_live_drag_cancel(source_object.id, end_system) return if source_object not in self._live_drag_objects: logger_live_drag.warn( 'Live Drag End called on an object not being Live Dragged. Object: {}' .format(source_object)) self.send_live_drag_cancel(source_object.id, end_system) return source_object_id = source_object.id if end_system == LiveDragLocation.BUILD_BUY: self.objects_moved_via_live_drag.add(source_object) else: self.objects_moved_via_live_drag.discard(source_object) self.cancel_live_drag_on_objects() next_object_id = None success = False inventory_item = source_object.inventoryitem_component if target_object is not None: live_drag_target_component = target_object.live_drag_target_component if live_drag_target_component is not None: (success, next_object_id ) = live_drag_target_component.drop_live_drag_object( source_object, self._live_drag_is_stack) elif source_object.parent_object() is target_object: success = True elif source_object.parent_object( ) is None and location is not None: inventory_item = source_object.inventoryitem_component if inventory_item is not None: if inventory_item.is_in_inventory(): (success, next_object_id ) = self.remove_drag_object_and_get_next_item( source_object) source_object.set_location(location) else: logger_live_drag.error( 'Live Drag Target Component missing on object: {} and {} cannot be slotted into it.' .format(target_object, source_object)) success = False elif inventory_item is not None and inventory_item.is_in_inventory(): if inventory_item.can_place_in_world or not inventory_item.inventory_only: if location is not None: source_object.set_location(location) (success, next_object_id ) = self.remove_drag_object_and_get_next_item(source_object) else: success = True if location is not None: source_object.set_location(location) if success: if gsi_handlers.live_drag_handlers.live_drag_archiver.enabled: gsi_handlers.live_drag_handlers.archive_live_drag( 'End', 'Operation', LiveDragLocation.GAMEPLAY_SCRIPT, end_system, live_drag_object_id=source_object_id, live_drag_target=target_object) if not self._live_drag_is_stack: next_object_id = None op = distributor.ops.LiveDragEnd(source_object_id, self._live_drag_start_system, end_system, next_object_id) distributor_system = Distributor.instance() distributor_system.add_op_with_no_owner(op) self._live_drag_objects = [] self._live_drag_start_system = LiveDragLocation.INVALID self._live_drag_is_stack = False else: self.send_live_drag_cancel(source_object_id, end_system) def cancel_live_drag(self, live_drag_object, end_system=LiveDragLocation.INVALID): live_drag_component = live_drag_object.live_drag_component if live_drag_component is None: logger_live_drag.warn( 'Live Drag Cancel called on an object with no Live Drag Component. Object: {}' .format(live_drag_object)) self.send_live_drag_cancel(live_drag_object.id) return if live_drag_component.live_drag_state == LiveDragState.NOT_LIVE_DRAGGING: logger_live_drag.warn( 'Live Drag Cancel called on an object not being Live Dragged. Object: {}' .format(live_drag_object)) else: self.cancel_live_drag_on_objects() self.send_live_drag_cancel(live_drag_object.id, end_system) def sell_live_drag_object(self, live_drag_object, end_system=LiveDragLocation.INVALID): live_drag_component = live_drag_object.live_drag_component if live_drag_component is None or not live_drag_object.definition.get_is_deletable( ): logger_live_drag.error( "Live Drag Sell called on object with no Live Drag Component or can't be deleted. Object: {}" .format(live_drag_object)) self.send_live_drag_cancel(live_drag_object.id, end_system) return def sell_response(dialog): op = distributor.ops.LiveDragEnd(live_drag_object.id, self._live_drag_start_system, end_system, next_stack_object_id=None) distributor_system = Distributor.instance() distributor_system.add_op_with_no_owner(op) if not dialog.accepted: self.cancel_live_drag_on_objects() return value = int( self.get_live_drag_object_value(live_drag_object, self._live_drag_is_stack)) for child_object in live_drag_object.get_all_children_gen(): value += self.get_live_drag_object_value( child_object) if child_object.definition.get_is_deletable( ) else 0 object_tags = set() if self._live_drag_is_stack: (_, stack_items) = self._get_stack_items_from_drag_object( live_drag_object, remove=True, is_stack=True) for item in stack_items: live_drag_component = item.live_drag_component live_drag_component.cancel_live_dragging( should_reset=False) item.base_value = 0 item.set_stack_count(0) object_tags.update(item.get_tags()) item.destroy(source=item, cause='Selling stack of live drag objects.') else: live_drag_object.live_drag_component.cancel_live_dragging( should_reset=False) object_tags.update(live_drag_object.get_tags()) if live_drag_object.is_in_inventory(): self.remove_drag_object_and_get_next_item(live_drag_object) else: live_drag_object.remove_from_client() object_tags = frozenset(object_tags) live_drag_object.base_value = 0 live_drag_object.destroy(source=live_drag_object, cause='Selling live drag object.') services.active_household().funds.add( value, Consts_pb2.TELEMETRY_OBJECT_SELL, self.active_sim, tags=object_tags) self._live_drag_objects = [] self._live_drag_start_system = LiveDragLocation.INVALID self._live_drag_is_stack = False self._live_drag_sell_dialog_active = False favorites_tracker = self.active_sim_info.favorites_tracker if favorites_tracker and favorites_tracker.is_favorite_stack( live_drag_object): dialog = LiveDragTuning.LIVE_DRAG_SELL_FAVORITE_DIALOG( owner=live_drag_object) elif self._live_drag_is_stack: dialog = LiveDragTuning.LIVE_DRAG_SELL_STACK_DIALOG( owner=live_drag_object) else: dialog = LiveDragTuning.LIVE_DRAG_SELL_DIALOG( owner=live_drag_object) dialog.show_dialog(on_response=sell_response) self._live_drag_sell_dialog_active = True def send_live_drag_cancel(self, live_drag_object_id, live_drag_end_system=LiveDragLocation.INVALID): if gsi_handlers.live_drag_handlers.live_drag_archiver.enabled: gsi_handlers.live_drag_handlers.archive_live_drag( 'Cancel', 'Operation', LiveDragLocation.GAMEPLAY_SCRIPT, live_drag_end_system, live_drag_object_id=live_drag_object_id) op = distributor.ops.LiveDragCancel(live_drag_object_id, self._live_drag_start_system, live_drag_end_system) distributor_system = Distributor.instance() distributor_system.add_op_with_no_owner(op) if not self._live_drag_sell_dialog_active: self._live_drag_objects = [] self._live_drag_start_system = LiveDragLocation.INVALID self._live_drag_is_stack = False def on_add(self): if self._account is not None: self._account.register_client(self) for sim_info in self._selectable_sims: self.on_sim_added_to_skewer(sim_info) distributor = Distributor.instance() distributor.add_object(self) distributor.add_client(self) self.send_selectable_sims_update() self.selectable_sims.add_watcher(self, self.send_selectable_sims_update) def on_remove(self): if self.active_sim is not None: self._set_active_sim_without_field_distribution(None) if self._account is not None: self._account.unregister_client(self) for sim_info in self._selectable_sims: self.on_sim_removed_from_skewer(sim_info) self.selectable_sims.remove_watcher(self) distributor = Distributor.instance() distributor.remove_client(self) self._selectable_sims = None self.active = False def get_objects_in_view_gen(self): for manager in services.client_object_managers(): for obj in manager.get_all(): yield obj def notify_active_sim_changed(self, old_sim, new_sim_info=None): new_sim = new_sim_info.get_sim_instance( ) if new_sim_info is not None else None self._active_sim_changed(old_sim, new_sim) vfx_mask.notify_client_mask_update(new_sim_info) def _get_selector_visual_type(self, sim_info): if sim_info.is_baby: return (Sims_pb2.SimPB.BABY, None) if sim_info.is_toddler and services.daycare_service( ).is_sim_info_at_daycare(sim_info): return (Sims_pb2.SimPB.AT_DAYCARE, None) if sim_info.household.missing_pet_tracker.is_pet_missing(sim_info): return (Sims_pb2.SimPB.PET_MISSING, None) sim = sim_info.get_sim_instance(allow_hidden_flags=ALL_HIDDEN_REASONS) for career in sim_info.careers.values(): if career.currently_at_work: if career.is_at_active_event and sim is None: return (Sims_pb2.SimPB.MISSING_ACTIVE_WORK, career.career_category) return (Sims_pb2.SimPB.AT_WORK, career.career_category) if career.is_late: if not career.taking_day_off: return (Sims_pb2.SimPB.LATE_FOR_WORK, career.career_category) if services.get_rabbit_hole_service( ).should_override_selector_visual_type(sim_info.id): return (Sims_pb2.SimPB.OTHER, None) if sim is not None and sim.has_hidden_flags( HiddenReasonFlag.RABBIT_HOLE): return (Sims_pb2.SimPB.OTHER, None) if services.hidden_sim_service().is_hidden(sim_info.id): return (Sims_pb2.SimPB.OTHER, None) tutorial_service = services.get_tutorial_service() if tutorial_service is not None and tutorial_service.is_sim_unselectable( sim_info): return (Sims_pb2.SimPB.OTHER, None) return (Sims_pb2.SimPB.NORMAL, None) def send_selectable_sims_update(self): msg = Sims_pb2.UpdateSelectableSims() for sim_info in self._selectable_sims: with ProtocolBufferRollback(msg.sims) as new_sim: new_sim.id = sim_info.sim_id if sim_info.career_tracker is None: logger.error( 'CareerTracker is None for selectable Sim {}'.format( sim_info)) else: career = sim_info.career_tracker.get_currently_at_work_career( ) new_sim.at_work = career is not None and not career.is_at_active_event new_sim.is_selectable = sim_info.is_enabled_in_skewer (selector_visual_type, career_category) = self._get_selector_visual_type(sim_info) new_sim.selector_visual_type = selector_visual_type if career_category is not None: new_sim.career_category = career_category new_sim.can_care_for_toddler_at_home = sim_info.can_care_for_toddler_at_home if not sim_info.is_instanced( allow_hidden_flags=ALL_HIDDEN_REASONS): new_sim.instance_info.zone_id = sim_info.zone_id new_sim.instance_info.world_id = sim_info.world_id new_sim.firstname = sim_info.first_name new_sim.lastname = sim_info.last_name zone_data_proto = services.get_persistence_service( ).get_zone_proto_buff(sim_info.zone_id) if zone_data_proto is not None: new_sim.instance_info.zone_name = zone_data_proto.name distributor = Distributor.instance() distributor.add_op_with_no_owner( GenericProtocolBufferOp(Operation.SELECTABLE_SIMS_UPDATE, msg)) @constproperty def is_sim(): return False
class BroadcasterService(Service): INTERVAL = TunableRealSecond(description='\n The time between broadcaster pulses. A lower number will impact\n performance.\n ', default=5) DEFAULT_QUADTREE_RADIUS = 0.1 def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._alarm_handle = None self._processing_task = None self._on_update_callbacks = CallableList() self._pending_broadcasters = [] self._active_broadcasters = [] self._cluster_requests = {} self._object_cache = None self._object_cache_tags = None self._pending_update = False self._quadtrees = defaultdict(sims4.geometry.QuadTree) def create_update_alarm(self): self._alarm_handle = add_alarm(self, interval_in_real_seconds(self.INTERVAL), self._on_update, repeating=True, use_sleep_time=False) def start(self): self.create_update_alarm() object_manager = services.object_manager() object_manager.register_callback(CallbackTypes.ON_OBJECT_LOCATION_CHANGED, self._update_object_cache) object_manager.register_callback(CallbackTypes.ON_OBJECT_ADD, self._update_object_cache) services.current_zone().wall_contour_update_callbacks.append(self._on_wall_contours_changed) def stop(self): if self._alarm_handle is not None: cancel_alarm(self._alarm_handle) self._alarm_handle = None if self._processing_task is not None: self._processing_task.stop() self._processing_task = None object_manager = services.object_manager() object_manager.unregister_callback(CallbackTypes.ON_OBJECT_LOCATION_CHANGED, self._update_object_cache) object_manager.unregister_callback(CallbackTypes.ON_OBJECT_ADD, self._update_object_cache) services.current_zone().wall_contour_update_callbacks.remove(self._on_wall_contours_changed) def add_broadcaster(self, broadcaster): if broadcaster not in self._pending_broadcasters: self._pending_broadcasters.append(broadcaster) if broadcaster.immediate: self._pending_update = True self._on_update_callbacks() def remove_broadcaster(self, broadcaster): if broadcaster in self._pending_broadcasters: self._pending_broadcasters.remove(broadcaster) if broadcaster in self._active_broadcasters: self._remove_from_cluster_request(broadcaster) self._remove_broadcaster_from_quadtree(broadcaster) self._active_broadcasters.remove(broadcaster) broadcaster.on_removed() self._on_update_callbacks() def _activate_pending_broadcasters(self): for broadcaster in self._pending_broadcasters: self._active_broadcasters.append(broadcaster) self.update_cluster_request(broadcaster) self._update_object_cache() self._pending_broadcasters.clear() def _add_broadcaster_to_quadtree(self, broadcaster): self._remove_broadcaster_from_quadtree(broadcaster) broadcaster_quadtree = self._quadtrees[broadcaster.routing_surface.secondary_id] broadcaster_bounds = sims4.geometry.QtCircle(sims4.math.Vector2(broadcaster.position.x, broadcaster.position.z), self.DEFAULT_QUADTREE_RADIUS) broadcaster_quadtree.insert(broadcaster, broadcaster_bounds) return broadcaster_quadtree def _remove_broadcaster_from_quadtree(self, broadcaster): broadcaster_quadtree = broadcaster.quadtree if broadcaster_quadtree is not None: broadcaster_quadtree.remove(broadcaster) def update_cluster_request(self, broadcaster): if broadcaster not in self._active_broadcasters: return clustering_request = broadcaster.get_clustering() if clustering_request is None: return self._remove_from_cluster_request(broadcaster) cluster_request_key = (type(broadcaster), broadcaster.routing_surface.secondary_id) if cluster_request_key in self._cluster_requests: cluster_request = self._cluster_requests[cluster_request_key] cluster_request.set_object_dirty(broadcaster) else: cluster_quadtree = self._quadtrees[broadcaster.routing_surface.secondary_id] cluster_request = clustering_request(lambda : self._get_broadcasters_for_cluster_request_gen(*cluster_request_key), quadtree=cluster_quadtree) self._cluster_requests[cluster_request_key] = cluster_request quadtree = self._add_broadcaster_to_quadtree(broadcaster) broadcaster.on_added_to_quadtree_and_cluster_request(quadtree, cluster_request) def _remove_from_cluster_request(self, broadcaster): cluster_request = broadcaster.cluster_request if cluster_request is not None: cluster_request.set_object_dirty(broadcaster) def _is_valid_cache_object(self, obj): if obj.is_sim: return False elif self._object_cache_tags: object_tags = obj.get_tags() if object_tags & self._object_cache_tags: return True else: return False return False return True def get_object_cache_info(self): return (self._object_cache, self._object_cache_tags) def _generate_object_cache(self): self._object_cache = WeakSet(obj for obj in services.object_manager().valid_objects() if self._is_valid_cache_object(obj)) def _update_object_cache(self, obj=None): if obj is None: self._object_cache = None self._object_cache_tags = None return if self._object_cache is not None and self._is_valid_cache_object(obj): self._object_cache.add(obj) def _is_valid_broadcaster(self, broadcaster): broadcasting_object = broadcaster.broadcasting_object if broadcasting_object is None or not broadcasting_object.visible_to_client: return False if broadcasting_object.is_in_inventory(): return False elif broadcasting_object.parent is not None and broadcasting_object.parent.is_sim: return False return True def _get_broadcasters_for_cluster_request_gen(self, broadcaster_type, broadcaster_level): for broadcaster in self._active_broadcasters: if broadcaster.guid == broadcaster_type.guid: if broadcaster.should_cluster(): if broadcaster.routing_surface.secondary_id == broadcaster_level: yield broadcaster def get_broadcasters_debug_gen(self): for cluster_request in self._cluster_requests.values(): for cluster in cluster_request.get_clusters_gen(): broadcaster_iter = cluster.objects_gen() yield next(broadcaster_iter) yield from cluster_request.get_rejects() for broadcaster in self._active_broadcasters: if not broadcaster.should_cluster(): if self._is_valid_broadcaster(broadcaster): yield broadcaster def get_broadcasters_gen(self): for (cluster_request_key, cluster_request) in self._cluster_requests.items(): is_cluster_dirty = cluster_request.is_dirty() for broadcaster in self._get_broadcasters_for_cluster_request_gen(*cluster_request_key): if self._is_valid_broadcaster(broadcaster): broadcaster.regenerate_constraint() for cluster in cluster_request.get_clusters_gen(): linkable_broadcasters_iter = (b for b in cluster.objects_gen() if self._is_valid_broadcaster(b)) master_broadcaster = next(linkable_broadcasters_iter, None) if master_broadcaster is None: continue master_broadcaster.set_linked_broadcasters(linkable_broadcasters_iter) yield master_broadcaster yield from (b for b in cluster_request.get_rejects() if self._is_valid_broadcaster(b)) for broadcaster in self._active_broadcasters: if not broadcaster.should_cluster(): if self._is_valid_broadcaster(broadcaster): yield broadcaster PathSegmentData = namedtuple('PathSegmentData', ('prev_pos', 'cur_pos', 'segment_vec', 'segment_mag_sq', 'segment_normal')) def get_broadcasters_along_route_gen(self, sim, path, start_time=0, end_time=0): path_segment_datas = {} start_index = max(0, path.node_at_time(start_time).index - 1) end_index = min(len(path) - 1, path.node_at_time(end_time).index) for broadcaster in self.get_broadcasters_gen(): if broadcaster.route_events: if not broadcaster.can_affect(sim): continue constraint = broadcaster.get_constraint() geometry = constraint.geometry if geometry is None: continue polygon = geometry.polygon if polygon is None: continue if not constraint.valid: continue constraint_pos = polygon.centroid() constraint_radius_sq = polygon.radius() constraint_radius_sq = constraint_radius_sq*constraint_radius_sq for index in range(end_index, start_index, -1): prev_index = index - 1 prev_node = path.nodes[prev_index] if not constraint.is_routing_surface_valid(prev_node.routing_surface_id): continue segment_key = (prev_index, index) segment_data = path_segment_datas.get(segment_key, None) if segment_data is None: cur_node = path.nodes[index] cur_pos = sims4.math.Vector3(*cur_node.position) prev_pos = sims4.math.Vector3(*prev_node.position) segment_vec = cur_pos - prev_pos segment_vec.y = 0 segment_mag_sq = segment_vec.magnitude_2d_squared() if sims4.math.almost_equal_sq(segment_mag_sq, 0): segment_normal = None else: segment_normal = segment_vec/sims4.math.sqrt(segment_mag_sq) segment_data = BroadcasterService.PathSegmentData(prev_pos, cur_pos, segment_vec, segment_mag_sq, segment_normal) path_segment_datas[segment_key] = segment_data else: (prev_pos, cur_pos, segment_vec, segment_mag_sq, segment_normal) = segment_data if segment_normal is None: constraint_vec = constraint_pos - prev_pos constraint_dist_sq = constraint_vec.magnitude_2d_squared() if constraint_radius_sq < constraint_dist_sq: continue else: constraint_vec = constraint_pos - prev_pos constraint_vec.y = 0 contraint_proj = constraint_vec - segment_normal*sims4.math.vector_dot_2d(constraint_vec, segment_normal) if constraint_radius_sq < contraint_proj.magnitude_2d_squared(): continue for (transform, _, time) in path.get_location_data_along_segment_gen(prev_index, index): if not geometry.test_transform(transform): continue yield (time, broadcaster) break break def get_pending_broadcasters_gen(self): yield from self._pending_broadcasters def _get_all_objects_gen(self): is_any_broadcaster_allowing_objects = True if self._object_cache else False if not is_any_broadcaster_allowing_objects: for broadcaster in self._active_broadcasters: (allow_objects, allow_objects_tags) = broadcaster.allow_objects.is_affecting_objects() if allow_objects: is_any_broadcaster_allowing_objects = True if allow_objects_tags is None: self._object_cache_tags = None break else: if self._object_cache_tags is None: self._object_cache_tags = set() self._object_cache_tags |= allow_objects_tags if is_any_broadcaster_allowing_objects: if self._object_cache is None: self._generate_object_cache() yield from list(self._object_cache) else: self._object_cache = None self._object_cache_tags = None yield from services.sim_info_manager().instanced_sims_gen() def register_callback(self, callback): if callback not in self._on_update_callbacks: self._on_update_callbacks.append(callback) def unregister_callback(self, callback): if callback in self._on_update_callbacks: self._on_update_callbacks.remove(callback) def _on_update(self, _): self._pending_update = True def _on_wall_contours_changed(self, *_, **__): self._update_object_cache() def provide_route_events(self, route_event_context, sim, path, failed_types=None, start_time=0, end_time=0, **kwargs): for (time, broadcaster) in self.get_broadcasters_along_route_gen(sim, path, start_time=start_time, end_time=end_time): resolver = broadcaster.get_resolver(sim) for route_event in broadcaster.route_events: if not failed_types is None: pass if not route_event_context.route_event_already_scheduled(route_event, provider=broadcaster) and route_event.test(resolver): route_event_context.add_route_event(RouteEventType.BROADCASTER, route_event(time=time, provider=broadcaster, provider_required=True)) def update(self): if self._pending_update: self._pending_update = False self._update() def _is_location_affected(self, constraint, transform, routing_surface): if constraint.geometry is not None and not constraint.geometry.test_transform(transform): return False elif not constraint.is_routing_surface_valid(routing_surface): return False return True def update_broadcasters_one_shot(self, broadcasters): for obj in self._get_all_objects_gen(): object_transform = None routing_surface = obj.routing_surface for broadcaster in broadcasters: if broadcaster.can_affect(obj): constraint = broadcaster.get_constraint() if not constraint.valid: continue if object_transform is None: parent = obj.parent if parent is None: object_transform = obj.transform else: object_transform = parent.transform if self._is_location_affected(constraint, object_transform, routing_surface): broadcaster.apply_broadcaster_effect(obj) broadcaster.remove_broadcaster_effect(obj) if not obj.valid_for_distribution: break def _update(self): try: self._activate_pending_broadcasters() current_broadcasters = set(self.get_broadcasters_gen()) for obj in self._get_all_objects_gen(): object_transform = None is_affected = False for broadcaster in current_broadcasters: if broadcaster.can_affect(obj): constraint = broadcaster.get_constraint() if not constraint.valid: continue if object_transform is None: parent = obj.parent if parent is None: object_transform = obj.transform else: object_transform = parent.transform if self._is_location_affected(constraint, object_transform, obj.routing_surface): broadcaster.apply_broadcaster_effect(obj) if not obj.valid_for_distribution: is_affected = False break is_affected = True if not is_affected: if self._object_cache is not None: self._object_cache.discard(obj) for broadcaster in current_broadcasters: broadcaster.on_processed() finally: self._on_update_callbacks()
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 DaycareService(Service): def __init__(self): self._unavailable_sims = WeakSet() self._global_unavailable_sims = WeakSet() self._daycare_interactions = WeakKeyDictionary() self._excluded_sims = WeakSet() self._nanny_dialog_shown = False def on_sim_reset(self, sim): sim_info = sim.sim_info daycare_interaction = self._daycare_interactions.get(sim_info) if daycare_interaction is not None: del self._daycare_interactions[sim_info] self._apply_daycare_effects_to_sim(sim_info) def get_available_sims_gen(self): for sim in services.sim_info_manager().instanced_sims_gen(allow_hidden_flags=ALL_HIDDEN_REASONS): if sim.sim_info in self._global_unavailable_sims: continue yield sim.sim_info def _apply_daycare_effects_to_sim(self, sim_info): self._excluded_sims.discard(sim_info) sim = services.object_manager().get(sim_info.id) if sim_info.is_baby: sim.empty_baby_state() elif sim_info.is_toddler: daycare_interaction = self._daycare_interactions.get(sim_info) if daycare_interaction is None: aop = AffordanceObjectPair(DaycareTuning.GO_TO_DAYCARE_INTERACTION, None, DaycareTuning.GO_TO_DAYCARE_INTERACTION, None) context = InteractionContext(sim, InteractionContext.SOURCE_SCRIPT, Priority.High, insert_strategy=QueueInsertStrategy.FIRST, must_run_next=True) execute_result = aop.test_and_execute(context) if execute_result: self._daycare_interactions[sim_info] = execute_result.interaction return True def _remove_daycare_effects_from_sim(self, sim_info): sim = services.object_manager().get(sim_info.id) if sim_info.is_baby and sim is not None: sim.enable_baby_state() elif sim_info.is_toddler: daycare_interaction = self._daycare_interactions.pop(sim_info, None) if daycare_interaction is not None: daycare_interaction.cancel(FinishingType.NATURAL, cancel_reason_msg='Daycare no longer necessary.', ignore_must_run=True) if sim_info in self._excluded_sims: self._excluded_sims.discard(sim_info) return False return True def _is_sim_available(self, sim_info, household, current_zone_id): if sim_info.zone_id == household.home_zone_id: if sim_info.zone_id != current_zone_id: if sim_info.career_tracker is None or sim_info.career_tracker.currently_at_work or services.hidden_sim_service().is_hidden(sim_info.id): return False return True elif sim_info not in self._unavailable_sims: return True return False def _start_nanny_service(self, household, sim_infos, callback_fn): def _can_trigger_nanny_service(sim_info): if not sim_info.is_toddler: return False else: daycare_interaction = self._daycare_interactions.get(sim_info) if daycare_interaction is not None: return False return True if not any(_can_trigger_nanny_service(sim_info) for sim_info in sim_infos): callback_fn() return def _on_response(dialog): self._nanny_dialog_shown = False if dialog.accepted: service_npc_service = services.current_zone().service_npc_service service_npc_service.request_service(household, DaycareTuning.NANNY_SERVICE_NPC, from_load=True) else: callback_fn() if self._nanny_dialog_shown or services.current_zone().service_npc_service.is_service_already_in_request_list(household, DaycareTuning.NANNY_SERVICE_NPC): return self._nanny_dialog_shown = True hire_nanny_dialog = DaycareTuning.NANNY_SERVICE_NPC_DIALOG(None) hire_nanny_dialog.show_dialog(additional_tokens=(DaycareTuning.NANNY_SERVICE_NPC.cost_up_front, DaycareTuning.NANNY_SERVICE_NPC.cost_hourly), on_response=_on_response) def _get_running_situation_for_service(self, service_npc): situation_manager = services.get_zone_situation_manager() if situation_manager is not None: for service_npc_situation in situation_manager.get_situations_by_type(service_npc.situation): return service_npc_situation def is_daycare_service_npc_available(self, sim_info=None, household=None): household = services.active_household() if household is None else household if household.home_zone_id == services.current_zone_id(): nanny_situation = self._get_running_situation_for_service(DaycareTuning.NANNY_SERVICE_NPC) if nanny_situation is not None: if sim_info is None: return True service_sim = nanny_situation.service_sim() if service_sim is not None and service_sim.sim_info is sim_info: return True if DaycareTuning.BUTLER_SERVICE_NPC is not None: butler_situation = self._get_running_situation_for_service(DaycareTuning.BUTLER_SERVICE_NPC) if butler_situation is not None: if butler_situation.is_in_childcare_state: if sim_info is None: return True service_sim = butler_situation.service_sim() if service_sim is not None: if service_sim.sim_info is sim_info: return True elif sim_info is None: all_hired_service_npcs = household.get_all_hired_service_npcs() for service_npc in (DaycareTuning.NANNY_SERVICE_NPC, DaycareTuning.BUTLER_SERVICE_NPC): if service_npc is None: continue if service_npc.guid64 in all_hired_service_npcs: return True return False def _is_any_sim_available(self, household): if self.is_daycare_service_npc_available(household=household): return True current_zone_id = services.current_zone_id() return any(self._is_sim_available(sim_info, household, current_zone_id) for sim_info in household.can_live_alone_info_gen()) def _is_everyone_on_vacation(self, household): return all(sim_info.is_in_travel_group() for sim_info in household.can_live_alone_info_gen()) def _enable_daycare_or_nanny_if_necessary(self, household): is_active_household = services.active_household() == household nanny_sim_infos = self.get_sim_infos_for_nanny(household) sent_sim_infos = [] for sim_info in nanny_sim_infos: if not sim_info.trait_tracker.has_trait(DaycareTuning.NANNY_TRAIT_ON_KIDS): sent_sim_infos.append(sim_info) sim_info.add_trait(DaycareTuning.NANNY_TRAIT_ON_KIDS) if self.is_sim_info_at_daycare(sim_info): self.remove_sim_info_from_daycare(sim_info) if sim_info.zone_id != household.home_zone_id: sim_info.inject_into_inactive_zone(household.home_zone_id) if is_active_household and sent_sim_infos: services.client_manager().get_first_client().send_selectable_sims_update() self._show_nanny_notification(household, sent_sim_infos, is_enable=True) if nanny_sim_infos: return if not self._is_any_sim_available(household): current_zone_id = services.current_zone_id() daycare_sim_infos = self.get_sim_infos_for_daycare(household) if not daycare_sim_infos: return def _on_send_to_daycare(): sent_sim_infos = [] for sim_info in daycare_sim_infos: if household.home_zone_id == current_zone_id: self._apply_daycare_effects_to_sim(sim_info) if not self.is_sim_info_at_daycare(sim_info): sent_sim_infos.append(sim_info) sim_info.add_trait(DaycareTuning.DAYCARE_TRAIT_ON_KIDS) if sim_info.zone_id != household.home_zone_id: sim_info.inject_into_inactive_zone(household.home_zone_id) if sim_info.zone_id != current_zone_id: if sim_info.away_action_tracker is not None: sim_info.away_action_tracker.reset_to_default_away_action() if is_active_household: services.client_manager().get_first_client().send_selectable_sims_update() self._show_daycare_notification(household, sent_sim_infos, is_enable=True) if is_active_household and household.home_zone_id == current_zone_id: self._start_nanny_service(household, daycare_sim_infos, _on_send_to_daycare) else: _on_send_to_daycare() def default_away_action(self, sim_info): highest_advertising_value = None highest_advertising_away_action = None for (commodity, away_action) in DaycareTuning.DAYCARE_AWAY_ACTIONS.items(): commodity_instance = sim_info.get_statistic(commodity, add=False) if commodity_instance is None: continue if not away_action.test(sim_info=sim_info, target=None): continue advertising_value = commodity_instance.autonomous_desire if not highest_advertising_value is None: if highest_advertising_value < advertising_value: highest_advertising_value = advertising_value highest_advertising_away_action = away_action highest_advertising_value = advertising_value highest_advertising_away_action = away_action return highest_advertising_away_action def _disable_daycare_or_nanny_if_necessary(self, household, returning_sim_infos=()): returned_children = [] eligible_nanny_count = self.get_number_of_eligible_nanny_sims(household) if eligible_nanny_count and not self._is_everyone_on_vacation(household): sim_infos_for_nanny = self.get_sim_infos_for_nanny(household, check_for_vacation=False) for sim_info in sim_infos_for_nanny: if sim_info.has_trait(DaycareTuning.NANNY_TRAIT_ON_KIDS): sim_info.remove_trait(DaycareTuning.NANNY_TRAIT_ON_KIDS) if sim_info not in returning_sim_infos: returned_children.append(sim_info) self._show_nanny_notification(household, returned_children, is_enable=False) eligible_daycare_count = self.get_number_of_eligible_daycare_sims(household) if eligible_daycare_count and self._is_any_sim_available(household): daycare_sim_infos = list(self.get_sim_infos_for_daycare(household)) for sim_info in tuple(daycare_sim_infos): if not self._remove_daycare_effects_from_sim(sim_info) or sim_info in returning_sim_infos: daycare_sim_infos.remove(sim_info) if self.is_sim_info_at_daycare(sim_info): self.remove_sim_info_from_daycare(sim_info) if not returned_children: self._show_daycare_notification(household, daycare_sim_infos, is_enable=False) if services.active_household() == household: services.client_manager().get_first_client().send_selectable_sims_update() def is_daycare_enabled(self, household): return not self._is_any_sim_available(household) def get_abandoned_toddlers(self, household, sims_infos_to_ignore=()): caretaker_zone_ids = set() offlot_toddlers = set() abandoned_toddlers = [] current_zone_id = services.current_zone_id() for sim_info in household: if sim_info in sims_infos_to_ignore: continue if not sim_info.is_toddler: if sim_info.can_live_alone: caretaker_zone_ids.add(sim_info.zone_id) if sim_info.zone_id == current_zone_id: continue if sim_info.zone_id == sim_info.household.home_zone_id: continue offlot_toddlers.add(sim_info) else: if sim_info.zone_id == current_zone_id: continue if sim_info.zone_id == sim_info.household.home_zone_id: continue offlot_toddlers.add(sim_info) for toddler in offlot_toddlers: if toddler.zone_id not in caretaker_zone_ids: abandoned_toddlers.append(toddler) return abandoned_toddlers def get_sim_infos_for_daycare(self, household): sim_infos_for_daycare = [] for sim_info in household: if not sim_info.is_toddler_or_younger: continue if sim_info.is_pet: continue if sim_info.zone_id != sim_info.household.home_zone_id: continue sim_infos_for_daycare.append(sim_info) sim_infos_for_daycare.extend(self.get_abandoned_toddlers(household)) return sim_infos_for_daycare def get_sim_infos_for_nanny(self, household, check_for_vacation=True): if check_for_vacation and not self._is_everyone_on_vacation(household): return [] sim_infos_for_nanny = [] for sim_info in household: if not sim_info.is_child_or_younger: continue if sim_info.is_pet: continue if sim_info.is_in_travel_group(): continue sim_infos_for_nanny.append(sim_info) sim_infos_for_nanny.extend(self.get_abandoned_toddlers(household)) return sim_infos_for_nanny def get_number_of_eligible_daycare_sims(self, household): return sum(1 for sim_info in household if sim_info.is_toddler_or_younger if not sim_info.is_pet) def get_number_of_eligible_nanny_sims(self, household): return sum(1 for sim_info in household if sim_info.is_child_or_younger if not sim_info.is_pet) def on_sim_spawn(self, sim_info): current_zone = services.current_zone() if not current_zone.is_zone_running: return if sim_info.is_child_or_younger: return household = sim_info.household if household is not None: if household.home_zone_id == current_zone.id: self._unavailable_sims.add(sim_info) self.set_sim_available(sim_info) else: self.set_sim_unavailable(sim_info) def on_loading_screen_animation_finished(self): household = services.active_household() if household is None: return if household.home_zone_id == services.current_zone_id(): returning_sim_infos = [sim_info for sim_info in services.sim_info_manager().get_traveled_to_zone_sim_infos() if sim_info not in self._unavailable_sims] if returning_sim_infos: for sim_info in returning_sim_infos: if sim_info.is_child_or_younger: continue self._unavailable_sims.add(sim_info) for sim_info in returning_sim_infos: if sim_info.is_child_or_younger: continue self.set_sim_available(sim_info, returning_sim_infos=returning_sim_infos) else: self._enable_daycare_or_nanny_if_necessary(household) else: self._enable_daycare_or_nanny_if_necessary(household) else: self._enable_daycare_or_nanny_if_necessary(household) services.get_event_manager().process_event(TestEvent.AvailableDaycareSimsChanged, sim_info=services.active_sim_info()) def refresh_household_daycare_nanny_status(self, sim_info, try_enable_if_selectable_toddler=False): household = services.active_household() if household is not None: try_enable = try_enable_if_selectable_toddler and (sim_info.is_toddler and sim_info.is_selectable) if not try_enable and (self.is_anyone_with_nanny(household) or self.is_anyone_at_daycare(household)): self._disable_daycare_or_nanny_if_necessary(household) else: self._enable_daycare_or_nanny_if_necessary(household) services.get_event_manager().process_event(TestEvent.AvailableDaycareSimsChanged, sim_info=sim_info) def refresh_daycare_status(self, baby): household = baby.household if household is None: return if self.is_daycare_enabled(household): self._apply_daycare_effects_to_sim(baby) else: self._remove_daycare_effects_from_sim(baby) def exclude_sim_from_daycare(self, sim_info): self._excluded_sims.add(sim_info) def is_sim_info_at_daycare(self, sim_info): return sim_info.has_trait(DaycareTuning.DAYCARE_TRAIT_ON_KIDS) def remove_sim_info_from_daycare(self, sim_info): sim_info.remove_trait(DaycareTuning.DAYCARE_TRAIT_ON_KIDS) def is_anyone_at_daycare(self, household): return any(sim_info.has_trait(DaycareTuning.DAYCARE_TRAIT_ON_KIDS) for sim_info in household if sim_info.is_toddler_or_younger) def is_anyone_with_nanny(self, household): return any(sim_info.has_trait(DaycareTuning.NANNY_TRAIT_ON_KIDS) for sim_info in household if sim_info.is_child_or_younger) def set_sim_available(self, sim_info, returning_sim_infos=()): household = sim_info.household daycare_previously_enabled = self.is_anyone_at_daycare(household) nanny_previously_enabled = False if daycare_previously_enabled else self.is_anyone_with_nanny(household) self._unavailable_sims.discard(sim_info) if daycare_previously_enabled or nanny_previously_enabled: self._disable_daycare_or_nanny_if_necessary(household, returning_sim_infos=returning_sim_infos) services.get_event_manager().process_event(TestEvent.AvailableDaycareSimsChanged, sim_info=sim_info) def set_sim_globally_available(self, sim_info): self._global_unavailable_sims.discard(sim_info) def set_sim_unavailable(self, sim_info): household = sim_info.household nanny_previously_disabled = not self.is_anyone_with_nanny(household) daycare_previously_disabled = True if not nanny_previously_disabled else not self.is_anyone_at_daycare(household) self._unavailable_sims.add(sim_info) if nanny_previously_disabled or daycare_previously_disabled: self._enable_daycare_or_nanny_if_necessary(household) services.get_event_manager().process_event(TestEvent.AvailableDaycareSimsChanged, sim_info=sim_info) def set_sim_globally_unavailable(self, sim_info): self._global_unavailable_sims.add(sim_info) def _show_nanny_notification(self, household, sim_infos, is_enable=False): if sim_infos: if is_enable: if len(sim_infos) == 1: self._show_notification(DaycareTuning.SEND_CHILD_TO_NANNY_NOTIFICATION_SINGLE, sim_infos, household) else: self._show_notification(DaycareTuning.SEND_CHILD_TO_NANNY_NOTIFICATION_MULTIPLE, sim_infos, household) elif len(sim_infos) == 1: self._show_notification(DaycareTuning.BRING_CHILD_BACK_FROM_NANNY_NOTIFICATION_SINGLE, sim_infos, household) else: self._show_notification(DaycareTuning.BRING_CHILD_BACK_FROM_NANNY_NOTIFICATION_MULTIPLE, sim_infos, household) def _show_daycare_notification(self, household, sim_infos, is_enable=False): if sim_infos: if is_enable: if len(sim_infos) == 1: self._show_notification(DaycareTuning.SEND_BABY_TO_DAYCARE_NOTIFICATION_SINGLE_BABY, sim_infos, household) else: self._show_notification(DaycareTuning.SEND_BABY_TO_DAYCARE_NOTIFICATION_MULTIPLE_BABIES, sim_infos, household) elif len(sim_infos) == 1: self._show_notification(DaycareTuning.BRING_BABY_BACK_FROM_DAYCARE_NOTIFICATION_SINGLE_BABY, sim_infos, household) else: self._show_notification(DaycareTuning.BRING_BABY_BACK_FROM_DAYCARE_NOTIFICATION_MULTIPLE_BABIES, sim_infos, household) def _show_notification(self, notification, sim_infos, household, resolver=None): if not services.current_zone().is_zone_running: return if not household.is_active_household: return additional_token = sim_infos if len(sim_infos) > 1 else sim_infos[0] dialog = notification(None, resolver) dialog.show_dialog(additional_tokens=(additional_token,)) def send_active_household_toddlers_home(self): active_household = services.active_household() if active_household is None: return instanced_toddlers = [sim for sim in active_household.instanced_sims_gen() if sim.sim_info.is_toddler] for toddler in instanced_toddlers: interaction_context = InteractionContext(toddler, InteractionContext.SOURCE_SCRIPT, Priority.Critical) toddler.push_super_affordance(TravelTuning.GO_HOME_INTERACTION, None, interaction_context)
class MasterController(sims4.service_manager.Service): get_next_id = UniqueIdGenerator() def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._enabled = True self._processing = False self._reset_in_progress = False self._last_work_timestamps = {} self._sims = set() self._active_work = {} self._denied_sims = OrderedDict() self._global_required_resources = WeakSet() self._gsi_entry = None self._gsi_log_entries = None def stop(self): self._remove_all_sims() if self._sims: logger.error('Sims {} should be empty. MC logic error.', self._sims, owner='mduke') self._sims.clear() if self._active_work: logger.error('Active Work {} should be empty. MC logic error.', self._active_work, owner='mduke') self._active_work.clear() if self._denied_sims: logger.error('Denied Sims {} should be empty. MC logic error.', self._denied_sims, owner='mduke') self._denied_sims.clear() @property def timeline(self): return services.time_service().sim_timeline def remove_all_sims_and_disable_on_teardown(self): self._enabled = False self._remove_all_sims() def _remove_all_sims(self): for sim in tuple(self._sims): self.remove_sim(sim) def add_sim(self, sim): logger.assert_raise( self._enabled == True, 'Attempting to add a sim to the master controller when it is not enabled.', owner='sscholl') self._sims.add(sim) self.set_timestamp_for_sim_to_now(sim) self._process(sim) def added_sims(self): return list(self._sims) def remove_sim(self, sim): self._last_work_timestamps.pop(sim, None) self._sims.discard(sim) del self._denied_sims[sim] if sim in self._denied_sims and sim.is_sim: sim.queue.on_head_changed.remove(self._process) def reset_timestamp_for_sim(self, sim): self._last_work_timestamps[sim] = 0 def set_timestamp_for_sim_to_now(self, sim): self._last_work_timestamps[sim] = self.get_next_id() def on_reset_sim(self, sim, reset_reason): self._active_work.pop(sim, None) def on_reset_begin(self): self._reset_in_progress = True def on_reset_end(self, *sims): self._reset_in_progress = False self._process(*sims) def add_interdependent_reset_records(self, sim, records): work_entry = self._active_work.get(sim, None) if work_entry is None: return records for other_sim in work_entry.resources: if other_sim is not sim: records.append( ResetRecord(other_sim, ResetReason.RESET_EXPECTED, sim, 'Work entry resource:{}'.format(work_entry))) def add_global_lock(self, resource): self._global_required_resources.add(resource) def remove_global_lock(self, resource): self._global_required_resources.discard(resource) def _process_work_entry(self, sim, work_entry, requested_sims, requested_resources): all_free = True must_run = not work_entry.cancelable immediate_cancels = [] if work_entry.additional_resources: for additional_resource in work_entry.additional_resources: if additional_resource in requested_resources: all_free = False break else: requested_resources.update(work_entry.additional_resources) for required_sim in work_entry.resources: self._gsi_add_log_entry( sim, 'PROCESS_WORK_ENTRY', 'Sim Resource: {}: testing if valid resource', required_sim) if required_sim not in self._sims: logger.error( 'Attempting to require a resource ({}) that is not managed by the MasterController.', required_sim) self._gsi_add_log_entry( sim, 'PROCESS_WORK_ENTRY', 'Denied because requested Sim not managed by the MC: {}.', required_sim) all_free = False if required_sim in requested_sims: all_free = False self._gsi_add_log_entry(sim, 'PROCESS_WORK_ENTRY', 'Already Requested') else: if required_sim in self._active_work: self._gsi_add_log_entry( sim, 'PROCESS_WORK_ENTRY', 'Sim Resource has Active Work: {} - ', str(self._active_work[required_sim])) if not must_run: all_free = False self._gsi_add_log_entry(sim, 'PROCESS_WORK_ENTRY', 'Work Entry is not must run') else: required_work_entry = self._active_work[required_sim] if not required_work_entry.cancelable: all_free = False requested_sims.add(required_sim) self._gsi_add_log_entry( sim, 'PROCESS_WORK_ENTRY', 'Sim Resource has work entry and cannot be canceled immediately' ) if not required_sim.is_sim: required_sim.on_requested_as_resource( work_entry) self._gsi_add_log_entry( sim, 'PROCESS_WORK_ENTRY', 'Sim Resource has work entry that can be canceled added to immediate_cancels' ) immediate_cancels.append( (required_sim, required_work_entry)) self._gsi_add_log_entry( sim, 'PROCESS_WORK_ENTRY', 'Sim Resource is free') else: self._gsi_add_log_entry( sim, 'PROCESS_WORK_ENTRY', 'Sim Resource has work entry that can be canceled added to immediate_cancels' ) immediate_cancels.append( (required_sim, required_work_entry)) self._gsi_add_log_entry(sim, 'PROCESS_WORK_ENTRY', 'Sim Resource is free') self._gsi_add_log_entry(sim, 'PROCESS_WORK_ENTRY', 'Sim Resource is free') if all_free: for (required_sim, required_work_entry) in immediate_cancels: self._gsi_add_log_entry(sim, 'PROCESS_WORK_ENTRY', '{} work entry canceled called.', required_sim) required_work_entry.cancel() if required_sim in self._active_work: del self._active_work[required_sim] for required_sim in work_entry.resources: self._gsi_add_log_entry(sim, 'PROCESS_WORK_ENTRY', 'work entry added to sim{}.', required_sim) self._active_work[required_sim] = work_entry requested_sims.add(required_sim) return True if sim not in self._denied_sims: self._gsi_add_log_entry(sim, 'PROCESS_WORK_ENTRY', 'Entry added to denied sims.') if sim.is_sim: sim.queue.on_head_changed.append(self._process) self._denied_sims[sim] = work_entry if must_run: requested_sims.update(work_entry.resources) self._gsi_add_log_entry(sim, 'PROCESS_WORK_ENTRY', 'work entry NOT added to sim.') return False def _sorted_sims(self, sims): return sorted( sims, key=lambda sim: (-sim.get_next_work_priority(), self._last_work_timestamps[sim])) def _process(self, *sims): if not self._enabled or self._processing or self._reset_in_progress: return self._processing = True sims_filtered = list(sims) try: requested_sims = set(self._global_required_resources) requested_resources = set() for work_entry in self._active_work.values(): if work_entry.additional_resources: requested_resources.update(work_entry.additional_resources) new_work_accepted = [] self._gsi_entry_initialize(*sims) self._gsi_add_sim_time_line_for_sims(sims, 'Start', 'Begin processing') sims_filtered = [ sim for sim in sims if sim not in self._denied_sims if sim in self._sims ] for sim in self._sorted_sims( itertools.chain(self._denied_sims, sims_filtered)): self._gsi_add_log_entry(sim, 'PROCESS', '----- START -----') if sim not in self._sims: continue if sim in requested_sims: continue existing_entry = self._active_work.get(sim) if existing_entry is not None and not existing_entry.cancelable: continue if sim in self._denied_sims and sim.is_sim: sim.queue.on_head_changed.remove(self._process) try: work_request = sim.get_next_work() finally: if sim in self._denied_sims and sim.is_sim: sim.queue.on_head_changed.append(self._process) if work_request.work_element is None: self._gsi_add_log_entry(sim, 'PROCESS', 'No Work Element') else: work_entry = WorkEntry( work_element=work_request.work_element, resources=work_request.required_sims, additional_resources=work_request.additional_resources, owner=sim, master_controller=self, on_accept=work_request.on_accept, debug_name=work_request._debug_name) self._gsi_add_sim_time_line_for_sim( sim, 'Create', 'Work Entry Created') self._gsi_add_log_entry( sim, 'PROCESS', 'Work Entry Created: required_sims:{}', str(work_request.required_sims)) if self._process_work_entry(sim, work_entry, requested_sims, requested_resources): if sim in self._denied_sims: if sim.is_sim: sim.queue.on_head_changed.remove(self._process) del self._denied_sims[sim] new_work_accepted.append((sim, work_entry)) if work_request.set_work_timestamp: self.set_timestamp_for_sim_to_now(sim) for (sim, work_entry) in new_work_accepted: self._gsi_add_log_entry(sim, 'PROCESS', 'Work Entry Start Called: {}', work_entry) self._gsi_add_sim_time_line_for_sim(sim, 'Start', 'Work Entry Started') work_entry.start() for sim in self._sims: if sim not in self._active_work: (work_element_idle, cancel_callable) = sim.get_idle_element() if work_element_idle is not None: work_entry = WorkEntry(work_element=work_element_idle, cancel_callable=cancel_callable, resources=(sim, ), owner=sim, master_controller=self) self._active_work[sim] = work_entry self._gsi_add_log_entry( sim, 'PROCESS', 'No active work - run idle behavior') if sim not in self._denied_sims and sim.is_sim: sim.queue.on_head_changed.append(self._process) self._denied_sims[sim] = work_entry work_entry.start() self._gsi_entry_finalize() self._processing = False except: logger.exception( 'Exception while processing the Master Controller.') finally: if self._processing: self._processing = False services.get_reset_and_delete_service().trigger_batch_reset( sims_filtered, ResetReason.RESET_ON_ERROR, None, 'Exception in _process in the MasterController.') def _gsi_create_active_work_entry(self): gsi_active_work = [] for (sim, work_entry) in self._active_work.items(): entry = {'sim': str(sim), 'work_entry': str(work_entry)} gsi_active_work.append(entry) return gsi_active_work def _gsi_entry_initialize(self, *sims_being_processed): if gsi_handlers.master_controller_handlers.archiver.enabled: self._gsi_entry = { 'sims_with_active_work': str([str(sim) for sim in self._active_work.keys()]), 'last_time_stamp': str(self._last_work_timestamps) } self._gsi_entry[ 'active_work_start'] = self._gsi_create_active_work_entry() self._gsi_log_entries = [] def _gsi_add_log_entry(self, sim, tag, log_message, *log_message_args): if gsi_handlers.master_controller_handlers.archiver.enabled: entry = { 'sim': str(sim) if sim is not None else '', 'tag': tag, 'log': log_message.format(*log_message_args) } self._gsi_log_entries.append(entry) def _gsi_add_sim_time_line_for_sim(self, sim, status, log_message): if gsi_handlers.sim_timeline_handlers.archiver.enabled: gsi_handlers.sim_timeline_handlers.archive_sim_timeline( sim, 'MasterController', status, log_message) def _gsi_add_sim_time_line_for_sims(self, sims, status, log_message): if gsi_handlers.sim_timeline_handlers.archiver.enabled: for sim in sims: gsi_handlers.sim_timeline_handlers.archive_sim_timeline( sim, 'MasterController', status, log_message) def _gsi_add_sim_time_line_entry(self, work_entry, status, log_message): if gsi_handlers.sim_timeline_handlers.archiver.enabled: for resource in work_entry.resources: if not resource.is_sim: continue if resource is work_entry.owner: message_to_log = '{}: as owner: {}'.format( log_message, resource) else: message_to_log = '{} as resource: {}'.format( log_message, resource, log_message) gsi_handlers.sim_timeline_handlers.archive_sim_timeline( resource, 'MasterController', status, message_to_log) def _gsi_entry_finalize(self): if gsi_handlers.master_controller_handlers.archiver.enabled: self._gsi_entry['sims_with_active_work_after'] = str( [str(sim) for sim in self._active_work.keys()]) self._gsi_entry['last_time_stamp_end'] = str( self._last_work_timestamps) self._gsi_entry[ 'active_work_end'] = self._gsi_create_active_work_entry() self._gsi_entry['Log'] = self._gsi_log_entries gsi_handlers.master_controller_handlers.archive_master_controller_entry( self._gsi_entry) self._gsi_entry = None self._gsi_log_entries = None
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 ReservationMixin: def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._reservation_handlers = () self._on_reservation_handlers_changed = None self._reservation_clobberers = None @property def in_use(self): if self._reservation_handlers: return True return False @property def self_or_part_in_use(self): if self._reservation_handlers: return True elif self.parts: return any(part.in_use for part in self.parts) return False def in_use_by(self, sim, owner=None): for handler in self._reservation_handlers: if handler.sim is not sim: continue if owner is not None and handler.reservation_interaction is not owner: continue return True return False def get_users(self, sims_only=False): users = set(handler.sim for handler in self._reservation_handlers if not sims_only or handler.sim.is_sim) if self.parts: for part in self.parts: users |= part.get_users(sims_only=sims_only) return frozenset(users) def get_reservation_handler(self, sim, **kwargs): reservation_type = ReservationHandlerBasic if not self.parts else ReservationHandlerAllParts return reservation_type(sim, self, **kwargs) def get_use_list_handler(self, sim, **kwargs): return ReservationHandlerUseList(sim, self, **kwargs) def may_reserve(self, reserver, *_, reservation_handler=None, _from_reservation_call=False, **kwargs): if reservation_handler is None: reservation_handler = self.get_reservation_handler(reserver) reserve_result = reservation_handler.may_reserve_internal(**kwargs) if gsi_handlers.sim_handlers_log.sim_reservation_archiver.enabled and reserver.is_sim: reserve_result_str = '{}: {}'.format( 'reserve' if not _from_reservation_call else 'may_reserve', reserve_result) gsi_handlers.sim_handlers_log.archive_sim_reservation( reservation_handler, reserve_result_str) return reserve_result def add_reservation_handler(self, reservation_handler): if isinstance(self._reservation_handlers, tuple): self._reservation_handlers = WeakSet() self._reservation_handlers.add(reservation_handler) if self._on_reservation_handlers_changed: self._on_reservation_handlers_changed(user=reservation_handler.sim, added=True) def get_reservation_handlers(self): return tuple(self._reservation_handlers) def remove_reservation_handler(self, reservation_handler): if not self._reservation_handlers: return self._reservation_handlers.discard(reservation_handler) if self._on_reservation_handlers_changed: self._on_reservation_handlers_changed(user=reservation_handler.sim, added=False) def add_reservation_clobberer(self, reservation_holder, reservation_clobberer): if self._reservation_clobberers is None: self._reservation_clobberers = defaultdict(WeakSet) self._reservation_clobberers[reservation_holder].add( reservation_clobberer) def is_reservation_clobberer(self, reservation_holder, reservation_clobberer): if self._reservation_clobberers is None: return False if reservation_holder not in self._reservation_clobberers: return False return reservation_clobberer in self._reservation_clobberers[ reservation_holder] def remove_reservation_clobberer(self, reservation_holder, reservation_clobberer): if self._reservation_clobberers is None: return if reservation_holder not in self._reservation_clobberers: return self._reservation_clobberers[reservation_holder].discard( reservation_clobberer) if not self._reservation_clobberers[reservation_holder]: del self._reservation_clobberers[reservation_holder] if not self._reservation_clobberers: self._reservation_clobberers = None def on_reset_get_interdependent_reset_records(self, reset_reason, reset_records): super().on_reset_get_interdependent_reset_records( reset_reason, reset_records) relevant_sims = self.get_users(sims_only=True) for sim in relevant_sims: if self.reset_reason() == ResetReason.BEING_DESTROYED: reset_records.append( ResetRecord(sim, ResetReason.RESET_EXPECTED, self, 'In use list of object being destroyed.')) else: body_target_part_owner = sim.posture_state.body.target if body_target_part_owner is not None: if body_target_part_owner.is_part: body_target_part_owner = body_target_part_owner.part_owner transition_controller = sim.queue.transition_controller if not body_target_part_owner is self: if not transition_controller is None: if not transition_controller.will_derail_if_given_object_is_reset( self): reset_records.append( ResetRecord(sim, ResetReason.RESET_EXPECTED, self, 'Transitioning To or In.')) reset_records.append( ResetRecord(sim, ResetReason.RESET_EXPECTED, self, 'Transitioning To or In.')) def usable_by_transition_controller(self, transition_controller): if transition_controller is None: return False required_sims = transition_controller.interaction.required_sims() targets = (self, ) + tuple( self.get_overlapping_parts()) if self.is_part else (self, ) for reservation_handler in itertools.chain.from_iterable( target.get_reservation_handlers() for target in targets): if reservation_handler.sim in required_sims: continue reservation_interaction = reservation_handler.reservation_interaction if reservation_interaction is None: continue if reservation_interaction.priority >= transition_controller.interaction.priority: return False if transition_controller.interaction.priority <= Priority.Low: return False return True def register_on_use_list_changed(self, callback): if self._on_reservation_handlers_changed is None: self._on_reservation_handlers_changed = CallableList() self._on_reservation_handlers_changed.append(callback) def unregister_on_use_list_changed(self, callback): if callback in self._on_reservation_handlers_changed: self._on_reservation_handlers_changed.remove(callback) if not self._on_reservation_handlers_changed: self._on_reservation_handlers_changed = None
class CaregiverSituation(SituationComplexCommon): CAREGIVER_EVENTS = (TestEvent.SituationStarted, TestEvent.AvailableDaycareSimsChanged) INSTANCE_TUNABLES = { 'caregiver_data': TunableTuple( description= '\n The relationship bits to apply to Sims.\n ', caregiver_bit=TunableReference( description= "\n The bit that is applied to Sims that are the situation owner's\n Sim's caregiver. This is, for example, a bit on an adult\n targeting a toddler.\n ", manager=services.get_instance_manager( sims4.resources.Types.RELATIONSHIP_BIT)), caregiver_job=SituationJob.TunableReference( description= '\n The situation job that caregivers are assigned when in this situation.\n ' ), caregiver_rolestate=RoleState.TunableReference( description= '\n The role state that caregivers are assigned when in this situation.\n ' ), care_dependent_bit=TunableReference( description= '\n The bit that is applied to Sims that are the situation owner\n This is, for example, a bit on a toddler targeting an adult.\n ', manager=services.get_instance_manager( sims4.resources.Types.RELATIONSHIP_BIT))), 'caregiver_relationships': TunableSet( description= '\n A list of bits that make Sims primary caregivers. If any Sim with\n any of these bits is instantiated and living in the same household \n as the care dependent, they are considered caregivers.\n \n If no primary caregiver exists, and no caregiver service exists,\n active TYAE Sims are made caregivers.\n ', tunable=TunableReference(manager=services.get_instance_manager( sims4.resources.Types.RELATIONSHIP_BIT), pack_safe=True)) } REMOVE_INSTANCE_TUNABLES = Situation.NON_USER_FACING_REMOVE_INSTANCE_TUNABLES def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._pending_caregivers = WeakSet() @classmethod def default_job(cls): pass @classmethod def _get_tuned_job_and_default_role_state_tuples(cls): return ((cls.caregiver_data.caregiver_job, cls.caregiver_data.caregiver_rolestate), ) def _is_valid_caregiver(self, care_dependent, caregiver, ignore_zone=False): if not ignore_zone and care_dependent.zone_id != caregiver.zone_id: return False if caregiver.is_toddler_or_younger: return False if caregiver.is_pet: return False if care_dependent.household_id == caregiver.household_id and any( caregiver.relationship_tracker.has_bit(care_dependent.sim_id, rel_bit) for rel_bit in self.caregiver_relationships): return True else: daycare_service = services.daycare_service() if daycare_service is not None and daycare_service.is_daycare_service_npc_available( sim_info=caregiver, household=care_dependent.household): return True return False def _update_caregiver_status(self): care_dependent = self._guest_list.host_sim if care_dependent is None: return if care_dependent.household is None: return if care_dependent.is_being_destroyed: return available_sims = tuple( sim_info for sim_info in services.daycare_service().get_available_sims_gen()) current_caregivers = set(self._situation_sims) for sim in current_caregivers: self._pending_caregivers.discard(sim) eligible_caregivers = set( sim_info for sim_info in available_sims if self._is_valid_caregiver(care_dependent, sim_info)) if not eligible_caregivers: eligible_caregivers = set( sim_info for sim_info in care_dependent.household.can_live_alone_info_gen() if sim_info in available_sims) for sim in self._pending_caregivers: eligible_caregivers.discard(sim.sim_info) for potential_caregiver in tuple(eligible_caregivers): sim = potential_caregiver.get_sim_instance( allow_hidden_flags=ALL_HIDDEN_REASONS) if sim is None or sim.is_being_destroyed: eligible_caregivers.discard(potential_caregiver) else: if sim in current_caregivers: continue self.invite_sim_to_job(sim, job=self.caregiver_data.caregiver_job) self._pending_caregivers.add(sim) care_dependent.relationship_tracker.add_relationship_bit( potential_caregiver.sim_id, self.caregiver_data.care_dependent_bit) potential_caregiver.relationship_tracker.add_relationship_bit( care_dependent.sim_id, self.caregiver_data.caregiver_bit) for sim in tuple(current_caregivers): if sim.sim_info not in eligible_caregivers: self._remove_caregiver_rel_bits(care_dependent, sim.sim_info) self.remove_sim_from_situation(sim) current_caregivers.discard(sim) def _remove_caregiver_rel_bits(self, care_dependent, other_sim_info=None): if other_sim_info is not None: care_dependent.relationship_tracker.remove_relationship_bit( other_sim_info.id, self.caregiver_data.care_dependent_bit) other_sim_info.relationship_tracker.remove_relationship_bit( care_dependent.id, self.caregiver_data.caregiver_bit) else: for relationship in care_dependent.relationship_tracker: other_sim_id = relationship.get_other_sim_id( care_dependent.sim_id) relationship.remove_bit(care_dependent.sim_id, other_sim_id, self.caregiver_data.care_dependent_bit) relationship.remove_bit(other_sim_id, care_dependent.sim_id, self.caregiver_data.caregiver_bit) def get_care_dependent_if_last_caregiver(self, sim_info, excluding_interaction_types=None): care_dependent = self._guest_list.host_sim if care_dependent.household.home_zone_id == services.current_zone_id(): return if not care_dependent.relationship_tracker.has_relationship( sim_info.id): return for relationship in care_dependent.relationship_tracker: if relationship.get_other_sim_info( care_dependent.sim_id) is sim_info: if not relationship.has_bit( care_dependent.sim_id, self.caregiver_data.care_dependent_bit): return if relationship.has_bit( care_dependent.sim_id, self.caregiver_data.care_dependent_bit): if excluding_interaction_types is not None: other_sim = relationship.get_other_sim( care_dependent.sim_id) if other_sim is None: continue if other_sim.has_any_interaction_running_or_queued_of_types( excluding_interaction_types): continue else: return elif relationship.has_bit(care_dependent.sim_id, self.caregiver_data.care_dependent_bit): if excluding_interaction_types is not None: other_sim = relationship.get_other_sim( care_dependent.sim_id) if other_sim is None: continue if other_sim.has_any_interaction_running_or_queued_of_types( excluding_interaction_types): continue else: return return care_dependent def start_situation(self): self._update_caregiver_status() services.get_event_manager().register(self, self.CAREGIVER_EVENTS) return super().start_situation() def _destroy(self): services.get_event_manager().unregister(self, self.CAREGIVER_EVENTS) care_dependent = self._guest_list.host_sim self._remove_caregiver_rel_bits(care_dependent) super()._destroy() def handle_event(self, sim_info, event, resolver): super().handle_event(sim_info, event, resolver) if event in self.CAREGIVER_EVENTS: self._update_caregiver_status()
class GameObject(ClientObjectMixin, ReservationMixin, ScriptObject, reset.ResettableObjectMixin): INSTANCE_TUNABLES = {'_transient_tuning': Tunable(description='\n If transient the object will always be destroyed and never put down.\n ', tunable_type=bool, default=False, tuning_filter=FilterTag.EXPERT_MODE, display_name='Transient'), 'additional_interaction_constraints': TunableList(description='\n A list of constraints that must be fulfilled in order to run the \n linked affordances. This should only be used when the same \n affordance uses different constraints based on the object.\n ', tunable=TunableTuple(constraint=TunableConstraintVariant(description='\n A constraint that must be fulfilled in order to interact with this object.\n '), affordance_links=TunableAffordanceFilterSnippet()), tuning_filter=FilterTag.EXPERT_MODE), 'autonomy_modifiers': TunableList(description='\n List of autonomy modifiers that will be applied to the tuned\n participant type. These can be used to tune object variations.\n ', tunable=TunableAutonomyModifier(locked_args={'commodities_to_add': (), 'score_multipliers': frozendict(), 'provided_affordance_compatibility': None, 'super_affordance_suppression_mode': autonomy.autonomy_modifier.SuperAffordanceSuppression.AUTONOMOUS_ONLY, 'suppress_self_affordances': False, 'only_scored_static_commodities': None, 'only_scored_stats': None, 'relationship_multipliers': None})), 'set_ico_as_carry_target': Tunable(description="\n Whether or not the crafting process should set the carry target\n to be the ICO. Example Usage: Sheet Music has this set to false\n because the sheet music is in the Sim's inventory and the Sim needs\n to carry the guitar/violin. This is a tunable on game object\n because the ICO in the crafting process can be any game object.\n ", tunable_type=bool, default=True), 'supported_posture_types': TunablePostureTypeListSnippet(description='\n The postures supported by this part. If empty, assumes all postures \n are supported.\n '), '_add_to_posture_graph_if_parented': Tunable(description="\n Whether or not the object should be added to the posture graph if it\n is parented to an object that isn't a surface. i.e. chairs that\n should be seatable when slotted into a campfire (which isn't a surface)\n ", tunable_type=bool, default=False), 'allow_preroll_multiple_targets': Tunable(description='\n When checked allows multiple sims to target this object during \n preroll autonomy. If not checked then the default preroll behavior\n will happen.\n \n The default setting is to only allow each target to be targeted\n once during preroll. However it makes sense in certain cases where\n multiple sims can use the same object at the same time to allow\n multiple targets.\n ', tunable_type=bool, default=False), 'icon_override': OptionalTunable(description='\n If enabled, the icon that will be displayed in the UI for this object.\n This does not override the build/buy icon, which can be overriden\n through the catalog.\n ', tunable=TunableResourceKey(tuning_group=GroupNames.UI, resource_types=sims4.resources.CompoundTypes.IMAGE)), 'flammable_area': TunableFlammableAreaVariant(description='\n How the object defines its area of flammability. This is used \n by the fire service to build the quadtree of flammable objects.\n '), '_provided_mobile_posture_affordances': OptionalTunable(description="\n If enabled, this object will add these postures to the posture\n graph. We need to do this for mobile postures that have no body\n target and we don't intend on them ever being included in searches\n for getting from one place to another without this object somewhere\n on the lot.\n ", tunable=TunableSet(description='\n The set of mobile posture providing interactions we want this\n object to provide.\n ', tunable=TunableReference(description='\n The posture providing interaction we want to add to the\n posture graph when this object is instanced.\n ', manager=services.affordance_manager(), pack_safe=True, class_restrictions='SuperInteraction'), minlength=1)), 'recycling_data': TunableTuple(description='\n Recycling information for this object.\n ', recycling_values=TunableMapping(description='\n Maps a buck type to the recycled value for this object.\n ', key_type=TunableEnumEntry(tunable_type=BucksType, default=BucksType.INVALID, invalid_enums=BucksType.INVALID, pack_safe=True), key_name='Bucks Type', value_type=TunableRange(description='\n Object multiplier for this buck type.\n ', tunable_type=float, default=1.0, minimum=0.0), value_name='Value'), recycling_loot=TunableList(description='\n Loot Actions that will be given when the object is recycled.\n SingleActorAndObjectResolver will be used where actor is specified\n by subject, and object is the object being recycled.\n ', tunable=TunableReference(description='\n A loot action applied.\n ', manager=services.get_instance_manager(sims4.resources.Types.ACTION), pack_safe=True))), 'tests_to_bypass_utility_requirement': TunableMapping(description='\n A mapping of utility types to tunable test sets. \n ', key_type=TunableEnumEntry(tunable_type=Utilities, default=Utilities.POWER), value_type=TunableTestSet(description='\n A test set to run when the object is the target of a\n recipe or interaction and the utility required for that recipe or \n interaction is absent. If at least one test group passes, \n then any interaction or recipe that requires the utility\n will be allowed to run despite the absence of the utility. \n \n ORs of ANDs.\n '))} @classmethod def _verify_tuning_callback(cls): if cls._provided_mobile_posture_affordances is not None: for affordance in cls._provided_mobile_posture_affordances: if affordance.provided_posture_type is None: logger.error("{} provides posture affordance {} but it doesn't provide a posture.", cls, affordance, owner='rmccord') elif not affordance.provided_posture_type.unconstrained: logger.error('{} provides posture affordance {} but the provided posture is not unconstrained and therefore requires a body target.', cls, affordance, owner='rmccord') elif affordance in PostureGraphService.POSTURE_PROVIDING_AFFORDANCES: logger.error('{} provides posture affordance {} but this is already provided by the posture graph in Posture Providing Affordances.', cls, affordance, owner='rmccord') def __init__(self, definition, **kwargs): super().__init__(definition, **kwargs) self._on_location_changed_callbacks = None self._transient = None self._created_constraints = None self._created_constraints_dirty = True self._household_owner_id = None self.new_in_inventory = True self.is_new_object = False self._provided_surface = UNSET zone = services.current_zone() account_id = build_buy.get_user_in_build_buy(zone.id) if account_id is not None: self.set_household_owner_id(zone.lot.owner_household_id) self.set_post_bb_fixup_needed() zone.set_to_fixup_on_build_buy_exit(self) self._hidden_flags = 0 self._local_tags = None self._persisted_tags = None self._is_surface = None self._build_buy_use_flags = 0 self._scheduled_elements = None self._work_locks = WeakSet() self._on_hidden_or_shown_callbacks = None def add_work_lock(self, handle): self._work_locks.add(handle) def remove_work_lock(self, handle): self._work_locks.discard(handle) @property def has_work_locks(self): if self._work_locks: return True return False @property def is_fire_related_object(self): if self.is_sim: return False return self.fire_retardant or self.flammable @constproperty def is_valid_for_height_checks(): return True def has_tag(self, tag): if self._local_tags and tag in self._local_tags: return True if self._persisted_tags and tag in self._persisted_tags: return True return self.definition.has_build_buy_tag(tag) def has_any_tag(self, tags): return any(self.has_tag(tag) for tag in tags) def get_tags(self): tags = frozenset(self.definition.build_buy_tags) if self._local_tags: tags |= self._local_tags return tags def append_tags(self, tag_set, persist=False): if self.manager is not None: self.manager.add_tags_and_object_to_cache(tag_set, self) if self._local_tags: self._local_tags = self._local_tags | tag_set else: self._local_tags = tag_set if persist: if self._persisted_tags: self._persisted_tags = self._persisted_tags | tag_set else: self._persisted_tags = tag_set def get_icon_info_data(self): return IconInfoData(obj_instance=self, obj_def_id=self.definition.id, obj_geo_hash=self.geometry_state, obj_material_hash=self.material_hash, obj_name=LocalizationHelperTuning.get_object_name(self)) @property def catalog_name(self): return get_object_catalog_name(self.definition.id) @property def catalog_description(self): return get_object_catalog_description(self.definition.id) def is_routing_surface_overlapped_at_position(self, position): routing_surface = self.provided_routing_surface if routing_surface is not None: (_, object_id) = services.terrain_service.terrain_object().get_routing_surface_height_and_surface_object_at(position.x, position.z, routing_surface) if object_id == self.id: return False return True @property def provided_routing_surface(self): if self._provided_surface is UNSET: self._provided_surface = None if self.routing_surface is not None: if self.has_component(FOOTPRINT_COMPONENT): if placement.has_object_surface_footprint(self.get_footprint()): self._provided_surface = routing.SurfaceIdentifier(services.current_zone_id(), self.routing_surface.secondary_id, routing.SurfaceType.SURFACETYPE_OBJECT) return self._provided_surface def get_icon_override(self): for icon_override in self._icon_override_gen(): if icon_override is not None: return icon_override @forward_to_components_gen def _icon_override_gen(self): if self.icon_override is not None: yield self.icon_override @forward_to_components def populate_localization_token(self, token): self.definition.populate_localization_token(token) def is_hidden(self, allow_hidden_flags=0): if int(self._hidden_flags) & ~int(allow_hidden_flags): return True return False def has_hidden_flags(self, hidden_flags): if int(self._hidden_flags) & int(hidden_flags): return True return False def hide(self, hidden_reasons_to_add): self._hidden_flags = self._hidden_flags | hidden_reasons_to_add if self._on_hidden_or_shown_callbacks is not None: self._on_hidden_or_shown_callbacks(self, hidden_reasons_to_add, added=True) def show(self, hidden_reasons_to_remove): self._hidden_flags = self._hidden_flags & ~hidden_reasons_to_remove if self._on_hidden_or_shown_callbacks is not None: self._on_hidden_or_shown_callbacks(self, hidden_reasons_to_remove, added=False) @property def transient(self): if self._transient is not None: return self._transient return self._transient_tuning @transient.setter def transient(self, value): self._transient = value @distributor.fields.Field(op=distributor.ops.SetBuildBuyUseFlags) def build_buy_use_flags(self): return self._build_buy_use_flags @build_buy_use_flags.setter def build_buy_use_flags(self, value): self._build_buy_use_flags = value @distributor.fields.Field(op=distributor.ops.SetOwnerId) def household_owner_id(self): return self._household_owner_id _resend_household_owner_id = household_owner_id.get_resend() def get_edges(self): (lower_bound, upper_bound) = self.get_fooptrint_polygon_bounds() if lower_bound is None or upper_bound is None: return () y = self.position.y transform = self.transform p0 = transform.transform_point(sims4.math.Vector3(lower_bound.x, y, lower_bound.z)) p1 = transform.transform_point(sims4.math.Vector3(lower_bound.x, y, upper_bound.z)) p2 = transform.transform_point(sims4.math.Vector3(upper_bound.x, y, upper_bound.z)) p3 = transform.transform_point(sims4.math.Vector3(upper_bound.x, y, lower_bound.z)) return ((p0, p1), (p1, p2), (p2, p3), (p3, p0)) def get_edge_constraint(self, constraint_width=1.0, inward_dir=False, return_constraint_list=False, los_reference_point=DEFAULT, sim=None): edges = self.get_edges() polygons = [] for (start, stop) in edges: along = sims4.math.vector_normalize(stop - start) inward = sims4.math.vector3_rotate_axis_angle(along, sims4.math.PI/2, sims4.math.Vector3.Y_AXIS()) if inward_dir: polygon = sims4.geometry.Polygon([start, start + constraint_width*inward, stop + constraint_width*inward, stop]) else: polygon = sims4.geometry.Polygon([start, stop, stop - constraint_width*inward, start - constraint_width*inward]) polygons.append(polygon) routing_surface = self.routing_surface if return_constraint_list: constraint_list = [] for polygon in polygons: restricted_polygon = sims4.geometry.RestrictedPolygon(polygon, ()) constraint = Constraint(routing_surface=routing_surface, geometry=restricted_polygon, los_reference_point=los_reference_point, posture_state_spec=STAND_AT_NONE_POSTURE_STATE_SPEC) constraint_list.append(constraint) return constraint_list else: geometry = sims4.geometry.RestrictedPolygon(sims4.geometry.CompoundPolygon(polygons), ()) constraint = Constraint(routing_surface=routing_surface, geometry=geometry, posture_state_spec=STAND_AT_NONE_POSTURE_STATE_SPEC) return constraint def get_created_constraint(self, tuned_constraint): if not self.additional_interaction_constraints: return if not self._created_constraints: self._created_constraints = {} if self._created_constraints_dirty: self._created_constraints.clear() for tuned_additional_constraint in self.additional_interaction_constraints: constraint = tuned_additional_constraint.constraint if constraint is not None: self._created_constraints[constraint] = constraint.create_constraint(None, self) self._created_constraints_dirty = False return self._created_constraints.get(tuned_constraint) @forward_to_components def register_rebate_tests(self, test_set): pass @forward_to_components def validate_definition(self): pass def _should_invalidate_location(self): parent = self.parent if parent is None: return True return parent._should_invalidate_location() def _notify_buildbuy_of_location_change(self, old_location): if self.persistence_group == PersistenceGroups.OBJECT and self._should_invalidate_location(): invalidate_object_location(self.id) def set_build_buy_lockout_state(self, lockout_state, lockout_timer=None): if self._build_buy_lockout_alarm_handler is not None: alarms.cancel_alarm(self._build_buy_lockout_alarm_handler) self._build_buy_lockout_alarm_handler = None elif self._build_buy_lockout and lockout_state: return if lockout_state: if lockout_timer is not None: time_span_real_time = clock.interval_in_real_seconds(lockout_timer) self._build_buy_lockout_alarm_handler = alarms.add_alarm_real_time(self, time_span_real_time, lambda *_: self.set_build_buy_lockout_state(False)) if lockout_state and not self.build_buy_lockout: self.reset(ResetReason.RESET_EXPECTED) self._build_buy_lockout = lockout_state self.resend_interactable() self.resend_tint() def on_location_changed(self, old_location): super().on_location_changed(old_location) self.mark_get_locations_for_posture_needs_update() self.clear_check_line_of_sight_cache() self._provided_surface = UNSET if self.id: self._update_persistence_group() self._notify_buildbuy_of_location_change(old_location) self.manager.on_location_changed(self) if self._on_location_changed_callbacks is not None: self._on_location_changed_callbacks(self, old_location, self.location) self._created_constraints_dirty = True def set_object_def_state_index(self, state_index): if type(self) != self.get_class_for_obj_state(state_index): logger.error("Attempting to change object {}'s state to one that would require a different runtime class. This is not supported.", self, owner='tastle') self.apply_definition(self.definition, state_index) self.model = self._model self.rig = self._rig self.resend_state_index() self.resend_slot() def register_on_location_changed(self, callback): if self._on_location_changed_callbacks is None: self._on_location_changed_callbacks = CallableListConsumingExceptions() self._on_location_changed_callbacks.append(callback) def unregister_on_location_changed(self, callback): if self._on_location_changed_callbacks is None: logger.error('Unregistering location changed callback on {} when there are none registered.', self) return if callback not in self._on_location_changed_callbacks: logger.error('Unregistering location changed callback on {} that is not registered. Callback: {}.', self, callback) return self._on_location_changed_callbacks.remove(callback) if not self._on_location_changed_callbacks: self._on_location_changed_callbacks = None def is_on_location_changed_callback_registered(self, callback): return callback in self._on_location_changed_callbacks def register_on_hidden_or_shown(self, callback): if self._on_hidden_or_shown_callbacks is None: self._on_hidden_or_shown_callbacks = CallableListConsumingExceptions() self._on_hidden_or_shown_callbacks.append(callback) def unregister_on_hidden_or_shown(self, callback): if self._on_hidden_or_shown_callbacks is None: logger.error('Unregistering hidden or shown callback on {} when there are none registered.', self) return if callback not in self._on_hidden_or_shown_callbacks: logger.error('Unregistering hidden or shown callback on {} that is not registered. Callback: {}.', self, callback) return self._on_hidden_or_shown_callbacks.remove(callback) if not self._on_hidden_or_shown_callbacks: self._on_hidden_or_shown_callbacks = None def is_on_hidden_or_shown_callback_registered(self, callback): if self._on_hidden_or_shown_callbacks is None: return False return callback in self._on_hidden_or_shown_callbacks def is_on_active_lot(self, tolerance=0): return self.persistence_group == PersistenceGroups.OBJECT @property def is_in_navmesh(self): if self._routing_context is not None and self._routing_context.object_footprint_id is not None: return True else: return False @property def may_move(self): return self.vehicle_component is not None or self.routing_component is not None and self.routing_component.object_routing_component is not None def get_surface_override_for_posture(self, source_posture_name): pass @property def add_to_posture_graph_if_parented(self): return self._add_to_posture_graph_if_parented @classproperty def provided_mobile_posture_affordances(cls): return cls._provided_mobile_posture_affordances or EMPTY_SET def get_joint_transform_for_joint(self, joint_name): transform = get_joint_transform_from_rig(self.rig, joint_name) transform = Transform.concatenate(transform, self.transform) return transform @property def object_radius(self): if self._routing_context is None: return routing.get_default_agent_radius() return self._routing_context.object_radius @property def persistence_group(self): return self._persistence_group @persistence_group.setter def persistence_group(self, value): self._persistence_group = value def _update_persistence_group(self): if self.is_in_inventory(): self.persistence_group = objects.persistence_groups.PersistenceGroups.OBJECT return if self.persistence_group == objects.persistence_groups.PersistenceGroups.OBJECT: if not services.current_zone().lot.is_position_on_lot(self.position, 0): remove_object_from_buildbuy_system(self.id) self.persistence_group = objects.persistence_groups.PersistenceGroups.IN_OPEN_STREET elif self.persistence_group == objects.persistence_groups.PersistenceGroups.IN_OPEN_STREET and services.current_zone().lot.is_position_on_lot(self.position, 0): self.persistence_group = objects.persistence_groups.PersistenceGroups.OBJECT add_object_to_buildbuy_system(self.id) def _fixup_pool_surface(self): if (self.item_location == ItemLocation.FROM_WORLD_FILE or self.item_location == ItemLocation.FROM_CONDITIONAL_LAYER) and (self.routing_surface.type != SurfaceType.SURFACETYPE_POOL and build_buy.PlacementFlags.REQUIRES_WATER_SURFACE & build_buy.get_object_placement_flags(self.definition.id)) and get_water_depth_at_location(self.location) > 0: routing_surface = self.routing_surface self.set_location(self.location.clone(routing_surface=SurfaceIdentifier(routing_surface.primary_id, routing_surface.secondary_id, SurfaceType.SURFACETYPE_POOL))) def _add_to_world(self): if self.persistence_group == PersistenceGroups.OBJECT: add_object_to_buildbuy_system(self.id) def _remove_from_world(self): if self.persistence_group == PersistenceGroups.OBJECT: remove_object_from_buildbuy_system(self.id) def on_add(self): super().on_add() self._add_to_world() self.register_on_location_changed(self._location_changed) if self.is_fire_related_object: fire_service = services.get_fire_service() self.register_on_location_changed(fire_service.flammable_object_location_changed) posture_graph_service = services.posture_graph_service() if posture_graph.is_object_mobile_posture_compatible(self): self.register_on_location_changed(posture_graph_service.mobile_posture_object_location_changed) if self.provided_mobile_posture_affordances: posture_graph_service.add_mobile_posture_provider(self) services.call_to_action_service().object_created(self) self.try_mark_as_new_object() def on_remove(self): zone = services.current_zone() if zone is not None and not zone.is_zone_shutting_down: services.get_event_manager().process_event(test_events.TestEvent.ObjectDestroyed, obj=self) super().on_remove() if not zone.is_zone_shutting_down: self._remove_from_world() self.unregister_on_location_changed(self._location_changed) if self.is_fire_related_object: fire_service = services.get_fire_service() if fire_service is not None: self.unregister_on_location_changed(fire_service.flammable_object_location_changed) posture_graph_service = services.posture_graph_service() if self.provided_mobile_posture_affordances: posture_graph_service.remove_mobile_posture_provider(self) if posture_graph.is_object_mobile_posture_compatible(self): posture_graph_service.remove_object_from_mobile_posture_quadtree(self) self.unregister_on_location_changed(posture_graph_service.mobile_posture_object_location_changed) else: self._on_location_changed_callbacks = None def on_added_to_inventory(self): super().on_added_to_inventory() self._remove_from_world() self.visibility = VisibilityState(False) def on_removed_from_inventory(self): super().on_removed_from_inventory() self._add_to_world() self.visibility = VisibilityState(True) @forward_to_components def on_buildbuy_exit(self): self._update_location_callbacks(update_surface=True) def _update_location_callbacks(self, update_surface=False): self._inside_status_change() self._natural_ground_status_change() if update_surface: self._surface_type_changed() @staticmethod def _location_changed(obj, old_loc, new_loc): if obj.zone_id: obj._update_location_callbacks(update_surface=old_loc.routing_surface != new_loc.routing_surface) obj._fixup_pool_surface() @forward_to_components def _surface_type_changed(self): pass def _inside_status_change(self, *_, **__): if self.is_outside: self._set_placed_outside() else: self._set_placed_inside() def _natural_ground_status_change(self, *_, **__): if self.routing_surface is not None and self.routing_surface.type == SurfaceType.SURFACETYPE_POOL: return if self.is_on_natural_ground(): self._set_placed_on_natural_ground() else: self._set_placed_off_natural_ground() @forward_to_components def _set_placed_outside(self): pass @forward_to_components def _set_placed_inside(self): pass @forward_to_components def _set_placed_on_natural_ground(self): pass @forward_to_components def _set_placed_off_natural_ground(self): pass @ored_forward_to_components def on_hovertip_requested(self): return False @property def is_outside(self): routing_surface = self.routing_surface level = 0 if routing_surface is None else routing_surface.secondary_id try: return is_location_outside(self.position, level) except RuntimeError: pass def is_on_natural_ground(self): if self.parent is not None: return False routing_surface = self.routing_surface level = 0 if routing_surface is None else routing_surface.secondary_id try: return is_location_natural_ground(self.position, level) except RuntimeError: pass def try_mark_as_new_object(self): if not (self.should_mark_as_new and services.current_zone().is_in_build_buy): return self.add_dynamic_component(objects.components.types.NEW_OBJECT_COMPONENT) def on_child_added(self, child, location): super().on_child_added(child, location) self.get_raycast_root().on_leaf_child_changed() def on_child_removed(self, child, new_parent=None): super().on_child_removed(child, new_parent=new_parent) self.get_raycast_root().on_leaf_child_changed() def on_leaf_child_changed(self): if self._raycast_context is not None: self._create_raycast_context() @property def forward_direction_for_picking(self): return sims4.math.Vector3.Z_AXIS() @property def route_target(self): parts = self.parts if parts is None: return (RouteTargetType.OBJECT, self) else: return (RouteTargetType.PARTS, parts) @property def should_mark_as_new(self): return True def is_surface(self, include_parts=False, ignore_deco_slots=False): if self._is_surface is None: self._is_surface = {} key = (include_parts, ignore_deco_slots) is_surface = self._is_surface.get(key) if is_surface is not None: return is_surface inventory_component = self.inventory_component if inventory_component is not None and inventory_component.has_get_put: self._is_surface[key] = True return True def is_valid_surface_slot(slot_type): if not (ignore_deco_slots and slot_type.is_deco_slot) and slot_type.is_surface: return True return False for runtime_slot in self.get_runtime_slots_gen(): if not include_parts and runtime_slot.owner is not self: continue if not any(is_valid_surface_slot(slot_type) for slot_type in runtime_slot.slot_types): continue if not runtime_slot.owner.is_same_object_or_part(self): continue self._is_surface[key] = True return True else: self._is_surface[key] = False return False def get_save_lot_coords_and_level(self): lot_coord_msg = LotCoord() parent = self.parent if parent is not None and parent.is_sim: parent.force_update_routing_location() starting_position = parent.position + parent.forward starting_location = placement.create_starting_location(position=starting_position, orientation=parent.orientation, routing_surface=self.location.world_routing_surface) fgl_context = placement.create_fgl_context_for_object(starting_location, self) (trans, orient) = placement.find_good_location(fgl_context) if trans is None: logger.warn('Unable to find good location to save object{}, which is parented to sim {} and cannot go into an inventory. Defaulting to location of sim.', self, parent) transform = parent.transform else: transform = sims4.math.Transform(trans, orient) if self.persistence_group == PersistenceGroups.OBJECT: transform = services.current_zone().lot.convert_to_lot_coordinates(transform) elif self.persistence_group == PersistenceGroups.OBJECT: transform = services.current_zone().lot.convert_to_lot_coordinates(self.transform) else: transform = self.transform lot_coord_msg.x = transform.translation.x lot_coord_msg.y = transform.translation.y lot_coord_msg.z = transform.translation.z lot_coord_msg.rot_x = transform.orientation.x lot_coord_msg.rot_y = transform.orientation.y lot_coord_msg.rot_z = transform.orientation.z lot_coord_msg.rot_w = transform.orientation.w if self.location.world_routing_surface is not None: level = self.location.level else: level = 0 return (lot_coord_msg, level) def save_object(self, object_list, *args, **kwargs): save_data = super().save_object(object_list, *args, **kwargs) if save_data is None: return save_data.slot_id = self.bone_name_hash (save_data.position, save_data.level) = self.get_save_lot_coords_and_level() inventory_plex_id = self.get_inventory_plex_id() if inventory_plex_id is not None: save_data.inventory_plex_id = inventory_plex_id save_data.scale = self.scale save_data.state_index = self.state_index if hasattr(save_data, 'buildbuy_use_flags'): save_data.buildbuy_use_flags = self._build_buy_use_flags save_data.cost = self.base_value save_data.ui_metadata = self.ui_metadata._value self.post_tooltip_save_data_stored() save_data.is_new = self.new_in_inventory save_data.is_new_object = self.is_new_object self.populate_icon_canvas_texture_info(save_data) if self._household_owner_id is not None: save_data.owner_id = self._household_owner_id save_data.needs_depreciation = self._needs_depreciation save_data.needs_post_bb_fixup = self._needs_post_bb_fixup if self._persisted_tags: save_data.persisted_tags.extend(self._persisted_tags) if self._multicolor is not None: for color in self._multicolor: color = getattr(color, 'value', color) multicolor_info_msg = save_data.multicolor.add() (multicolor_info_msg.x, multicolor_info_msg.y, multicolor_info_msg.z, _) = sims4.color.to_rgba(color) save_data.created_from_lot_template = False save_data.stack_sort_order = self.get_stack_sort_order() if self.material_state: save_data.material_state = self.material_state.state_name_hash if self.geometry_state: save_data.geometry_state = self.geometry_state if self.model: model_key = sims4.resources.get_protobuff_for_key(self.model) save_data.model_override_resource_key = model_key parent = self.bb_parent if parent is not None: if not parent.is_sim: save_data.parent_id = parent.id if not (parent is None or not parent.is_sim): save_data.object_parent_type = self._parent_type save_data.encoded_parent_location = self._parent_location inventory = self.inventory_component if inventory is not None: if not inventory.is_shared_inventory: save_data.unique_inventory = inventory.save_items() return save_data def load_object(self, object_data, **kwargs): if object_data.HasField('owner_id'): self._household_owner_id = object_data.owner_id if self.is_downloaded: self.base_value = self.catalog_value else: self.base_value = object_data.cost self.new_in_inventory = object_data.is_new super().load_object(object_data, **kwargs) if object_data.HasField('texture_id') and self.canvas_component is not None: self.canvas_component.set_painting_texture_id(object_data.texture_id) if object_data.HasField('needs_depreciation'): self._needs_depreciation = object_data.needs_depreciation if object_data.HasField('needs_post_bb_fixup'): self._needs_post_bb_fixup = object_data.needs_post_bb_fixup else: self._needs_post_bb_fixup = self._needs_depreciation inventory = self.inventory_component if inventory is not None: inventory.load_items(object_data.unique_inventory) if sims4.protocol_buffer_utils.has_field(object_data, 'buildbuy_use_flags'): self._build_buy_use_flags = object_data.buildbuy_use_flags self.is_new_object = object_data.is_new_object if self.is_new_object: self.add_dynamic_component(objects.components.types.NEW_OBJECT_COMPONENT) if object_data.persisted_tags is not None: self.append_tags(set(object_data.persisted_tags)) def finalize(self, **kwargs): super().finalize(**kwargs) self.try_post_bb_fixup(**kwargs) if self.is_fire_related_object: fire_service = services.get_fire_service() if fire_service is not None: fire_service.flammable_object_location_changed(self) if posture_graph.is_object_mobile_posture_compatible(self): posture_graph_service = services.current_zone().posture_graph_service posture_graph_service.mobile_posture_object_location_changed(self) def set_household_owner_id(self, new_owner_id): self._household_owner_id = new_owner_id self._resend_household_owner_id() if self.live_drag_component is not None: self.live_drag_component.resolve_live_drag_household_permission() def get_household_owner_id(self): return self._household_owner_id def get_object_property(self, property_type): if property_type == GameObjectProperty.CATALOG_PRICE: return self.definition.price if property_type == GameObjectProperty.MODIFIED_PRICE: return self.current_value if property_type == GameObjectProperty.RARITY: return self.get_object_rarity_string() if property_type == GameObjectProperty.GENRE: return Genre.get_genre_localized_string(self) if property_type == GameObjectProperty.RECIPE_NAME or property_type == GameObjectProperty.RECIPE_DESCRIPTION: return self.get_craftable_property(self, property_type) if property_type == GameObjectProperty.OBJ_TYPE_REL_ID: return services.relationship_service().get_object_type_rel_id(self) logger.error('Requested property_type {} not found on game_object'.format(property_type), owner='camilogarcia') def update_ownership(self, sim, make_sim_owner=True): household_id = sim.household_id if self._household_owner_id != household_id: if self.ownable_component is not None: self.ownable_component.update_sim_ownership(None) self.set_household_owner_id(household_id) if make_sim_owner and self.ownable_component is not None: self.ownable_component.update_sim_ownership(sim.sim_id) @property def flammable(self): fire_service = services.get_fire_service() if fire_service is not None: return fire_service.is_object_flammable(self) return False def object_bounds_for_flammable_object(self, fire_retardant_bonus): return self.flammable_area.get_bounds_for_flammable_object(self, fire_retardant_bonus) @property def is_set_as_head(self): parent = self.parent if parent is None: return False if not parent.is_sim: return False if parent.current_object_set_as_head is None: return False else: parent_head = parent.current_object_set_as_head() if not self.is_same_object_or_part(parent_head): return False return True @classmethod def register_tuned_animation(cls, *_, **__): pass @classmethod def add_auto_constraint(cls, *_, **__): pass def may_reserve(self, sim, *args, **kwargs): for child in self.children: child_targets = child.parts if child.parts else (child,) for child_target in child_targets: if child_target.is_sim: continue reserve_result = child_target.may_reserve(sim, *args, **kwargs) if not reserve_result: return reserve_result return super().may_reserve(sim, *args, **kwargs) def make_transient(self): self.transient = True self._destroy_if_not_in_use() def _destroy_if_not_in_use(self): if self.is_part: self.part_owner._destroy_if_not_in_use() return if self.self_or_part_in_use: return if not self.transient: return self.schedule_destroy_asap(source=self, cause='Destroying unused transient object.') posture_graph_service = services.current_zone().posture_graph_service if posture_graph_service.is_object_pending_deletion(self): posture_graph_service.finalize_object_deletion(self) def remove_reservation_handler(self, *args, **kwargs): super().remove_reservation_handler(*args, **kwargs) self._destroy_if_not_in_use() def schedule_element(self, timeline, element): resettable_element = reset.ResettableElement(element, self) resettable_element.on_scheduled(timeline) timeline.schedule(resettable_element) return resettable_element def register_reset_element(self, element): if self._scheduled_elements is None: self._scheduled_elements = set() self._scheduled_elements.add(element) def unregister_reset_element(self, element): if self._scheduled_elements is not None: self._scheduled_elements.discard(element) if not self._scheduled_elements: self._scheduled_elements = None def on_reset_element_hard_stop(self): self.reset(reset_reason=ResetReason.RESET_EXPECTED) def on_reset_get_elements_to_hard_stop(self, reset_reason): elements_to_reset = super().on_reset_get_elements_to_hard_stop(reset_reason) if self._scheduled_elements is not None: scheduled_elements = list(self._scheduled_elements) self._scheduled_elements = None for element in scheduled_elements: elements_to_reset.append(element) element.unregister() return elements_to_reset def get_gsi_portal_items(self, key_name, value_name): household_owner_id = self.household_owner_id household_owner = services.household_manager().get(household_owner_id) name = household_owner.name if household_owner is not None else 'Not Owned' return [{key_name: 'Household Owner', value_name: name}]
class InventoryStorage: UI_SORT_TYPES = TunableList( description= "\n A list of gameplay-based sort types used in the sim's inventory in the UI.\n ", tunable=TunableTuple( description= '\n Data that defines this sort for the inventory UI.\n ', sort_name=TunableLocalizedString( description= '\n The name displayed in the UI for this sort type.\n ' ), object_data=TunableVariant( description= '\n The object data that determines the sort order of\n this sort type.\n ', states=TunableList( description= '\n States whose values are used to sort on for this sort type. \n ', tunable=TunableReference( description= '\n A State to sort on.\n ', manager=services.get_instance_manager( sims4.resources.Types.OBJECT_STATE), class_restrictions='ObjectState')), default='states'), is_ascending=Tunable( description= '\n Whether a higher value from object_data will sort first.\n If a high value means that the object should sort lower \n (E.G. brokenness), this should be false.\n ', tunable_type=bool, default=True), debug_name=Tunable( description= '\n A unique name used to select this inventory sort type through \n the console command ui.inventory.set_sort_filter when the inventory\n ui is open.\n ', tunable_type=str, default='NONE'), export_class_name='InventoryUISortTypeTuple', export_modes=ExportModes.ClientBinary)) UI_FILTER_TYPES = TunableList( description= "\n A list of filter categories containing filter types used to filter the sim's\n inventory in the UI. The inventory can also be sorted by filter type; \n filters lower on this list will sort lower when sorted by filter type.\n ", tunable=TunableTuple( description= '\n A category of filters in the UI. Contains a name and a list of filters.\n ', filters=TunableList( description= '\n The filters used in this category. \n ', tunable=TunableTuple( description= '\n Data that defines a filter type in the inventory UI.\n ', tags=TunableTags( description= '\n Tags that should be considered part of this filter.\n ', binary_type=EnumBinaryExportType.EnumUint32), filter_name=TunableLocalizedString( description= '\n The name displayed in the UI for this filter type. \n ' ), debug_name=Tunable( description= '\n A unique name used to select this inventory filter type through \n the console command ui.inventory.set_sort_filter when the inventory\n ui is open.\n ', tunable_type=str, default='NONE'), export_class_name='InventoryUIFilterTypeTuple')), category_name=TunableLocalizedString( description= '\n The name displayed in the UI for this filter category.\n ' ), export_class_name='InventoryUIFilterCategoryTuple', export_modes=ExportModes.ClientBinary)) def __init__(self, inventory_type, item_location, max_size=None, allow_compaction=True, allow_ui=True, hidden_storage=False): self._objects = {} self._owners = WeakSet() self._inventory_type = inventory_type self._item_location = item_location self._max_size = max_size self._allow_compaction = allow_compaction self._allow_ui = allow_ui self._hidden_storage = hidden_storage self._stacks_with_options_counter = None def __len__(self): return len(self._objects) def __iter__(self): yield from iter(self._objects.values()) def __contains__(self, obj_id): return obj_id in self._objects def __getitem__(self, obj_id): if obj_id in self._objects: return self._objects[obj_id] def __repr__(self): return 'InventoryStorage<{},{}>'.format(self._inventory_type, self._get_inventory_id()) def register(self, owner): self._owners.add(owner) def unregister(self, owner): self._owners.discard(owner) def has_owners(self): if self._owners: return True return False def get_owners(self): return tuple(self._owners) @property def allow_ui(self): return self._allow_ui @allow_ui.setter def allow_ui(self, value): self._allow_ui = value def discard_object_id(self, obj_id): if obj_id in self._objects: del self._objects[obj_id] def discard_all_objects(self): for obj in self._objects.values(): self._distribute_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_REMOVE, obj) obj.inventoryitem_component.set_inventory_type(None, None) self._objects.clear() def can_insert(self, obj): if not obj.can_go_in_inventory_type(self._inventory_type): return False elif self._max_size is not None and sum( inventory_obj.stack_count() for inventory_obj in self) >= self._max_size: return False return True def insert(self, obj, inventory_object=None, compact=True): if not self.can_insert(obj): return False try: obj.on_before_added_to_inventory() except: logger.exception( 'Exception invoking on_before_added_to_inventory. obj: {}', obj) self._insert(obj, inventory_object) try: obj.on_added_to_inventory() except: logger.exception( 'Exception invoking on_added_to_inventory. obj: {}', obj) compacted_obj_id = None compacted_count = None if compact: (compacted_obj_id, compacted_count) = self._try_compact(obj) if compacted_obj_id is None: for owner in self._owners: try: owner.on_object_inserted(obj) except: logger.exception( 'Exception invoking on_object_inserted. obj: {}, owner: {}', obj, owner) self._distribute_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_ADD, obj) sent_stack_update = False if obj.inventoryitem_component.has_stack_option: if self._stacks_with_options_counter is None: self._stacks_with_options_counter = defaultdict(int) stack_id = obj.inventoryitem_component.get_stack_id() stack_objects = self._stacks_with_options_counter[stack_id] if stack_objects == 0: self._distribute_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_SET_STACK_OPTION, obj) sent_stack_update = True self._stacks_with_options_counter[stack_id] += 1 if not sent_stack_update: obj_owner = obj.inventoryitem_component.get_inventory().owner if obj_owner.is_sim and obj_owner.sim_info.favorites_tracker is not None and obj_owner.sim_info.favorites_tracker.is_favorite_stack( obj): self._distribute_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_SET_STACK_OPTION, obj) else: for owner in self._owners: try: owner.on_object_id_changed(obj, compacted_obj_id, compacted_count) except: logger.exception( 'Exception invoking on_object_id_changed. obj: {}, owner: {}', obj, owner) self._distribute_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_UPDATE, obj, obj_id=compacted_obj_id) return True def update_object_stack_by_id(self, obj_id, new_stack_id): if obj_id not in self._objects: return obj = self._objects[obj_id] self._distribute_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_REMOVE, obj) obj.set_stack_id(new_stack_id) self._distribute_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_ADD, obj) def remove(self, obj, count=1, move_to_object_manager=True): if obj.id not in self._objects: return False old_stack_count = obj.stack_count() split_obj = self._try_split(obj, count) try: obj.on_before_removed_from_inventory() except: logger.exception( 'Exception invoking on_before_removed_from_inventory. obj: {}', obj) self._remove(obj, move_to_object_manager=move_to_object_manager) try: obj.on_removed_from_inventory() except: logger.exception( 'Exception invoking on_removed_from_inventory. obj: {}', obj) if split_obj is None: for owner in self._owners: try: owner.on_object_removed(obj) except: logger.exception( 'Exception invoking on_object_removed. obj: {}, owner: {}', obj, owner) self._distribute_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_REMOVE, obj) if obj.inventoryitem_component.has_stack_option and self._stacks_with_options_counter is not None: stack_id = obj.inventoryitem_component.get_stack_id() self._stacks_with_options_counter[stack_id] -= 1 if stack_id in self._stacks_with_options_counter <= 0: if self._stacks_with_options_counter[stack_id] < 0: logger.error( 'Counter went negative for stack_id {} with scheme {}', stack_id, obj.inventoryitem_component.stack_scheme, owner='jdimailig') del self._stacks_with_options_counter[stack_id] else: for owner in self._owners: try: owner.on_object_id_changed(split_obj, obj.id, old_stack_count) except: logger.exception( 'Exception invoking on_object_id_changed. obj: {}, owner: {}', obj, owner) self._distribute_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_UPDATE, split_obj, obj_id=obj.id) return True def _insert(self, obj, inventory_object): self._objects[obj.id] = obj obj.inventoryitem_component.set_inventory_type(self._inventory_type, inventory_object) obj.item_location = self._item_location if self._inventory_type == InventoryType.SIM: obj.inventoryitem_component.is_hidden = self._hidden_storage object_manager = services.object_manager() if obj.id in object_manager: object_manager.move_to_inventory( obj, services.current_zone().inventory_manager) obj.set_parent(None) posture_graph_service = services.current_zone( ).posture_graph_service if posture_graph_service.is_object_pending_deletion(obj): posture_graph_service.finalize_object_deletion(obj) def _remove(self, obj, move_to_object_manager=False): if move_to_object_manager: services.current_zone().inventory_manager.move_to_world( obj, services.object_manager()) obj.item_location = ItemLocation.ON_LOT obj.inventoryitem_component.set_inventory_type( None, None, from_removal=not move_to_object_manager) del self._objects[obj.id] def _get_compact_data(self, obj): try: obj.inventoryitem_component.save_for_stack_compaction = True return obj.get_attribute_save_data() finally: obj.inventoryitem_component.save_for_stack_compaction = False obj.post_tooltip_save_data_stored() def _try_compact(self, obj): if not self._allow_compaction: return (None, None) if len(self._objects) < 2: return (None, None) if obj.has_component( components.types.OBJECT_CLAIM_COMPONENT ) and obj.object_claim_component.requires_claiming: return (None, None) similar = None def_id = obj.definition.id data = self._get_compact_data(obj) stack_id = obj.inventoryitem_component.get_stack_id() for other in self._objects.values(): if def_id != other.definition.id: continue if other is obj: continue if stack_id != other.inventoryitem_component.get_stack_id(): continue if not any(interaction.should_reset_based_on_pipeline_progress for interaction in other.interaction_refs): other_data = self._get_compact_data(other) if data == other_data: similar = other break if similar is None: return (None, None) similar_id = similar.id similar_count = similar.stack_count() self._remove(similar) similar.destroy(source=self, cause='InventoryStorage compaction') obj.update_stack_count(similar_count) return (similar_id, similar_count) def _try_split(self, obj, count): if count >= obj.stack_count(): return clone = obj.inventoryitem_component.get_clone_for_stack_split() self._insert(clone, obj.inventoryitem_component.last_inventory_owner) clone.update_stack_count(-count) obj.set_stack_count(count) clone.on_added_to_inventory() return clone def _get_inventory_id(self): if InventoryTypeTuning.is_shared_between_objects(self._inventory_type): return int(self._inventory_type) if self._owners: return next(iter(self._owners)).owner.id logger.error( "Non-shared storage that's missing an owner: InventoryStorage<{},{}>", self._inventory_type, 0) return 0 def _get_inventory_ui_type(self): if InventoryTypeTuning.is_shared_between_objects(self._inventory_type): return UI_pb2.InventoryItemUpdate.TYPE_SHARED return UI_pb2.InventoryItemUpdate.TYPE_OBJECT def _get_inventory_update_message(self, update_type, obj, obj_id=None, allow_while_zone_not_running=False): if not self._allow_ui: return if not services.current_zone( ).is_zone_running and not allow_while_zone_not_running: return if services.current_zone().is_zone_shutting_down: return msg = UI_pb2.InventoryItemUpdate() msg.type = update_type msg.inventory_id = self._get_inventory_id() msg.inventory_type = self._get_inventory_ui_type() msg.stack_id = obj.inventoryitem_component.get_stack_id() if obj_id is None: msg.object_id = obj.id else: msg.object_id = obj_id if update_type == UI_pb2.InventoryItemUpdate.TYPE_ADD: add_data = UI_pb2.InventoryItemData() add_data.definition_id = obj.definition.id msg.add_data = add_data if update_type == UI_pb2.InventoryItemUpdate.TYPE_ADD or update_type == UI_pb2.InventoryItemUpdate.TYPE_UPDATE: dynamic_data = UI_pb2.DynamicInventoryItemData() dynamic_data.value = obj.current_value dynamic_data.count = obj.stack_count() dynamic_data.new_object_id = obj.id dynamic_data.is_new = obj.new_in_inventory dynamic_data.sort_order = obj.get_stack_sort_order() icon_info = obj.get_icon_info_data() build_icon_info_msg(icon_info, None, dynamic_data.icon_info) recipe_name = obj.get_tooltip_field( TooltipFieldsComplete.recipe_name ) or obj.get_craftable_property(GameObjectProperty.RECIPE_NAME) if recipe_name is not None: dynamic_data.recipe_name = recipe_name if obj.custom_name is not None: dynamic_data.custom_name = obj.custom_name if InventoryStorage.UI_SORT_TYPES: sort_type = 0 for sort_type_data in InventoryStorage.UI_SORT_TYPES: value = None try: abs_value = None state_component = obj.state_component if state_component is None: continue for state in sort_type_data.object_data: if state_component.has_state(state): test_value = float( state_component.get_state(state).value) abs_test_value = abs(test_value) if value is None: value = test_value elif abs_value < abs_test_value: value = test_value abs_value = abs_test_value except TypeError: pass if value is not None: sort_data_item = UI_pb2.InventoryItemSortData() sort_data_item.type = sort_type sort_data_item.value = value dynamic_data.sort_data.append(sort_data_item) sort_type += 1 if update_type == UI_pb2.InventoryItemUpdate.TYPE_ADD: msg.add_data.dynamic_data = dynamic_data else: msg.update_data = dynamic_data if update_type == UI_pb2.InventoryItemUpdate.TYPE_SET_STACK_OPTION: dynamic_data = UI_pb2.DynamicInventoryItemData() if obj.inventoryitem_component.has_stack_option: obj.inventoryitem_component.populate_stack_icon_info_data( dynamic_data.icon_info) obj_owner = obj.inventoryitem_component.get_inventory().owner if obj_owner.is_sim: favorites_tracker = obj_owner.sim_info.favorites_tracker if favorites_tracker is not None: if favorites_tracker.is_favorite_stack(obj): dynamic_data.is_favorite = True msg.update_data = dynamic_data return msg def _distribute_inventory_update_message(self, update_type, obj, obj_id=None): msg = self._get_inventory_update_message(update_type, obj, obj_id=obj_id) if msg is not None: op = GenericProtocolBufferOp(Operation.INVENTORY_ITEM_UPDATE, msg) Distributor.instance().add_op_with_no_owner(op) def distribute_inventory_update_message(self, obj): if obj.id not in self._objects: return False msg = self._get_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_UPDATE, obj) if msg is not None: op = GenericProtocolBufferOp(Operation.INVENTORY_ITEM_UPDATE, msg) Distributor.instance().add_op_with_no_owner(op) def distribute_inventory_stack_update_message(self, obj): if obj.id not in self._objects: return msg = self._get_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_SET_STACK_OPTION, obj) if msg is not None: op = GenericProtocolBufferOp(Operation.INVENTORY_ITEM_UPDATE, msg) Distributor.instance().add_op_with_no_owner(op) def distribute_owned_inventory_update_message(self, obj, owner): if obj.id not in self._objects: return False msg = self._get_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_UPDATE, obj) if msg is not None: op = GenericProtocolBufferOp(Operation.INVENTORY_ITEM_UPDATE, msg) Distributor.instance().add_op(owner, op) def get_item_update_ops_gen(self): stack_options_set = set() for obj in self._objects.values(): message = self._get_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_ADD, obj, allow_while_zone_not_running=True) if message is None: continue yield (obj, GenericProtocolBufferOp(Operation.INVENTORY_ITEM_UPDATE, message)) if not obj.inventoryitem_component.has_stack_option: obj_owner = obj.inventoryitem_component.get_inventory().owner if obj_owner.is_sim: if obj_owner.sim_info.favorites_tracker is None: continue stack_id = obj.inventoryitem_component.get_stack_id() if stack_id in stack_options_set: continue option_msg = self._get_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_SET_STACK_OPTION, obj, allow_while_zone_not_running=True) if option_msg is not None: stack_options_set.add(stack_id) yield (obj, GenericProtocolBufferOp( Operation.INVENTORY_ITEM_UPDATE, option_msg)) else: stack_id = obj.inventoryitem_component.get_stack_id() if stack_id in stack_options_set: continue option_msg = self._get_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_SET_STACK_OPTION, obj, allow_while_zone_not_running=True) if option_msg is not None: stack_options_set.add(stack_id) yield (obj, GenericProtocolBufferOp( Operation.INVENTORY_ITEM_UPDATE, option_msg)) def open_ui_panel(self, obj): if not self._allow_ui: return False msg = UI_pb2.OpenInventory() msg.object_id = obj.id msg.inventory_id = self._get_inventory_id() msg.inventory_type = self._get_inventory_ui_type() op = GenericProtocolBufferOp(Operation.OPEN_INVENTORY, msg) Distributor.instance().add_op_with_no_owner(op) return True