class Subject: def __init__(self, parent): self.parent = parent self._observers_lock = RLock() self._observers = WeakSet() def addObserver(self, observer): with self._observers_lock: self._observers.add(observer) logger.debug("%s is being observed by %s", stringFor(self.parent), stringFor(observer)) def removeObserver(self, observer): with self._observers_lock: try: self._observers.remove(observer) except KeyError: logger.error("Tried to remove observer %s twice from %s", stringFor(observer), stringFor(self.parent)) def clearObservers(self): with self._observers_lock: self._observers.clear() logger.debug("%s observers were cleaned.", stringFor(self.parent)) def notify(self, event, *args): with self._observers_lock: observers = list(self._observers) for obs in observers: logger.debug("%s is about to notify %s to %s", stringFor(self.parent), event, stringFor(obs)) obs.onNotify(self.parent, event, args)
class Subject(object): def __init__(self, parent, loggingLevel=logging.INFO): super(Subject, self).__init__() self._logger = logging.getLogger("[OBSERVER {} ({})]".format( parent.__class__.__name__.upper(), id(parent))) self._logger.setLevel(loggingLevel) self.parent = parent self._observers_lock = RLock() self._observers = WeakSet() def addObserver(self, observer): with self._observers_lock: self._observers.add(observer) self._logger.debug("%s is being observed by %s", stringFor(self.parent), stringFor(observer)) def removeObserver(self, observer): with self._observers_lock: try: self._observers.remove(observer) except KeyError: self._logger.error("Tried to remove observer %s twice from %s", stringFor(observer), stringFor(self.parent)) def hasObserver(self, observer): with self._observers_lock: return observer in self._observers def clearObservers(self): with self._observers_lock: self._observers.clear() self._logger.debug("%s observers were cleaned.", stringFor(self.parent)) def notify(self, event, *args): with self._observers_lock: observers = list(self._observers) for obs in observers: self._logger.debug("%s is about to notify %s to %s", stringFor(self.parent), event, stringFor(obs)) try: obs.onNotify(self.parent, event, args) except Exception as e: self._logger.error( "Catched exception trying to notify %s to %s with arguments: %s", str(event), str(obs), str(args)) self._logger.exception(e)
class ObjectManager(DistributableObjectManager, GameObjectManagerMixin, AttractorManagerMixin): FIREMETER_DISPOSABLE_OBJECT_CAP = Tunable( int, 5, description= 'Number of disposable objects a lot can have at any given moment.') BED_TAGS = TunableTuple( description= '\n Tags to check on an object to determine what type of bed an object is.\n ', beds=TunableSet( description= '\n Tags that consider an object as a bed other than double beds.\n ', tunable=TunableEnumWithFilter(tunable_type=tag.Tag, default=tag.Tag.INVALID, filter_prefixes=BED_PREFIX_FILTER)), double_beds=TunableSet( description= '\n Tags that consider an object as a double bed\n ', tunable=TunableEnumWithFilter(tunable_type=tag.Tag, default=tag.Tag.INVALID, filter_prefixes=BED_PREFIX_FILTER)), kid_beds=TunableSet( description= '\n Tags that consider an object as a kid bed\n ', tunable=TunableEnumWithFilter(tunable_type=tag.Tag, default=tag.Tag.INVALID, filter_prefixes=BED_PREFIX_FILTER)), other_sleeping_spots=TunableSet( description= '\n Tags that considered sleeping spots.\n ', tunable=TunableEnumWithFilter(tunable_type=tag.Tag, default=tag.Tag.INVALID, filter_prefixes=BED_PREFIX_FILTER))) HOUSEHOLD_INVENTORY_OBJECT_TAGS = TunableTags( description= '\n List of tags to apply to every household inventory proxy object.\n ' ) INVALID_UNPARENTED_OBJECT_TAGS = TunableTags( description= '\n Objects with these tags should not exist without a parent. An obvious\n case is for transient objects. They should only exist as a carried object,\n thus parented to a sim, when loading into a save game.\n ' ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._crafting_cache = CraftingObjectCache() self._sim_spawn_conditions = collections.defaultdict(set) self._water_terrain_object_cache = WaterTerrainObjectCache() self._client_connect_callbacks = CallableList() self._portal_cache = WeakSet() self._portal_added_callbacks = CallableList() self._portal_removed_callbacks = CallableList() self._front_door_candidates_changed_callback = CallableList() self._all_bed_tags = self.BED_TAGS.beds | self.BED_TAGS.double_beds | self.BED_TAGS.kid_beds | self.BED_TAGS.other_sleeping_spots self._tag_to_object_list = defaultdict(set) self._whim_set_cache = Counter() self._posture_providing_object_cache = None self._objects_to_ignore_portal_validation_cache = [] @classproperty def save_error_code(cls): return persistence_error_types.ErrorCodes.SERVICE_SAVE_FAILED_OBJECT_MANAGER @property def crafting_cache(self): return self._crafting_cache @property def water_terrain_object_cache(self): return self._water_terrain_object_cache def portal_cache_gen(self): yield from self._portal_cache def on_client_connect(self, client): all_objects = list(self._objects.values()) for game_object in all_objects: game_object.on_client_connect(client) def move_to_inventory(self, obj, inventory_manager): logger.assert_raise( isinstance(inventory_manager, InventoryManager), 'Trying to move object to a non-inventory manager: {}', inventory_manager, owner='tingyul') logger.assert_raise( obj.id, 'Attempting to move an object that was never added or has already been removed', owner='tingyul') logger.assert_raise( self._objects.get(obj.id) is obj, 'Attempting to move an object {} that is not in this manager or not the same object {} in manager', obj, self._objects.get(obj.id), owner='tingyul') del self._objects[obj.id] obj.manager = inventory_manager inventory_manager._objects[obj.id] = obj self.remove_object_from_object_tags_cache(obj) self.remove_object_from_posture_providing_cache(obj) def add(self, obj, *args, **kwargs): super().add(obj, *args, **kwargs) self.add_object_to_object_tags_cache(obj) self.add_object_to_posture_providing_cache(obj) def remove(self, obj, *args, **kwargs): super().remove(obj, *args, **kwargs) current_zone = services.current_zone() if not current_zone.is_zone_shutting_down: self.remove_object_from_object_tags_cache(obj) self.remove_object_from_posture_providing_cache(obj) def add_object_to_object_tags_cache(self, obj): self.add_tags_and_object_to_cache(obj.get_tags(), obj) def add_tags_and_object_to_cache(self, tags, obj): if obj.id not in self: logger.error( "Trying to add object to tag cache when the object isn't in the manager: {}", obj, owner='tingyul') return for tag in tags: object_list = self._tag_to_object_list[tag] object_list.add(obj) def remove_object_from_object_tags_cache(self, obj): for tag in obj.get_tags(): if tag not in self._tag_to_object_list: continue object_list = self._tag_to_object_list[tag] if obj not in object_list: continue object_list.remove(obj) if not object_list: del self._tag_to_object_list[tag] def _should_save_object_on_lot(self, obj): parent = obj.parent if parent is not None and parent.is_sim: inventory = parent.inventory_component if inventory.should_save_parented_item_to_inventory(obj): return False else: vehicle_component = obj.vehicle_component if vehicle_component is not None: driver = vehicle_component.driver if driver is not None and driver.is_sim: inventory = driver.inventory_component if inventory.should_save_parented_item_to_inventory( obj): return False else: vehicle_component = obj.vehicle_component if vehicle_component is not None: driver = vehicle_component.driver if driver is not None and driver.is_sim: inventory = driver.inventory_component if inventory.should_save_parented_item_to_inventory(obj): return False return True def add_object_to_posture_providing_cache(self, obj): if not obj.provided_mobile_posture_affordances: return if self._posture_providing_object_cache is None: self._posture_providing_object_cache = set() self._posture_providing_object_cache.add(obj) posture_graph_service = services.posture_graph_service() if not posture_graph_service.has_built_for_zone_spin_up: posture_graph_service.on_mobile_posture_object_added_during_zone_spinup( obj) def remove_object_from_posture_providing_cache(self, obj): if not obj.provided_mobile_posture_affordances: return self._posture_providing_object_cache.remove(obj) if not self._posture_providing_object_cache: self._posture_providing_object_cache = None def get_posture_providing_objects(self): return self._posture_providing_object_cache or () def rebuild_objects_to_ignore_portal_validation_cache(self): self._objects_to_ignore_portal_validation_cache.clear() for obj in self._objects.values(): if not obj.routing_component is not None: if not obj.inventoryitem_component is not None: if obj.live_drag_component is not None: self._objects_to_ignore_portal_validation_cache.append( obj.id) self._objects_to_ignore_portal_validation_cache.append(obj.id) def clear_objects_to_ignore_portal_validation_cache(self): self._objects_to_ignore_portal_validation_cache.clear() def get_objects_to_ignore_portal_validation_cache(self): return self._objects_to_ignore_portal_validation_cache def clear_caches_on_teardown(self): self._tag_to_object_list.clear() self._water_terrain_object_cache.clear() if self._posture_providing_object_cache is not None: self._posture_providing_object_cache.clear() self.clear_objects_to_ignore_portal_validation_cache() build_buy.unregister_build_buy_exit_callback( self._water_terrain_object_cache.refresh) def pre_save(self): all_objects = list(self._objects.values()) lot = services.current_zone().lot for (_, inventory) in lot.get_all_object_inventories_gen( shared_only=True): for game_object in inventory: all_objects.append(game_object) for game_object in all_objects: game_object.update_all_commodities() @staticmethod def save_game_object(game_object, object_list, open_street_objects): save_result = None if game_object.persistence_group == objects.persistence_groups.PersistenceGroups.OBJECT: save_result = game_object.save_object(object_list.objects, ItemLocation.ON_LOT, 0) else: if game_object.item_location == ItemLocation.ON_LOT or game_object.item_location == ItemLocation.INVALID_LOCATION: item_location = ItemLocation.FROM_OPEN_STREET else: item_location = game_object.item_location save_result = game_object.save_object(open_street_objects.objects, item_location, 0) return save_result def save(self, object_list=None, zone_data=None, open_street_data=None, store_travel_group_placed_objects=False, **kwargs): if object_list is None: return open_street_objects = file_serialization.ObjectList() total_beds = 0 double_bed_exist = False kid_bed_exist = False alternative_sleeping_spots = 0 university_roommate_beds = 0 if store_travel_group_placed_objects: objects_to_save_for_clean_up = [] roommate_bed_tags = set() roommate_service = services.get_roommate_service() if roommate_service is not None: roommate_bed_tags = roommate_service.BED_TAGS for game_object in self._objects.values(): if self._should_save_object_on_lot(game_object): save_result = ObjectManager.save_game_object( game_object, object_list, open_street_objects) if not save_result: continue if zone_data is None: continue if store_travel_group_placed_objects and save_result.owner_id != 0: placement_flags = build_buy.get_object_placement_flags( game_object.definition.id) if build_buy.PlacementFlags.NON_INVENTORYABLE not in placement_flags: objects_to_save_for_clean_up.append(save_result) if not game_object.definition.has_build_buy_tag( *self._all_bed_tags): continue if game_object.definition.has_build_buy_tag( *self.BED_TAGS.double_beds): double_bed_exist = True total_beds += 1 elif game_object.definition.has_build_buy_tag( *self.BED_TAGS.kid_beds): total_beds += 1 kid_bed_exist = True elif game_object.definition.has_build_buy_tag( *self.BED_TAGS.other_sleeping_spots): alternative_sleeping_spots += 1 elif game_object.definition.has_build_buy_tag( *self.BED_TAGS.beds): total_beds += 1 if len(roommate_bed_tags) > 0: if game_object.definition.has_build_buy_tag( *roommate_bed_tags): university_roommate_beds += 1 if open_street_data is not None: open_street_data.objects = open_street_objects if zone_data is not None: bed_info_data = gameplay_serialization.ZoneBedInfoData() bed_info_data.num_beds = total_beds bed_info_data.double_bed_exist = double_bed_exist bed_info_data.kid_bed_exist = kid_bed_exist bed_info_data.alternative_sleeping_spots = alternative_sleeping_spots if roommate_service is not None: household_and_roommate_cap = roommate_service.HOUSEHOLD_AND_ROOMMATE_CAP bed_info_data.university_roommate_beds = min( household_and_roommate_cap, university_roommate_beds) zone_data.gameplay_zone_data.bed_info_data = bed_info_data if store_travel_group_placed_objects: current_zone = services.current_zone() save_game_protocol_buffer = services.get_persistence_service( ).get_save_game_data_proto() self._clear_clean_up_data_for_zone(current_zone, save_game_protocol_buffer) self._save_clean_up_destination_data( current_zone, objects_to_save_for_clean_up, save_game_protocol_buffer) lot = services.current_zone().lot for (inventory_type, inventory) in lot.get_all_object_inventories_gen( shared_only=True): for game_object in inventory: game_object.save_object(object_list.objects, ItemLocation.OBJECT_INVENTORY, inventory_type) def _clear_clean_up_data_for_zone(self, current_zone, save_game_protocol_buffer): current_zone_id = current_zone.id current_open_street_id = current_zone.open_street_id destination_clean_up_data = save_game_protocol_buffer.destination_clean_up_data for clean_up_save_data in destination_clean_up_data: indexes_to_clean_up = [] for (index, old_object_clean_up_data) in enumerate( clean_up_save_data.object_clean_up_data_list): if not old_object_clean_up_data.zone_id == current_zone_id: if old_object_clean_up_data.world_id == current_open_street_id: indexes_to_clean_up.append(index) indexes_to_clean_up.append(index) if len(indexes_to_clean_up) == len( clean_up_save_data.object_clean_up_data_list): clean_up_save_data.ClearField('object_clean_up_data_list') else: for index in reversed(indexes_to_clean_up): del clean_up_save_data.object_clean_up_data_list[index] def _save_clean_up_destination_data(self, current_zone, objects_to_save_for_clean_up, save_game_protocol_buffer): household_manager = services.household_manager() travel_group_manager = services.travel_group_manager() clean_up_save_data = None for object_data in sorted(objects_to_save_for_clean_up, key=lambda x: x.owner_id): owner_id = object_data.owner_id if clean_up_save_data is None or clean_up_save_data.household_id != owner_id: household = household_manager.get(owner_id) travel_group = None if household is not None: travel_group = household.get_travel_group() for clean_up_save_data in save_game_protocol_buffer.destination_clean_up_data: if clean_up_save_data.household_id != owner_id: continue if travel_group is not None: if travel_group.id == clean_up_save_data.travel_group_id: break if clean_up_save_data.travel_group_id in travel_group_manager: continue break with ProtocolBufferRollback( clean_up_save_data.object_clean_up_data_list ) as object_clean_up_data: if object_data.loc_type == ItemLocation.ON_LOT: object_clean_up_data.zone_id = current_zone.id else: object_clean_up_data.world_id = current_zone.open_street_id object_clean_up_data.object_data = object_data def add_sim_spawn_condition(self, sim_id, callback): for sim in services.sim_info_manager().instanced_sims_gen(): if sim.id == sim_id: logger.error( 'Sim {} is already in the world, cannot add the spawn condition', sim) return self._sim_spawn_conditions[sim_id].add(callback) def remove_sim_spawn_condition(self, sim_id, callback): if callback not in self._sim_spawn_conditions.get(sim_id, ()): logger.error( 'Trying to remove sim spawn condition with invalid id-callback pair ({}-{}).', sim_id, callback) return self._sim_spawn_conditions[sim_id].remove(callback) def trigger_sim_spawn_condition(self, sim_id): if sim_id in self._sim_spawn_conditions: for callback in self._sim_spawn_conditions[sim_id]: callback() del self._sim_spawn_conditions[sim_id] def add_portal_lock(self, sim, callback): self.register_portal_added_callback(callback) for portal in self.portal_cache_gen(): portal.lock_sim(sim) def register_portal_added_callback(self, callback): if callback not in self._portal_added_callbacks: self._portal_added_callbacks.append(callback) def unregister_portal_added_callback(self, callback): if callback in self._portal_added_callbacks: self._portal_added_callbacks.remove(callback) def register_portal_removed_callback(self, callback): if callback not in self._portal_removed_callbacks: self._portal_removed_callbacks.append(callback) def unregister_portal_removed_callback(self, callback): if callback in self._portal_removed_callbacks: self._portal_removed_callbacks.remove(callback) def _is_valid_portal_object(self, portal): portal_component = portal.get_component(PORTAL_COMPONENT) if portal_component is None: return False return portal.has_portals() def add_portal_to_cache(self, portal): if portal not in self._portal_cache and self._is_valid_portal_object( portal): self._portal_cache.add(portal) self._portal_added_callbacks(portal) def remove_portal_from_cache(self, portal): if portal in self._portal_cache: self._portal_cache.remove(portal) self._portal_removed_callbacks(portal) def register_front_door_candidates_changed_callback(self, callback): if callback not in self._front_door_candidates_changed_callback: self._front_door_candidates_changed_callback.append(callback) def unregister_front_door_candidates_changed_callback(self, callback): if callback in self._front_door_candidates_changed_callback: self._front_door_candidates_changed_callback.remove(callback) def on_front_door_candidates_changed(self): self._front_door_candidates_changed_callback() def cleanup_build_buy_transient_objects(self): household_inventory_proxy_objects = self.get_objects_matching_tags( self.HOUSEHOLD_INVENTORY_OBJECT_TAGS) for obj in household_inventory_proxy_objects: self.remove(obj) def get_objects_matching_tags(self, tags: set, match_any=False): matching_objects = None for tag in tags: objs = self._tag_to_object_list[ tag] if tag in self._tag_to_object_list else set() if matching_objects is None: matching_objects = objs elif match_any: matching_objects |= objs else: matching_objects &= objs if not matching_objects: break if matching_objects: return frozenset(matching_objects) return EMPTY_SET def get_num_objects_matching_tags(self, tags: set, match_any=False): matching_objects = self.get_objects_matching_tags(tags, match_any) return len(matching_objects) @contextmanager def batch_commodity_flags_update(self): default_fn = self.clear_commodity_flags_for_objs_with_affordance try: affordances = set() self.clear_commodity_flags_for_objs_with_affordance = affordances.update yield None finally: self.clear_commodity_flags_for_objs_with_affordance = default_fn self.clear_commodity_flags_for_objs_with_affordance(affordances) def clear_commodity_flags_for_objs_with_affordance(self, affordances): for obj in self.valid_objects(): if not obj.has_updated_commodity_flags(): continue if any(affordance in affordances for affordance in obj.super_affordances()): obj.clear_commodity_flags() def get_all_objects_with_component_gen(self, component_definition): if component_definition is None: return for obj in self.valid_objects(): if obj.has_component(component_definition): yield obj def get_objects_with_tag_gen(self, tag): yield from self.get_objects_matching_tags((tag, )) def get_objects_with_tags_gen(self, *tags): yield from self.get_objects_matching_tags(tags, match_any=True) def on_location_changed(self, obj): self._registered_callbacks[CallbackTypes.ON_OBJECT_LOCATION_CHANGED]( obj) def process_invalid_unparented_objects(self): invalid_objects = self.get_objects_matching_tags( self.INVALID_UNPARENTED_OBJECT_TAGS, match_any=True) for invalid_object in invalid_objects: if invalid_object.parent is None: logger.error( 'Invalid unparented object {} existed in game. Cleaning up.', invalid_object) invalid_object.destroy( source=invalid_object, cause='Invalid unparented object found on zone spin up.') @classproperty def supports_parenting(self): return True def add_active_whim_set(self, whim_set): self._whim_set_cache[whim_set] += 1 def remove_active_whim_set(self, whim_set): self._whim_set_cache[whim_set] -= 1 if self._whim_set_cache[whim_set] <= 0: del self._whim_set_cache[whim_set] @property def active_whim_sets(self): return set(self._whim_set_cache.keys())
class BaseStructure(GameElementBase): neighbourhoodThreshold = 2 REACTION_THRESHOLD = util.REACTION_THRESHOLD class Veil(object): def __init__(self, structure, time, stopCallback=None, fadeOutTime=120.0, fadeInTime=120.0): self._structure = structure self._timeToVeil = time self._maxFadeOutTime = fadeOutTime self._fadeOutTime = fadeOutTime self._maxFadeInTime = fadeInTime self._fadeInTime = fadeInTime self._stopCallback = stopCallback self._action = self._veiling def onTick(self): if self._action is not None: self._action() def _veiling(self): if self._fadeInTime > 0: self._fadeInTime -= 1 self._setVeil(self._fadeInTime / self._maxFadeInTime) else: self._action = self._veilHolding def _veilHolding(self): if self._timeToVeil > 0: self._timeToVeil -= 1 self._setVeil(0) else: self._action = self._unveiling def _unveiling(self): if self._fadeOutTime > 0: self._fadeOutTime -= 1 self._setVeil(1 - (self._fadeOutTime / self._maxFadeOutTime)) else: self._action = self._finished def _finished(self): self._removeVeil() self._stopCallback() self._action = None def _setVeil(self, value): fxNode = avg.HueSatFXNode(0, 100 * value - 100, 100 * value - 100, False) self._structure._image.setEffect(fxNode) def _removeVeil(self): self._structure._image.setEffect(None) def __init__(self, crystalManager=None, framesToGrowth=None, startCrystals=20, *args, **kwargs): GameElementBase.__init__(self, *args, **kwargs) if self.owner is not None: self.owner.addStructure(self) self._graph = nx.Graph() self._elementMapping = {} self._fixElements = WeakSet() if __debug__: print "FRAMESTO GROWTH", framesToGrowth self.framesToGrowth = framesToGrowth self.growTimer = 0 self._overDriveCounter = 0 self._overdrive = False self._veil = None self._growLock = False self._depletedCallback = None self._gameOverCallback = None self.crystalManager = crystalManager if crystalManager is None: logging.warn("Structure {!s} has no valid CrystalManager".format(self)) else: self.crystalManager.registerStructure(self) self._shadowWidth = util.CRYSTAL_SIZE * StructureElement.shapeOverflowFactor ** 2 if self.owner is None: self._shadowColor = "000000" else: self._shadowColor = self.owner.color player = avg.Player.get() self._canvas = player.createCanvas( id=str(id(self)), size=(max(util.WINDOW_SIZE), max(util.WINDOW_SIZE)), handleevents=True, multisamplesamples=4, ) self._blackCanvas = player.createCanvas( id=str(id(self)) + "Black", size=(max(util.WINDOW_SIZE), max(util.WINDOW_SIZE)), handleevents=True, multisamplesamples=4, ) self._canvasRoot = self._canvas.getRootNode() self._blackBackground = self._blackCanvas.getRootNode() self._shadowImage = avg.ImageNode( href="canvas:{}Black".format(id(self)), parent=self._root, size=(max(util.WINDOW_SIZE), max(util.WINDOW_SIZE)), opacity=0.4, ) util.centerNodeOnPosition(self._shadowImage, (0, 0)) self._graphVisRoot = avg.DivNode(parent=self._canvasRoot) self._image = avg.ImageNode( href="canvas:{}".format(id(self)), parent=self._root, size=(max(util.WINDOW_SIZE), max(util.WINDOW_SIZE)) ) util.centerNodeOnPosition(self._image, (0, 0)) self._edgeNodes = dict() self._shadowNodes = dict() self._initStructureCore() assert self.checkSanity() while len(self._graph) < startCrystals: self.growSimple() self.rotationEnabled = True self._tickTimer = None self._startTickTimer() def getOffscreenPosForElement(self, pos): return util.vectorAdd(pos, util.vectorMult(self._canvasRoot.size, 0.5)) def getElementNodeParent(self): return self._canvasRoot def _startTickTimer(self): self._stopTickTimer() player = avg.Player.get() self._tickTimer = player.setOnFrameHandler(self.onTick) def _stopTickTimer(self): player = avg.Player.get() if self._tickTimer is not None: player.clearInterval(self._tickTimer) self._tickTimer = None def delete(self): if not self.alive: return self._stopTickTimer() if self.owner is not None: self.owner.removeStructure(self) for element in self._elementMapping.values(): if element.node is not None: element.node.unlink(True) element.node = None self._elementMapping = None for node in self._edgeNodes.values(): node.unlink(True) for node in self._shadowNodes.values(): node.unlink(True) self._edgeNodes = None self._shadowNodes = None self._rootParent = None self._canvasRoot.unlink(True) self._canvasRoot = None self._shadowImage.unlink(True) self._shadowImage = None self._graphVisRoot.unlink(True) self._graphVisRoot = None self._image.unlink(True) self._image = None player = avg.Player.get() player.deleteCanvas(self._blackCanvas.getID()) player.deleteCanvas(self._canvas.getID()) self._blackCanvas = None self._canvas = None self._fixElements = None self.crystalManager.removeStructure(self) self.crystalManager = None GameElementBase.delete(self) def _initStructureCore(self): raise NotImplementedError def _updateElement(self, element): self._elementMapping[element.shape] = element self._elementMapping[element.node] = element self._elementMapping[element.id] = element def getElement(self, obj): return self._elementMapping[obj] def startOverdrive(self, duration): self._overDriveCounter += duration def _overdriveEnd(self): pass @property def size(self): return self._size def _initPhysic(self, position, angle): mass = pymunk.inf moment = pymunk.moment_for_circle(mass, 0, 1) self._body = physic.BaseBody(self, mass, moment) self._space.add(self._body) def _createCircleShape(self, absPos, r=util.CRYSTAL_RADIUS): circle = pymunk.Circle(self._body, r, self._body.world_to_local(absPos)) circle.collision_type = physic.StructureCollisionType self._addShape(circle) return circle @property def depletedCallback(self): return self._depletedCallback @depletedCallback.setter def depletedCallback(self, fun): self._depletedCallback = fun def veil(self, time): if self._veil is None: self._veil = BaseStructure.Veil(self, time, self._veilEnd) def _veilEnd(self): self._veil = None def getAllElements(self): return [self.getElement(id) for id in self._graph] def onTick(self): if self._overDriveCounter > 0: self._overdrive = True self._overDriveCounter -= 1 if self._overDriveCounter == 0: self._overdrive = False self._overdriveEnd() self.grow() if self._veil is not None: self._veil.onTick() @property def gameOverCallback(self): return self._gameOverCallback @gameOverCallback.setter def gameOverCallback(self, fun): self._gameOverCallback = fun def onWallCollision(self, other): if self._gameOverCallback is not None: self._gameOverCallback() self.gameOverCallback = None def checkSanity(self): return True def updateNeigbourhoodVisualisation(self): if __debug__: if not hasattr(self, "debugNodes"): self.debugNodes = [] while len(self.debugNodes) < self._graph.number_of_edges(): debugNode = avg.LineNode(parent=self._root) debugNode.color = "FF0000" debugNode.strokewidth = 2 debugNode.opacity = 0.5 debugNode.fillopacity = 0.5 self.debugNodes.append(debugNode) while len(self.debugNodes) > self._graph.number_of_edges(): self.debugNodes.pop().unlink(True) for edge, node in zip(self._graph.edges_iter(), self.debugNodes): nodeIdA, nodeIdB = edge node.unlink(False) node.pos1 = tuple(self.getElement(nodeIdA).shape.offset) node.pos2 = tuple(self.getElement(nodeIdB).shape.offset) self._root.appendChild(node) def removeSurplusElements(self, element): assert self.checkSanity() removeCounter = 0 sameNeighbors = self.getSameExtendedNeighborhood(element) if self.REACTION_THRESHOLD > 0 and len(sameNeighbors) >= self.REACTION_THRESHOLD: for neighbor in sameNeighbors: self.removeElement(neighbor) removeCounter += 1 assert self.checkSanity() removeCounter += self.removeNotReachableElements() return removeCounter def getGraphNeighbors(self, element): return [self.getElement(id) for id in self._graph.neighbors_iter(element.id)] def addElement(self, elementToCreate, color, relPos, rotation=0): assert self.checkSanity() if color is None: element = elementToCreate(relPos, rotation, self, self._helpSystem) else: element = elementToCreate(color, relPos, rotation, self, self._helpSystem) self._updateElement(element) self._addElementShadow(element) self._graph.add_node(element.id) assert self.checkSanity() self.updateNeigbors(element) assert self.checkSanity() return element def _removeEdgeNodesForElement(self, element): neighbors = self.getGraphNeighbors(element) for edge in self._graph.edges_iter([element.id] + neighbors): if element.id in edge: self._removeEdgeNodes(edge) def _addEdgeNodes(self, edge): edge = tuple(sorted(edge)) elementA = self.getElement(edge[0]) elementB = self.getElement(edge[1]) shadowLine = avg.LineNode(parent=self._blackBackground) shadowLine.color = self._shadowColor shadowLine.strokewidth = self._shadowWidth shadowLine.opacity = 1 shadowLine.fillopacity = 1 shadowLine.pos1 = self.getOffscreenPosForElement(elementA.position) shadowLine.pos2 = self.getOffscreenPosForElement(elementB.position) self._shadowNodes[edge] = shadowLine avg.LinearAnim(shadowLine, "opacity", 700, 0, 1).start() edgeLine = avg.LineNode(parent=self._graphVisRoot) edgeLine.color = "F88017" edgeLine.strokewidth = util.CRYSTAL_SIZE * 0.1 edgeLine.opacity = 1 edgeLine.fillopacity = 1 edgeLine.pos1 = self.getOffscreenPosForElement(elementA.position) edgeLine.pos2 = self.getOffscreenPosForElement(elementB.position) avg.LinearAnim(edgeLine, "opacity", 5000, 0, 1).start() self._edgeNodes[edge] = edgeLine def _removeEdgeNodes(self, edge): edge = tuple(sorted(edge)) if edge in self._edgeNodes: edgeNode = self._edgeNodes[edge] avg.LinearAnim( edgeNode, "opacity", 100, edgeNode.opacity, 0, False, None, lambda: edgeNode.unlink(True) ).start() del self._edgeNodes[edge] if edge in self._shadowNodes: shadowNode = self._shadowNodes[edge] avg.LinearAnim( shadowNode, "opacity", 700, shadowNode.opacity, 0, False, None, lambda: shadowNode.unlink(True) ).start() self._shadowNodes[edge].unlink(True) del self._shadowNodes[edge] def _addElementShadow(self, element): roundShadow = avg.CircleNode( parent=self._blackBackground, pos=self.getOffscreenPosForElement(element.position), r=self._shadowWidth / 2, fillcolor=self._shadowColor, opacity=0, fillopacity=1, ) avg.LinearAnim(roundShadow, "fillopacity", 700, 0, 1).start() self._shadowNodes[element.shape] = roundShadow def _removeElementShadow(self, element): self._shadowNodes[element.shape].unlink(True) del self._shadowNodes[element.shape] def onCrystalCollision(self, other, hitShape): try: element = self.getElement(hitShape) except KeyError: return element.onCollision(other) def getColorCount(self): return collections.Counter([e.color for e in self._elementMapping.values()]) def removeElement(self, element): assert self.checkSanity() assert isinstance(element, StructureElement) assert element.parent == self element.onDestroyAction() self._removeEdgeNodesForElement(element) self._removeElementShadow(element) self._graph.remove_node(element.id) self._removeShape(element.shape) if element in self._fixElements: self._fixElements.remove(element) # avg.LinearAnim(element.node, "opacity",1000, 1 , 0, False, None, lambda:element.node.unlink(True)).start() # if self._fixElements: targetPos = random.choice(list(self._fixElements)).node.pos avg.LinearAnim( element.node, "pos", 1000, element.node.pos, targetPos, False, None, lambda: element.node.unlink(True) ).start() else: avg.LinearAnim(element.node, "opacity", 1000, 1, 0, False, None, lambda: element.node.unlink(True)).start() del self._elementMapping[element.shape] del self._elementMapping[element.node] del self._elementMapping[element.id] assert self.checkSanity() self._checkDepleted() if __debug__: self.updateNeigbourhoodVisualisation() def updateNeigbors(self, element, reset=False): assert self.checkSanity() assert isinstance(element, StructureElement) if reset: self._removeEdgeNodesForElement(element) self._graph.remove_node(element.id) self._graph.add_node(element.id) assert len(self._graph.neighbors(element.id)) == 0 for shape in self.getPhysicNeigbors(element): shapeId = self.getElement(shape).id if shapeId in self._graph: self._graph.add_edge(element.id, shapeId) self._addEdgeNodes((element.id, shapeId)) assert self.checkSanity() if __debug__: self.updateNeigbourhoodVisualisation() def getPhysicNeigbors(self, element): shape = element.shape testBody = pymunk.Body() testBody.position = self._body.local_to_world(shape.offset) toTest = pymunk.Circle(testBody, self.neighbourhoodThreshold * util.CRYSTAL_SIZE / 2) result = self._space.shape_query(toTest) result = filter(lambda s: s in self._shapes, result) return result def randomizeNeighbors(self, element): spatialInfo = [] for element in list(self.getGraphNeighbors(element)): spatialInfo.append((element.position, element.node.angle)) self.removeElement(element) for pos, angle in spatialInfo: color, toCreate = self.crystalManager.getNextStructureElement(self) self.addElement(toCreate, color, pos, angle) def _checkDepleted(self): if len(self._graph) == 0 and self.depletedCallback is not None: self._depletedCallback() self._depletedCallback = None def removeNeighbors(self, element): for element in list(self.getGraphNeighbors(element)): self.removeElement(element) def _swapShapes(self, elementA, elementB): assert self.checkSanity() if (elementA in self._fixElements) ^ (elementB in self._fixElements): if elementA in self._fixElements: self._fixElements.remove(elementA) self._fixElements.add(elementB) elif elementB in self._fixElements: self._fixElements.remove(elementB) self._fixElements.add(elementA) elementA.shape, elementB.shape = elementB.shape, elementA.shape self._updateElement(elementA) self._updateElement(elementB) assert self.checkSanity() def getSameExtendedNeighborhood(self, element): assert self.checkSanity() neighbourFilter = filter(lambda otherId: element.isSameType(self.getElement(otherId)), self._graph) subGraph = self._graph.subgraph(neighbourFilter) for graph in nx.connected_components(subGraph): if element.id in graph: assert self.checkSanity() return [self.getElement(i) for i in graph] else: assert self.checkSanity() return [] def removeNotReachableElements(self): toRemove = [] removeCounter = 0 for graph in nx.connected_component_subgraphs(self._graph): if all(element.id not in graph for element in self._fixElements): toRemove.append(graph) for graph in toRemove: for element in [self.getElement(i) for i in graph]: self.removeElement(element) removeCounter += 1 return removeCounter # def writeState(self): # assert(self.checkSanity()) # nodes = [] # for i, node in self._idNodeMapping.items(): # if node is not self: # nodes.append((i, # node.itemType, # node.pos.x, # node.pos.y, # node.shape.offset[0], # node.shape.offset[1])) # else: # nodes.append((0, # node.itemType, # node.pos.x, # node.pos.y, # 0,0)) # # edges= [] # for a,b in self._graph.edges_iter(): # if a == id(self): a=0 # if b == id(self): b=0 # edges.append((a,b)) # # with open("test", "w") as outFile: # pickle.dump((nodes,edges), outFile) # assert(self.checkSanity()) # # def loadState(self, filename): # assert(self.checkSanity()) # nodes, edges = pickle.load(filename) # # nodeMapping = dict() # # for i, crystalType, posX, posY, offsetX, offsetY in nodes: # if i == 0: # nodeMapping[i] = self # self._graph.add_node(id(self)) # else: # nodeMapping[i] = self.addElement(crystalType, # (posX,posY), # (offsetX, offsetY), # False) # self._idNodeMapping[id(nodeMapping[i])]= nodeMapping[i] # # for a,b in edges: # self._graph.add_edge(id(nodeMapping[a]), id(nodeMapping[b]) ) # assert(self.checkSanity()) # def grow(self): if self._growLock: return if self._overdrive: pass elif self.framesToGrowth is None: return elif self.growTimer < self.framesToGrowth: self.growTimer += 1 return else: self.growTimer = 0 newNode = self.growOutwards() # self.growSimple() def growOutwards(self): newSpot, elementChain = self.getRandomDestinationPath() if newSpot is None: return color, crystalType = self.getElementTypeToGrow() newElement = self.addElement(crystalType, color, newSpot) for element in reversed(elementChain[1:]): self._swapShapes(element, newElement) for element in elementChain[1:]: self.updateNeigbors(element, True) self.updateNeigbors(newElement, True) newElement.node.pos = elementChain[0].node.pos newPositions = itertools.chain( [element.node.pos for element in elementChain[1:]], (self.getOffscreenPosForElement(newSpot),) ) elementChain = itertools.chain((newElement,), elementChain[1:]) self._growLock = True for element, newPosition in zip(elementChain, newPositions): avg.LinearAnim(element.node, "pos", 1000, element.node.pos, newPosition).start() avg.LinearAnim(newElement.node, "opacity", 1000, 0, 1, False, None, self._resetGrowLock).start() assert self.checkSanity() def _resetGrowLock(self): self._growLock = False def getRandomDestinationPath(self): newSpot, targetElement = self.searchSpot() if targetElement is None: return None, None assert isinstance(targetElement, StructureElement) paths = [] for fixElement in self._fixElements: try: path = nx.shortest_path(self._graph, fixElement.id, targetElement.id) paths.append(path) except nx.exception.NetworkXNoPath: pass if not paths: return None, None shortestPath = min(paths, key=len) return newSpot, map(lambda x: self.getElement(x), shortestPath) def growSimple(self): newSpot, adjacentElement = self.searchSpot() if newSpot: color, crystalType = self.getElementTypeToGrow() element = self.addElement(crystalType, color, relPos=newSpot) return element def getElementTypeToGrow(self): return self.crystalManager.getNextStructureElement(self) def removeElementsInArea(self, pos, radius): deletedelements = 0 for shape in self._getShapesInCircle(pos, radius): if shape in self._elementMapping: deletedelements += self.getElement(shape).delete() return deletedelements def _getShapesInCircle(self, pos, radius): testBody = pymunk.Body() testBody.position = pos shapeToTest = pymunk.Circle(body=testBody, radius=radius) intersections = self._space.shape_query(shapeToTest) return intersections def searchSpot(self): if self._graph.nodes(): spots = [] if not self._fixElements: return None, None fixelement = random.choice(list(self._fixElements)) spots = self.checkForSpace(fixelement) spots = self._filterSpots(spots, fixelement) if spots: return random.choice(spots), fixelement node = random.choice(self._graph.nodes()) element = self.getElement(node) spots = self.checkForSpace(element) spots = self._filterSpots(spots, element) if spots: return random.choice(spots), element return None, None def _filterSpots(self, spots, origin): return spots def checkForSpace(self, element): radius = element.shape.radius + util.CRYSTAL_RADIUS + 1 availablePositions = [] stepsize = 360 // 4 maxNeigbors = 0 for alpha in range(0, 360, stepsize): alpha += random.randint(0, stepsize - 1) # alpha+= random.randrange(stepsize) angle = (2 * math.pi) * (alpha / 360.0) vector = util.getVectotInDirection(angle, radius) posToCheck = util.vectorAdd(self.toAbsPos(element.position), vector) testBody = pymunk.Body() testBody.position = posToCheck shapeToTestInner = pymunk.Circle(body=testBody, radius=util.CRYSTAL_SIZE / 2) shapeToTestOuter = pymunk.Circle( body=testBody, radius=(util.CRYSTAL_SIZE / 2) * self.neighbourhoodThreshold ) intersectionsInner = self._space.shape_query(shapeToTestInner) # dnode = avg.CircleNode(parent=self._root.getParent(), pos=posToCheck, r=util.CRYSTAL_SIZE/2,fillcolor="00FFFF", strokewidth=0) if len(intersectionsInner) == 0 or (len(intersectionsInner) == 1 and element.shape in intersectionsInner): intersectionsOuter = self._space.shape_query(shapeToTestOuter) neighborCount = len(intersectionsOuter) # dnode.fillopacity=0.5 if neighborCount > maxNeigbors: maxNeigbors = neighborCount availablePositions = [self.toRelPos(posToCheck)] elif neighborCount == maxNeigbors: availablePositions.append(self.toRelPos(posToCheck)) return availablePositions
class BroadcasterService(Service): __qualname__ = 'BroadcasterService' 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._pending_update = False self._quadtrees = defaultdict(sims4.geometry.QuadTree) def start(self): self._alarm_handle = add_alarm_real_time(self, interval_in_real_seconds( self.INTERVAL), self._on_update, repeating=True, use_sleep_time=False) 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._update_object_cache) 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._update_object_cache) def add_broadcaster(self, broadcaster): if broadcaster not in self._pending_broadcasters: self._pending_broadcasters.append(broadcaster) 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 _update_object_cache(self, obj=None): if obj is None: self._object_cache = None return if self._object_cache is not None: self._object_cache.add(obj) def _is_valid_broadcaster(self, broadcaster): broadcasting_object = broadcaster.broadcasting_object if broadcasting_object is None: return False if broadcasting_object.is_in_inventory(): return False if 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: while broadcaster.guid == broadcaster_type.guid: if broadcaster.should_cluster( ) and broadcaster.routing_surface.secondary_id == broadcaster_level: yield broadcaster def get_broadcasters_gen(self, inspect_only=False): for (cluster_request_key, cluster_request) in self._cluster_requests.items(): is_cluster_dirty = cluster_request.is_dirty() if is_cluster_dirty: for broadcaster in self._get_broadcasters_for_cluster_request_gen( *cluster_request_key): broadcaster.regenerate_constraint() while not is_cluster_dirty or not inspect_only: while True: for cluster in cluster_request.get_clusters_gen(): broadcaster_iter = cluster.objects_gen() master_broadcaster = next(broadcaster_iter) master_broadcaster.set_linked_broadcasters( list(broadcaster_iter)) yield master_broadcaster for broadcaster in self._active_broadcasters: while not broadcaster.should_cluster(): if self._is_valid_broadcaster(broadcaster): yield broadcaster def get_pending_broadcasters_gen(self): yield self._pending_broadcasters def _get_all_objects_gen(self): if any(broadcaster.allow_objects for broadcaster in self._active_broadcasters): if self._object_cache is None: self._object_cache = WeakSet( services.object_manager().valid_objects()) yield list(self._object_cache) else: self._object_cache = None yield 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 update(self): if self._pending_update: self._pending_update = False self._update() def _update(self): try: self._activate_pending_broadcasters() current_broadcasters = set(self.get_broadcasters_gen()) for obj in self._get_all_objects_gen(): is_affected = False for broadcaster in current_broadcasters: while broadcaster.can_affect(obj): constraint = broadcaster.get_constraint() if not constraint.valid: pass if constraint.geometry is None or constraint.geometry.contains_point( obj.position ) and constraint.routing_surface == obj.routing_surface: broadcaster.apply_broadcaster_effect(obj) is_affected = True while not is_affected: if self._object_cache is not None: self._object_cache.remove(obj) for broadcaster in current_broadcasters: broadcaster.on_processed() finally: self._on_update_callbacks()
class ConnectionObject: def __init__(self, type, source, destination, save_id=None): self.__type = type self.__data = type.ufl_type.build_default(None) self.__source = ref(source) self.__destination = ref(destination) self.__visuals = WeakSet() self.__cache = ModelTemporaryDataCache(None) if save_id is None: self.__save_id = uuid4() else: self.__save_id = save_id def add_visual(self, visual): if visual.object is not self: raise Exception self.__visuals.add(visual) def remove_visual(self, visual): self.__visuals.remove(visual) @property def visuals(self): yield from self.__visuals @property def cache(self): return self.__cache @property def type(self): return self.__type @property def data(self): return self.__data @property def source(self): return self.__source() @property def destination(self): return self.__destination() @property def project(self): return self.__source().project def reverse(self): self.__source, self.__destination = self.__destination, self.__source for visual in self.__visuals: visual._reverse() self.__cache.invalidate() def get_other_end(self, element): if self.__source() is element: return self.__destination() elif self.__destination() is element: return self.__source() else: return None def is_connected_with(self, element): return self.__source() is element or self.__destination() is element @property def save_id(self): return self.__save_id def create_appearance_object(self, ruler): return self.__type.create_appearance_object(self, ruler) def create_label_object(self, id, ruler): return self.__type.get_label(id).create_appearance_object(self, ruler) def apply_ufl_patch(self, patch): self.__data.apply_patch(patch) self.__cache.refresh() @property def has_ufl_dialog(self): return self.__type.ufl_type.has_attributes def create_ufl_dialog(self, options=UflDialogOptions.standard): if not self.__type.ufl_type.has_attributes: raise Exception dialog = UflDialog(self.__type.ufl_type, options) dialog.associate(self.__data) return dialog
class BroadcasterService(Service): __qualname__ = 'BroadcasterService' 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._pending_update = False self._quadtrees = defaultdict(sims4.geometry.QuadTree) def start(self): self._alarm_handle = add_alarm_real_time(self, interval_in_real_seconds(self.INTERVAL), self._on_update, repeating=True, use_sleep_time=False) 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._update_object_cache) 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._update_object_cache) def add_broadcaster(self, broadcaster): if broadcaster not in self._pending_broadcasters: self._pending_broadcasters.append(broadcaster) 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 _update_object_cache(self, obj=None): if obj is None: self._object_cache = None return if self._object_cache is not None: self._object_cache.add(obj) def _is_valid_broadcaster(self, broadcaster): broadcasting_object = broadcaster.broadcasting_object if broadcasting_object is None: return False if broadcasting_object.is_in_inventory(): return False if 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: while broadcaster.guid == broadcaster_type.guid: if broadcaster.should_cluster() and broadcaster.routing_surface.secondary_id == broadcaster_level: yield broadcaster def get_broadcasters_gen(self, inspect_only=False): for (cluster_request_key, cluster_request) in self._cluster_requests.items(): is_cluster_dirty = cluster_request.is_dirty() if is_cluster_dirty: for broadcaster in self._get_broadcasters_for_cluster_request_gen(*cluster_request_key): broadcaster.regenerate_constraint() while not is_cluster_dirty or not inspect_only: while True: for cluster in cluster_request.get_clusters_gen(): broadcaster_iter = cluster.objects_gen() master_broadcaster = next(broadcaster_iter) master_broadcaster.set_linked_broadcasters(list(broadcaster_iter)) yield master_broadcaster for broadcaster in self._active_broadcasters: while not broadcaster.should_cluster(): if self._is_valid_broadcaster(broadcaster): yield broadcaster def get_pending_broadcasters_gen(self): yield self._pending_broadcasters def _get_all_objects_gen(self): if any(broadcaster.allow_objects for broadcaster in self._active_broadcasters): if self._object_cache is None: self._object_cache = WeakSet(services.object_manager().valid_objects()) yield list(self._object_cache) else: self._object_cache = None yield 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 update(self): if self._pending_update: self._pending_update = False self._update() def _update(self): try: self._activate_pending_broadcasters() current_broadcasters = set(self.get_broadcasters_gen()) for obj in self._get_all_objects_gen(): is_affected = False for broadcaster in current_broadcasters: while broadcaster.can_affect(obj): constraint = broadcaster.get_constraint() if not constraint.valid: pass if constraint.geometry is None or constraint.geometry.contains_point(obj.position) and constraint.routing_surface == obj.routing_surface: broadcaster.apply_broadcaster_effect(obj) is_affected = True while not is_affected: if self._object_cache is not None: self._object_cache.remove(obj) for broadcaster in current_broadcasters: broadcaster.on_processed() finally: self._on_update_callbacks()
class Ensemble(metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(sims4.resources.Types.ENSEMBLE)): ENSEMBLE_PRIORITIES = TunableList(description='\n A list of ensembles by priority. Those with higher guids will be\n considered more important than those with lower guids.\n \n IMPORTANT: All ensemble types must be referenced in this list.\n ', tunable=TunableReference(description='\n A single ensemble.\n ', manager=services.get_instance_manager(sims4.resources.Types.ENSEMBLE), pack_safe=True)) @staticmethod def get_ensemble_priority(ensemble_type): index = 0 for ensemble in Ensemble.ENSEMBLE_PRIORITIES: if ensemble is ensemble_type: return index index += 1 logger.error('Ensemble of type {} not found in Ensemble Priorities. Please add the ensemble to ensemble.ensemble.', ensemble_type) INSTANCE_TUNABLES = {'max_ensemble_radius': TunableDistanceSquared(description="\n The maximum distance away from the center of mass that Sims will\n receive an autonomy bonus for.\n \n If Sims are beyond this distance from the ensemble's center of mass,\n then they will autonomously consider to run any interaction from\n ensemble_autonomous_interactions.\n \n Any such interaction will have an additional constraint that is a\n circle whose radius is this value.\n ", default=1.0), 'ensemble_autonomy_bonus_multiplier': TunableRange(description='\n The autonomy multiplier that will be applied for objects within the\n autonomy center of mass.\n ', tunable_type=float, default=2.0, minimum=1.0), 'ensemble_autonomous_interactions': TunableSet(description="\n This is a set of self interactions that are generated for Sims part \n of this ensemble.\n \n The interactions don't target anything and have an additional\n constraint equivalent to the circle defined by the ensemble's center\n of mass and radius.\n ", tunable=TunableReference(manager=services.get_instance_manager(sims4.resources.Types.INTERACTION), pack_safe=True)), 'visible': Tunable(description='\n If this ensemble is visible and displays to the UI.\n ', tunable_type=bool, default=True), 'rally': Tunable(description='\n If this is True then this ensemble will offer rallying behavior.\n ', tunable_type=bool, default=True), 'center_of_mass_multiplier': TunableMultiplier.TunableFactory(description="\n Define multipliers that control the weight that a Sim has when\n determining the ensemble's center of mass.\n "), 'max_limit': OptionalTunable(description='\n If enabled this ensemble will have a maximum number of Sims that\n can be a part of it.\n ', tunable=TunableRange(description='\n The maximum number of Sims that can be in this ensemble.\n ', tunable_type=int, default=8, minimum=2)), 'prohibited_species': TunableSet(description='\n A set of species that cannot be added to this type of ensemble.\n ', tunable=TunableEnumEntry(description='\n A species that cannot be added to this type of ensemble.\n ', tunable_type=Species, default=Species.HUMAN, invalid_enums=(Species.INVALID,)))} def __init__(self): self._guid = None self._sims = WeakSet() def __iter__(self): yield from self._sims def __len__(self): return len(self._sims) @property def guid(self): return self._guid @classmethod def can_add_sim_to_ensemble(cls, sim): if sim.species in cls.prohibited_species: return False return True def add_sim_to_ensemble(self, sim): if sim in self._sims: return self._sims.add(sim) if self.ensemble_autonomous_interactions: sim_info_utils.apply_super_affordance_commodity_flags(sim, self, self.ensemble_autonomous_interactions) if self.visible: op = UpdateEnsemble(self._guid, sim.id, True) Distributor.instance().add_op_with_no_owner(op) def remove_sim_from_ensemble(self, sim): self._sims.remove(sim) if self.ensemble_autonomous_interactions: sim_info_utils.remove_super_affordance_commodity_flags(sim, self) if self.visible: op = UpdateEnsemble(self._guid, sim.id, False) Distributor.instance().add_op_with_no_owner(op) def is_sim_in_ensemble(self, sim): return sim in self._sims def start_ensemble(self): self._guid = id_generator.generate_object_id() if self.visible: op = StartEnsemble(self._guid) Distributor.instance().add_op_with_no_owner(op) def end_ensemble(self): if self.ensemble_autonomous_interactions: for sim in self._sims: sim_info_utils.remove_super_affordance_commodity_flags(sim, self) self._sims.clear() if self.visible: op = EndEnsemble(self._guid) Distributor.instance().add_op_with_no_owner(op) @cached def _get_sim_weight(self, sim): return self.center_of_mass_multiplier.get_multiplier(SingleSimResolver(sim.sim_info)) @cached def calculate_level_and_center_of_mass(self): sims_per_level = defaultdict(list) for sim in self._sims: sims_per_level[sim.level].append(sim) best_level = max(sims_per_level, key=lambda level: (len(sims_per_level[level]), -level)) best_sims = sims_per_level[best_level] center_of_mass = sum((sim.position*self._get_sim_weight(sim) for sim in best_sims), sims4.math.Vector3.ZERO())/sum(self._get_sim_weight(sim) for sim in best_sims) return (best_level, center_of_mass) def is_within_ensemble_radius(self, obj): (level, center_of_mass) = self.calculate_level_and_center_of_mass() if obj.level != level: return False else: distance = (obj.position - center_of_mass).magnitude_squared() if distance > self.max_ensemble_radius: return False return True @cached def get_ensemble_multiplier(self, target): if self.is_within_ensemble_radius(target): return self.ensemble_autonomy_bonus_multiplier return 1 def get_center_of_mass_constraint(self): if not self: logger.warn('No Sims in ensemble when trying to construct constraint.') return ANYWHERE (level, position) = self.calculate_level_and_center_of_mass() routing_surface = routing.SurfaceIdentifier(services.current_zone_id(), level, routing.SurfaceType.SURFACETYPE_WORLD) return Circle(position, sqrt(self.max_ensemble_radius), routing_surface) def get_ensemble_autonomous_interactions_gen(self, context, **interaction_parameters): if self.is_within_ensemble_radius(context.sim): return for ensemble_affordance in self.ensemble_autonomous_interactions: affordance = EnsembleConstraintProxyInteraction.generate(ensemble_affordance, self) yield from affordance.potential_interactions(context.sim, context, **interaction_parameters)
class ObjectManager(DistributableObjectManager): __qualname__ = 'ObjectManager' FIREMETER_DISPOSABLE_OBJECT_CAP = Tunable(int, 5, description='Number of disposable objects a lot can have at any given moment.') BED_TAGS = TunableTuple(description='\n Tags to check on an object to determine what type of bed an object is.\n ', beds=TunableSet(description='\n Tags that consider an object as a bed other than double beds.\n ', tunable=TunableEnumWithFilter(tunable_type=tag.Tag, default=tag.Tag.INVALID, filter_prefixes=BED_PREFIX_FILTER)), double_beds=TunableSet(description='\n Tags that consider an object as a double bed\n ', tunable=TunableEnumWithFilter(tunable_type=tag.Tag, default=tag.Tag.INVALID, filter_prefixes=BED_PREFIX_FILTER)), kid_beds=TunableSet(description='\n Tags that consider an object as a kid bed\n ', tunable=TunableEnumWithFilter(tunable_type=tag.Tag, default=tag.Tag.INVALID, filter_prefixes=BED_PREFIX_FILTER)), other_sleeping_spots=TunableSet(description='\n Tags that considered sleeping spots.\n ', tunable=TunableEnumWithFilter(tunable_type=tag.Tag, default=tag.Tag.INVALID, filter_prefixes=BED_PREFIX_FILTER))) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._crafting_cache = CraftingObjectCache() self._sim_spawn_conditions = collections.defaultdict(set) self._client_connect_callbacks = CallableList() self._portal_cache = WeakSet() self._portal_added_callbacks = CallableList() self._portal_removed_callbacks = CallableList() self._front_door_candidates_changed_callback = CallableList() self._all_bed_tags = self.BED_TAGS.beds | self.BED_TAGS.double_beds | self.BED_TAGS.kid_beds | self.BED_TAGS.other_sleeping_spots @property def crafting_cache(self): return self._crafting_cache def portal_cache_gen(self): yield self._portal_cache def on_client_connect(self, client): all_objects = list(self._objects.values()) for game_object in all_objects: game_object.on_client_connect(client) def move_to_inventory(self, obj, inventory_manager): del self._objects[obj.id] obj.manager = inventory_manager inventory_manager._objects[obj.id] = obj if hasattr(obj, 'on_added_to_inventory'): obj.on_added_to_inventory() def add(self, obj, *args, **kwargs): super().add(obj, *args, **kwargs) if obj.objectage_component is None: current_zone = services.current_zone() current_zone.increment_object_count(obj) current_zone.household_manager.increment_household_object_count(obj.get_household_owner_id()) def remove(self, obj, *args, **kwargs): super().remove(obj, *args, **kwargs) if obj.objectage_component is None: current_zone = services.current_zone() current_zone.decrement_object_count(obj) current_zone.household_manager.decrement_household_object_count(obj.get_household_owner_id()) def _should_save_object_on_lot(self, obj): parent = obj.parent if parent is not None and parent.is_sim: if obj.can_go_in_inventory_type(InventoryType.SIM): return False return True def pre_save(self): all_objects = list(self._objects.values()) lot = services.current_zone().lot for (_, inventory) in lot.get_all_object_inventories_gen(): for game_object in inventory: all_objects.append(game_object) for game_object in all_objects: game_object.update_all_commodities() def save(self, object_list=None, zone_data=None, open_street_data=None, **kwargs): if object_list is None: return open_street_objects = file_serialization.ObjectList() total_beds = 0 double_bed_exist = False kid_bed_exist = False alternative_sleeping_spots = 0 for game_object in self._objects.values(): while self._should_save_object_on_lot(game_object): if game_object.persistence_group == objects.persistence_groups.PersistenceGroups.OBJECT: save_result = game_object.save_object(object_list.objects, ItemLocation.ON_LOT, 0) else: if game_object.item_location == ItemLocation.ON_LOT or game_object.item_location == ItemLocation.INVALID_LOCATION: item_location = ItemLocation.FROM_OPEN_STREET else: item_location = game_object.item_location save_result = game_object.save_object(open_street_objects.objects, item_location, 0) if not save_result: pass if zone_data is None: pass def_build_buy_tags = game_object.definition.build_buy_tags if not def_build_buy_tags & self._all_bed_tags: pass if def_build_buy_tags & self.BED_TAGS.double_beds: double_bed_exist = True total_beds += 1 elif def_build_buy_tags & self.BED_TAGS.kid_beds: total_beds += 1 kid_bed_exist = True elif def_build_buy_tags & self.BED_TAGS.other_sleeping_spots: alternative_sleeping_spots += 1 elif def_build_buy_tags & self.BED_TAGS.beds: total_beds += 1 if open_street_data is not None: open_street_data.objects = open_street_objects if zone_data is not None: bed_info_data = gameplay_serialization.ZoneBedInfoData() bed_info_data.num_beds = total_beds bed_info_data.double_bed_exist = double_bed_exist bed_info_data.kid_bed_exist = kid_bed_exist bed_info_data.alternative_sleeping_spots = alternative_sleeping_spots zone_data.gameplay_zone_data.bed_info_data = bed_info_data lot = services.current_zone().lot for (inventory_type, inventory) in lot.get_all_object_inventories_gen(): for game_object in inventory: game_object.save_object(object_list.objects, ItemLocation.OBJECT_INVENTORY, inventory_type) def valid_objects(self): return [obj for obj in self._objects.values() if not obj._hidden_flags] def get_objects_of_type_gen(self, *definitions): for obj in self._objects.values(): while any(obj.definition is d for d in definitions): yield obj def get_objects_with_tag_gen(self, tag): for obj in self._objects.values(): while build_buy.get_object_has_tag(obj.definition.id, tag): yield obj def add_sim_spawn_condition(self, sim_id, callback): for sim in services.sim_info_manager().instanced_sims_gen(): while sim.id == sim_id: logger.error('Sim {} is already in the world, cannot add the spawn condition', sim) return self._sim_spawn_conditions[sim_id].add(callback) def remove_sim_spawn_condition(self, sim_id, callback): if callback not in self._sim_spawn_conditions.get(sim_id, ()): logger.error('Trying to remove sim spawn condition with invalid id-callback pair ({}-{}).', sim_id, callback) return self._sim_spawn_conditions[sim_id].remove(callback) def trigger_sim_spawn_condition(self, sim_id): if sim_id in self._sim_spawn_conditions: for callback in self._sim_spawn_conditions[sim_id]: callback() del self._sim_spawn_conditions[sim_id] def register_portal_added_callback(self, callback): if callback not in self._portal_added_callbacks: self._portal_added_callbacks.append(callback) def unregister_portal_added_callback(self, callback): if callback in self._portal_added_callbacks: self._portal_added_callbacks.remove(callback) def register_portal_removed_callback(self, callback): if callback not in self._portal_removed_callbacks: self._portal_removed_callbacks.append(callback) def unregister_portal_removed_callback(self, callback): if callback in self._portal_removed_callbacks: self._portal_removed_callbacks.remove(callback) def add_portal_to_cache(self, portal): self._portal_cache.add(portal) self._portal_added_callbacks(portal) def remove_portal_from_cache(self, portal): self._portal_cache.remove(portal) self._portal_removed_callbacks(portal) def register_front_door_candidates_changed_callback(self, callback): if callback not in self._front_door_candidates_changed_callback: self._front_door_candidates_changed_callback.append(callback) def unregister_front_door_candidates_changed_callback(self, callback): if callback in self._front_door_candidates_changed_callback: self._front_door_candidates_changed_callback.remove(callback) def on_front_door_candidates_changed(self): self._front_door_candidates_changed_callback() def advertising_objects_gen(self, motives:set=DEFAULT): if not motives: return if motives is DEFAULT: for obj in self.valid_objects(): while obj.commodity_flags: yield obj return for obj in self.valid_objects(): while obj.commodity_flags & motives: yield obj def get_all_objects_with_component_gen(self, component): if component is None: return for obj in self.valid_objects(): if obj.has_component(component.instance_attr): yield obj else: while obj.has_component(component.class_attr): yield obj def on_location_changed(self, obj): self._registered_callbacks[CallbackTypes.ON_OBJECT_LOCATION_CHANGED](obj) @classproperty def supports_parenting(self): return True