class RoutingComponent(Component, HasTunableFactory, AutoFactoryInit, component_name=types.ROUTING_COMPONENT): _pathplan_context = None FACTORY_TUNABLES = {'plan_context_data': TunableTuple(description='\n Data used to populate fields on the path plan context.\n ', default_context=PathPlanContextWrapper.TunableFactory(description="\n If no age override is specified, the default path plan data to\n use for this agent's path planning.\n "), context_age_species_overrides=TunableList(description='\n List of age-species path plan context overrides for a specific\n routing agent.\n ', tunable=TunableTuple(description='\n Overrides to the path plan context of the agent defined by a\n combination of age and species.\n ', age=TunableEnumEntry(description='\n The age this override applies to.\n ', tunable_type=Age, default=Age.ADULT), species=TunableEnumEntry(description='\n The species this override applies to.\n ', tunable_type=SpeciesExtended, default=SpeciesExtended.HUMAN, invalid_enums=(SpeciesExtended.INVALID,)), context_override=PathPlanContextWrapper.TunableFactory()))), 'walkstyle_behavior': WalksStyleBehavior.TunableFactory(description='\n Define the walkstyle behavior for owners of this component.\n '), 'object_routing_component': OptionalTunable(description='\n If enabled, this object will have an Object Routing component, which\n controls an object routing behavior based on triggered states.\n ', tunable=ObjectRoutingComponent.TunableFactory())} def __init__(self, owner, **kwargs): super().__init__(owner, **kwargs) self.owner = owner walkstyle_behavior = self.get_walkstyle_behavior() self._walkstyle_requests = [WalkStyleRequest(self.owner, walkstyle=walkstyle_behavior.default_walkstyle, priority=-1)] self._walk_style_handles = {} self.wading_buff_handle = None self.last_route_has_wading_nodes = False self._routing_stage_event_callbacks = defaultdict(CallableList) if owner.is_sim: owner.remove_component(objects.components.types.FOOTPRINT_COMPONENT) self._path_plan_context_map = {} self.on_slot = None self.stand_slot_reservation_removed_callbacks = CallableList() self._active_follow_path_weakref = None self.on_follow_path = CallableList() self.on_plan_path = CallableList() self.on_intended_location_changed = CallableList() self._current_path = None self._routing_slave_data = [] self._routing_master_ref = None self._default_agent_radius = None self._route_event_context = RouteEventContext() self._route_interaction = None self._animation_context = None self._initial_carry_targets = None self._route_event_provider_requests = None def get_subcomponents_gen(self): yield from super().get_subcomponents_gen() if self.object_routing_component is not None: object_routing_component = self.object_routing_component(self.owner) yield from object_routing_component.get_subcomponents_gen() @property def current_path(self): return self._current_path @property def object_radius(self): return self._pathplan_context.agent_radius @property def route_event_context(self): return self._route_event_context @componentmethod def register_routing_stage_event(self, routing_stage_event, callback): self._routing_stage_event_callbacks[routing_stage_event].register(callback) def _on_routing_stage_event(self, routing_stage_event, **kwargs): callbacks = self._routing_stage_event_callbacks.get(routing_stage_event) if callbacks is not None: callbacks(self.owner, routing_stage_event, **kwargs) @contextmanager def temporary_walkstyle_request(self, walkstyle_request_factory): try: request = walkstyle_request_factory(self.owner) request.start() yield None finally: request.stop() @componentmethod def unregister_routing_stage_event(self, routing_stage_event, callback): self._routing_stage_event_callbacks[routing_stage_event].unregister(callback) @componentmethod def get_walkstyle_behavior(self): return self.walkstyle_behavior @componentmethod def get_cost_valid_walkstyle(self, cost_tuning): for walkstyle in self._walkstyle_requests: if walkstyle.priority != WalkStylePriority.COMBO: walkstyle_cost = cost_tuning.get(walkstyle.walkstyle, None) if walkstyle_cost is None: return walkstyle.walkstyle current_value = self.owner.get_stat_value(walkstyle_cost.walkstyle_cost_statistic) if current_value - walkstyle_cost.cost > walkstyle_cost.walkstyle_cost_statistic.min_value: return walkstyle.walkstyle walkstyle_behavior = self.get_walkstyle_behavior() return walkstyle_behavior.default_walkstyle @componentmethod def get_default_walkstyle(self): return self.walkstyle_behavior.get_default_walkstyle(self.owner) @componentmethod def get_walkstyle(self): for walkstyle in self._walkstyle_requests: if walkstyle.priority != WalkStylePriority.COMBO: return walkstyle.walkstyle walkstyle_behavior = self.get_walkstyle_behavior() return walkstyle_behavior.default_walkstyle @componentmethod def get_walkstyle_for_path(self, path): return self.walkstyle_behavior.get_walkstyle_for_path(self.owner, path) @componentmethod def get_walkstyle_list(self): return tuple(request.walkstyle for request in self._walkstyle_requests) @componentmethod def get_walkstyle_requests(self): return self._walkstyle_requests def _get_walkstyle_key(self): if self.owner.is_sim: return (self.owner.age, self.owner.gender, self.owner.extended_species) return (Age.ADULT, Gender.MALE, SpeciesExtended.HUMAN) @componentmethod def request_walkstyle(self, walkstyle_request, uid): if walkstyle_request.priority != WalkStylePriority.COMBO and not has_walkstyle_info(walkstyle_request.walkstyle, *self._get_walkstyle_key()): return self._walkstyle_requests.append(walkstyle_request) self._walkstyle_requests.sort(reverse=True, key=operator.attrgetter('priority')) self._walk_style_handles[uid] = RegistryHandle(lambda : self._unrequest_walkstyle(walkstyle_request)) self._update_walkstyle() @componentmethod def remove_walkstyle(self, uid): if uid in self._walk_style_handles: self._walk_style_handles[uid].release() del self._walk_style_handles[uid] def _unrequest_walkstyle(self, walkstyle_request): self._walkstyle_requests.remove(walkstyle_request) self._update_walkstyle() def _update_walkstyle(self): for primitive in self.owner.primitives: try: primitive.request_walkstyle_update() except AttributeError: pass @componentmethod def get_additional_scoring_for_surface(self, surface_type): return self._pathplan_context.surface_preference_scoring.get(surface_type, 0) @componentmethod def add_location_to_quadtree(self, *args, **kwargs): path_plan_context = self._pathplan_context return path_plan_context.add_location_to_quadtree(*args, **kwargs) @componentmethod def remove_location_from_quadtree(self, *args, **kwargs): path_plan_context = self._pathplan_context return path_plan_context.remove_location_from_quadtree(*args, **kwargs) @property def _pathplan_context(self): age_key = getattr(self.owner, 'age', DEFAULT) species_key = getattr(self.owner, 'extended_species', DEFAULT) combined_override_key = (age_key, species_key) if combined_override_key in self._path_plan_context_map: return self._path_plan_context_map[combined_override_key] for override in self.plan_context_data.context_age_species_overrides: if override.age == age_key: if override.species == species_key: self._path_plan_context_map[combined_override_key] = override.context_override(self.owner) return self._path_plan_context_map[combined_override_key] self._path_plan_context_map.clear() self._path_plan_context_map[combined_override_key] = self.plan_context_data.default_context(self.owner) return self._path_plan_context_map[combined_override_key] @componentmethod def get_routing_context(self): return self.pathplan_context def on_sim_added(self): self._update_quadtree_location() def on_sim_removed(self): self.on_slot = None self._routing_master_ref = None self.clear_routing_slaves() def on_reset_internal_state(self, reset_reason): self.clear_routing_slaves() def add_callbacks(self): self.owner.register_on_location_changed(self._update_quadtree_location) self.owner.register_on_location_changed(self._check_violations) if self.get_walkstyle_behavior().supports_wading_walkstyle_buff(self.owner): self.owner.register_on_location_changed(self.get_walkstyle_behavior().check_for_wading) self.on_plan_path.append(self._on_update_goals) self.on_intended_location_changed.append(self.owner.refresh_los_constraint) self.on_intended_location_changed.append(self.owner._update_social_geometry_on_location_changed) self.on_intended_location_changed.append(lambda *_, **__: self.owner.two_person_social_transforms.clear()) self.on_intended_location_changed.append(self.owner.update_intended_position_on_active_lot) def remove_callbacks(self): self.on_intended_location_changed.clear() if self._on_update_goals in self.on_plan_path: self.on_plan_path.remove(self._on_update_goals) if self.owner._on_location_changed_callbacks is not None and self._check_violations in self.owner._on_location_changed_callbacks: self.owner.unregister_on_location_changed(self._check_violations) if self.owner._on_location_changed_callbacks is not None and self._update_quadtree_location in self.owner._on_location_changed_callbacks: self.owner.unregister_on_location_changed(self._update_quadtree_location) @property def pathplan_context(self): return self._pathplan_context def get_or_create_routing_context(self): return self._pathplan_context @property def routing_context(self): return self._pathplan_context @property def connectivity_handles(self): return self._pathplan_context.connectivity_handles @property def is_moving(self): if self.owner.is_sim: return not self.owner.location.almost_equal(self.owner.intended_location) else: return self.current_path is not None @property def routing_master(self): if self._routing_master_ref is not None: return self._routing_master_ref() @routing_master.setter def routing_master(self, value): self._routing_master_ref = value.ref() if value is not None else None @property def route_interaction(self): return self._route_interaction @property def animation_context(self): if self._route_interaction is not None: return self._route_interaction.animation_context return self._animation_context SLAVE_RADIUS_MODIFIER = 0.5 MAX_ALLOWED_AGENT_RADIUS = 0.25 def _update_agent_radius(self): agent_radius_datas = [slave_data for slave_data in self._routing_slave_data if slave_data.should_increase_master_agent_radius] if not agent_radius_datas: return max_x = max(abs(slave_data.offset[0]) for slave_data in agent_radius_datas) max_x = max(max_x*self.SLAVE_RADIUS_MODIFIER, self._default_agent_radius) self._pathplan_context.agent_radius = min(self.MAX_ALLOWED_AGENT_RADIUS, max_x) def add_routing_slave(self, slave_data): if len(self._routing_slave_data) == 0: self._default_agent_radius = self._pathplan_context.agent_radius slave_data.slave.routing_master = self.owner self._routing_slave_data.append(slave_data) slave_data.on_add() self._update_agent_radius() def clear_routing_slaves(self): for slave_data in self._routing_slave_data: slave_data.slave.routing_master = None slave_data.on_release() self._routing_slave_data.clear() self._restore_agent_radius() self._update_agent_radius() @componentmethod def get_routing_slave_data(self): return self._routing_slave_data @componentmethod def get_formation_data_for_slave(self, obj): for slave_data in self._routing_slave_data: if slave_data.slave is obj: return slave_data @componentmethod def get_all_routing_slave_data_gen(self): yield from self._routing_slave_data @componentmethod def get_routing_slave_data_count(self, formation_type): return sum(1 for slave_data in self._routing_slave_data if slave_data.formation_type is formation_type) def clear_slave(self, slave): for slave_data in self._routing_slave_data: if slave_data.slave is slave: self._routing_slave_data.remove(slave_data) slave_data.on_release() break self._restore_agent_radius() slave.routing_master = None self._update_agent_radius() @componentmethod def write_slave_data_msg(self, route_msg, path=None): transitioning_sims = () actor = self.owner if actor.is_sim: if actor.transition_controller is not None: transitioning_sims = actor.transition_controller.get_transitioning_sims() for slave_data in self.get_routing_slave_data(): if slave_data.should_slave_for_path(path): if slave_data.slave in transitioning_sims: continue (slave_actor, slave_msg) = slave_data.add_routing_slave_to_pb(route_msg, path=path) slave_actor.write_slave_data_msg(slave_msg, path=path) for slave_actor in actor.children: if not slave_actor.is_sim: continue carry_walkstyle_behavior = slave_actor.get_walkstyle_behavior().carry_walkstyle_behavior if carry_walkstyle_behavior is None: continue with ProtocolBufferRollback(route_msg.slaves) as slave_msg: slave_msg.id = slave_actor.id slave_msg.type = Routing_pb2.SlaveData.SLAVE_PAIRED_CHILD walkstyle_override_msg = slave_msg.walkstyle_overrides.add() walkstyle_override_msg.from_walkstyle = 0 walkstyle_override_msg.to_walkstyle = carry_walkstyle_behavior.default_carry_walkstyle for (walkstyle, carry_walkstyle) in carry_walkstyle_behavior.carry_walkstyle_overrides.items(): walkstyle_override_msg = slave_msg.walkstyle_overrides.add() walkstyle_override_msg.from_walkstyle = walkstyle walkstyle_override_msg.to_walkstyle = carry_walkstyle slave_actor.write_slave_data_msg(slave_msg, path=path) def _restore_agent_radius(self): if self._default_agent_radius is not None: if len(self._routing_slave_data) == 0: self._pathplan_context.agent_radius = self._default_agent_radius self._default_agent_radius = None def contains_slave(self, slave): return any(slave.id == slave_data.slave.id for slave_data in self._routing_slave_data) def _on_update_goals(self, goal_list, starting): NUM_GOALS_TO_RESERVE = 2 for (index, goal) in enumerate(goal_list, start=1): if index > NUM_GOALS_TO_RESERVE: break if starting: self.add_location_to_quadtree(placement.ItemType.SIM_INTENDED_POSITION, position=goal.position, orientation=goal.orientation, routing_surface=goal.routing_surface_id, index=index) else: self.remove_location_from_quadtree(placement.ItemType.SIM_INTENDED_POSITION, index=index) def set_portal_mask_flag(self, flag): self._pathplan_context.set_portal_key_mask(self._pathplan_context.get_portal_key_mask() | flag) def clear_portal_mask_flag(self, flag): self._pathplan_context.set_portal_key_mask(self._pathplan_context.get_portal_key_mask() & ~flag) def set_portal_discouragement_mask_flag(self, flag): self._pathplan_context.set_portal_discourage_key_mask(self._pathplan_context.get_portal_discourage_key_mask() | flag) def clear_portal_discouragement_mask_flag(self, flag): self._pathplan_context.set_portal_discourage_key_mask(self._pathplan_context.get_portal_discourage_key_mask() & ~flag) @componentmethod def update_portal_locks(self): sim = self.owner sim.manager.add_portal_lock(sim, sim._portal_added_callback) def _update_quadtree_location(self, *_, **__): self.add_location_to_quadtree(placement.ItemType.SIM_POSITION) def add_stand_slot_reservation(self, interaction, position, routing_surface, excluded_sims): interaction.add_liability(STAND_SLOT_LIABILITY, StandSlotReservationLiability(self.owner, interaction)) excluded_sims.add(self.owner) self._stand_slot_reservation = position self.add_location_to_quadtree(placement.ItemType.ROUTE_GOAL_SUPPRESSOR, position=position, routing_surface=routing_surface) pathplan_context = self._pathplan_context reservation_radius = pathplan_context.agent_radius*2 polygon = sims4.geometry.generate_circle_constraint(6, position, reservation_radius) self.on_slot = (position, polygon, routing_surface) UserFootprintHelper.force_move_sims_in_polygon(polygon, routing_surface, exclude=excluded_sims) def remove_stand_slot_reservation(self, interaction): self.remove_location_from_quadtree(placement.ItemType.ROUTE_GOAL_SUPPRESSOR) self.on_slot = None self.stand_slot_reservation_removed_callbacks(sim=self) def get_stand_slot_reservation_violators(self, excluded_sims=()): if not self.on_slot: return (_, polygon, routing_surface) = self.on_slot violators = [] excluded_sims = {sim for sim in itertools.chain((self.owner,), excluded_sims)} for sim_nearby in placement.get_nearby_sims_gen(polygon.centroid(), routing_surface, radius=polygon.radius(), exclude=excluded_sims): if sims4.geometry.test_point_in_polygon(sim_nearby.position, polygon): if not sim_nearby.ignore_blocking_near_destination: violators.append(sim_nearby) return violators def _check_violations(self, *_, **__): if services.privacy_service().check_for_late_violators(self.owner): return for reaction_trigger in self.owner.reaction_triggers.values(): reaction_trigger.intersect_and_execute(self.owner) def create_route_interaction(self): if self.owner.is_sim: aop = AffordanceObjectPair(AnimationInteraction, None, AnimationInteraction, None, hide_unrelated_held_props=False) context = InteractionContext(self.owner, InteractionContext.SOURCE_SCRIPT, Priority.High) self._route_interaction = aop.interaction_factory(context).interaction else: self._animation_context = AnimationContext() self._animation_context.add_ref(self._current_path) for slave_data in self.get_routing_slave_data(): slave_data.slave.routing_component.create_route_interaction() def cancel_route_interaction(self): if self._route_interaction is not None: self._route_interaction.cancel(FinishingType.AUTO_EXIT, 'Route Ended.') self._route_interaction.on_removed_from_queue() self._route_interaction = None if self._animation_context is not None: self._animation_context.release_ref(self._current_path) self._animation_context = None for slave_data in self.get_routing_slave_data(): slave_data.slave.routing_component.cancel_route_interaction() def set_follow_path(self, follow_path): self._active_follow_path_weakref = weakref.ref(follow_path) def clear_follow_path(self): self._active_follow_path_weakref = None def _get_active_follow_path(self): if self._active_follow_path_weakref is not None: return self._active_follow_path_weakref() def get_approximate_cancel_location(self): follow_path = self._get_active_follow_path() if follow_path is not None: ret = follow_path.get_approximate_cancel_location() if ret is not None: return ret return (self.owner.intended_transform, self.owner.intended_routing_surface) @componentmethod def set_routing_path(self, path): if path is None: if gsi_handlers.route_event_handlers.archiver.enabled: gsi_handlers.route_event_handlers.archive_route_events(self._current_path, self.owner, gsi_handlers.route_event_handlers.PATH_TYPE_FINISHED, clear=True) self._on_routing_stage_event(RoutingStageEvent.ROUTE_END, path=self._current_path) self.cancel_route_interaction() self._current_path = None if self.owner.is_sim and self.owner.transition_controller is not None and self._initial_carry_targets != self.owner.posture_state.carry_targets: self.owner.transition_controller.derail(DerailReason.CONSTRAINTS_CHANGED, self.owner) self._initial_carry_targets = None return if self.owner.is_sim: self._initial_carry_targets = self.owner.posture_state.carry_targets self._current_path = path if self.pathplan_context.disable_fake_portals: self._current_path.remove_fake_portals() self.create_route_interaction() self._on_routing_stage_event(RoutingStageEvent.ROUTE_START, path=path) walkstyle = self.walkstyle_behavior.apply_walkstyle_to_path(self.owner, self._current_path) origin_q = (float(self.owner.orientation.x), float(self.owner.orientation.y), float(self.owner.orientation.z), float(self.owner.orientation.w)) origin_t = (float(self.owner.position.x), float(self.owner.position.y), float(self.owner.position.z)) (age, gender, species) = self._get_walkstyle_key() self._current_path.nodes.apply_initial_timing(origin_q, origin_t, walkstyle, age, gender, species, int(services.time_service().sim_now), services.current_zone_id()) @componentmethod def update_routing_path(self, time_offset): if self._current_path is None: return walkstyle = self.walkstyle_behavior.apply_walkstyle_to_path(self.owner, self._current_path, time_offset=time_offset) (age, gender, species) = self._get_walkstyle_key() self._current_path.nodes.update_timing(walkstyle, age, gender, species, time_offset, services.current_zone_id()) @componentmethod def update_slave_positions_for_path(self, path, transform, orientation, routing_surface, distribute=True, canceled=False): transitioning_sims = () if self.owner.is_sim: if self.owner.transition_controller is not None: transitioning_sims = self.owner.transition_controller.get_transitioning_sims() for slave_data in self.get_routing_slave_data(): if slave_data.slave in transitioning_sims: continue slave_data.update_slave_position(transform, orientation, routing_surface, distribute=distribute, path=path, canceled=canceled) def add_route_event_provider(self, request): if self._route_event_provider_requests is None: self._route_event_provider_requests = [] self._route_event_provider_requests.append(request) def remove_route_event_provider(self, request): if self._route_event_provider_requests is not None and request in self._route_event_provider_requests: self._route_event_provider_requests.remove(request) if not self._route_event_provider_requests: self._route_event_provider_requests = None def route_event_executed(self, event_id): if self._route_event_context is None: return self._route_event_context.handle_route_event_executed(event_id, self.owner, path=self._current_path) def route_event_skipped(self, event_id): if self._route_event_context is None: return self._route_event_context.handle_route_event_skipped(event_id, self.owner, path=self._current_path) def remove_route_event_by_data(self, event_data): if self._route_event_context is None: return self._route_event_context.remove_route_event_by_data(event_data) @componentmethod def route_finished(self, path_id): for primitive in self.owner.primitives: if hasattr(primitive, 'route_finished'): primitive.route_finished(path_id) @componentmethod def route_time_update(self, path_id, current_time): for primitive in self.owner.primitives: if hasattr(primitive, 'route_time_update'): primitive.route_time_update(path_id, current_time) def _gather_route_events(self, path, **kwargs): owner = self.owner if owner.is_sim: interaction = owner.transition_controller.interaction if owner.transition_controller is not None else None if interaction is not None and interaction.is_super: interaction.provide_route_events(self._route_event_context, owner, path, **kwargs) owner.Buffs.provide_route_events_from_buffs(self._route_event_context, owner, path, **kwargs) broadcaster_service = services.current_zone().broadcaster_service broadcaster_service.provide_route_events(self._route_event_context, owner, path, **kwargs) if owner.weather_aware_component is not None: owner.weather_aware_component.provide_route_events(self._route_event_context, owner, path, **kwargs) if self._route_event_provider_requests is not None: for request in self._route_event_provider_requests: request.provide_route_events(self._route_event_context, owner, path, **kwargs) object_manager = services.object_manager(owner.zone_id) if object_manager is not None: for node in path.nodes: if node.portal_object_id != 0: portal_object = object_manager.get(node.portal_object_id) if portal_object is not None: portal_object.provide_route_events(node.portal_id, self._route_event_context, owner, path, node=node, **kwargs) def clear_route_events(self, *args, **kwargs): if self._route_event_context is None: return self._route_event_context.clear_route_events() for slave_data in self.get_routing_slave_data(): slave_data.slave.routing_component.clear_route_events() def schedule_and_process_route_events_for_new_path(self, path): if self._route_event_context is None: return if self.owner.is_sim and not self.owner.posture.mobile: return self.clear_route_events() start_time = RouteEventContext.ROUTE_TRIM_START end_time = min(start_time + ROUTE_EVENT_WINDOW_DURATION, path.duration()) self._gather_route_events(path, start_time=start_time, end_time=end_time) self._route_event_context.schedule_route_events(self.owner, path) self._route_event_context.process_route_events(self.owner) for slave_data in self.get_routing_slave_data(): slave_data.slave.routing_component.schedule_and_process_route_events_for_new_path(path) if gsi_handlers.route_event_handlers.archiver.enabled: gsi_handlers.route_event_handlers.archive_route_events(path, self.owner, gsi_handlers.route_event_handlers.PATH_TYPE_INITIAL) def append_route_events_to_route_msg(self, route_msg): if self._route_event_context is None: return self._route_event_context.append_route_events_to_route_msg(route_msg) for slave_data in self.get_routing_slave_data(): slave_data.slave.routing_component.append_route_events_to_route_msg(route_msg) def update_route_events_for_current_path(self, path, current_time, time_offset): (failed_events, failed_types) = self._route_event_context.prune_stale_events_and_get_failed_types(self.owner, path, current_time) start_time = current_time window_duration = ROUTE_EVENT_WINDOW_DURATION if time_offset < 0: window_duration -= time_offset end_time = min(start_time + window_duration, path.duration()) self._gather_route_events(path, failed_types=failed_types, start_time=start_time, end_time=end_time) self._route_event_context.schedule_route_events(self.owner, path, start_time=start_time) should_update = True if failed_events else False or self._route_event_context.has_pending_events_to_process() for slave_data in self.get_routing_slave_data(): should_update |= slave_data.slave.routing_component.update_route_events_for_current_path(path, current_time, time_offset) if gsi_handlers.route_event_handlers.archiver.enabled and gsi_handlers.route_event_handlers.update_log_enabled: gsi_handlers.route_event_handlers.archive_route_events(path, self.owner, gsi_handlers.route_event_handlers.PATH_TYPE_UPDATE) return should_update def process_updated_route_events(self): self._route_event_context.process_route_events(self.owner) for slave_data in self.get_routing_slave_data(): slave_data.slave.routing_component.process_updated_route_events() @componentmethod def should_route_instantly(self): zone = services.current_zone() if zone.force_route_instantly: return True if self.owner.is_sim: if not (zone.are_sims_hitting_their_marks and self.owner._allow_route_instantly_when_hitting_marks): return False else: return not services.sim_spawner_service().sim_is_leaving(self.owner) return False
class SituationGoal(SituationGoalDisplayMixin, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager( sims4.resources.Types.SITUATION_GOAL)): INSTANCE_SUBCLASSES_ONLY = True IS_TARGETED = False INSTANCE_TUNABLES = { '_pre_tests': TunableSituationGoalPreTestSet( description= '\n A set of tests on the player sim and environment that all must\n pass for the goal to be given to the player. e.g. Player Sim\n has cooking skill level 7.\n ', tuning_group=GroupNames.TESTS), '_post_tests': TunableSituationGoalPostTestSet( description= '\n A set of tests that must all pass when the player satisfies the\n goal_test for the goal to be consider completed. e.g. Player\n has Drunk Buff when Kissing another sim at Night.\n ', tuning_group=GroupNames.TESTS), '_cancel_on_travel': Tunable( description= '\n If set, this situation goal will cancel (technically, complete\n with score overridden to 0 so that situation score is not\n progressed) if situation changes zone.\n ', tunable_type=bool, default=False, tuning_group=GroupNames.TESTS), '_environment_pre_tests': TunableSituationGoalEnvironmentPreTestSet( description= '\n A set of sim independent pre tests.\n e.g. There are five desks.\n ', tuning_group=GroupNames.TESTS), 'role_tags': TunableSet( TunableEnumEntry(Tag, Tag.INVALID), description= '\n This goal will only be given to Sims in SituationJobs or Role\n States marked with one of these tags.\n ' ), '_cooldown': TunableSimMinute( description= '\n The cooldown of this situation goal. Goals that have been\n completed will not be chosen again for the amount of time that\n is tuned.\n ', default=600, minimum=0), '_iterations': Tunable( description= '\n Number of times the player must perform the action to complete the goal\n ', tunable_type=int, default=1), '_score': Tunable( description= '\n The number of points received for completing the goal.\n ', tunable_type=int, default=10), 'score_on_iteration_complete': OptionalTunable( description= '\n If enabled then we will add an amount of score to the situation\n with every iteration of the situation goal completing.\n ', tunable=Tunable( description= '\n An amount of score that should be applied when an iteration\n completes.\n ', tunable_type=int, default=10)), '_pre_goal_loot_list': TunableList( description= '\n A list of pre-defined loot actions that will applied to every\n sim in the situation when this situation goal is started.\n \n Do not use this loot list in an attempt to undo changes made by\n the RoleStates to the sim. For example, do not attempt\n to remove buffs or commodities added by the RoleState.\n ', tunable=SituationGoalLootActions.TunableReference()), '_goal_loot_list': TunableList( description= '\n A list of pre-defined loot actions that will applied to every\n sim in the situation when this situation goal is completed.\n \n Do not use this loot list in an attempt to undo changes made by\n the RoleStates to the sim. For example, do not attempt\n to remove buffs or commodities added by the RoleState.\n ', tunable=SituationGoalLootActions.TunableReference()), 'noncancelable': Tunable( description= '\n Checking this box will prevent the player from canceling this goal in the whim system.', tunable_type=bool, default=False), 'time_limit': Tunable( description= '\n Timeout (in Sim minutes) for Sim to complete this goal. The default state of 0 means\n time is unlimited. If the goal is not completed in time, any tuned penalty loot is applied.', tunable_type=int, default=0), 'penalty_loot_list': TunableList( description= '\n A list of pre-defined loot actions that will applied to the Sim who fails\n to complete this goal within the tuned time limit.\n ', tunable=SituationGoalLootActions.TunableReference()), 'goal_awarded_notification': OptionalTunable( description= '\n If enabled, this goal will have a notification associated with it.\n It is up to whatever system awards the goal (e.g. the Whim system)\n to display the notification when necessary.\n ', tunable=TunableUiDialogNotificationSnippet()), 'goal_completion_notification': OptionalTunable(tunable=UiDialogNotification.TunableFactory( description= '\n A TNS that will fire when this situation goal is completed.\n ' )), 'goal_completion_notification_and_modal_target': OptionalTunable( description= '\n If enabled then we will use the tuned situation job to pick a\n random sim in the owning situation with that job to be the target\n sim of the notification and modal dialog.\n ', tunable=TunableReference( description= '\n The situation job that will be used to find a sim in the owning\n situation to be the target sim.\n ', manager=services.get_instance_manager( sims4.resources.Types.SITUATION_JOB))), 'audio_sting_on_complete': TunableResourceKey( description= '\n The sound to play when this goal is completed.\n ', resource_types=(sims4.resources.Types.PROPX, ), default=None, allow_none=True, tuning_group=GroupNames.AUDIO), 'goal_completion_modal_dialog': OptionalTunable(tunable=UiDialogOk.TunableFactory( description= '\n A modal dialog that will fire when this situation goal is\n completed.\n ' )), 'visible_minor_goal': Tunable( description= '\n Whether or not this goal should be displayed in the minor goals\n list if this goal is for a player facing situation.\n ', tunable_type=bool, default=True, tuning_group=GroupNames.UI), 'display_type': TunableEnumEntry( description= '\n How this goal is presented in user-facing situations.\n ', tunable_type=SituationGoalDisplayType, default=SituationGoalDisplayType.NORMAL, tuning_group=GroupNames.UI) } @classmethod def can_be_given_as_goal(cls, actor, situation, **kwargs): if actor is not None: resolver = event_testing.resolver.DataResolver( actor.sim_info, None) result = cls._pre_tests.run_tests(resolver) if not result: return result else: resolver = GlobalResolver() environment_test_result = cls._environment_pre_tests.run_tests( resolver) if not environment_test_result: return environment_test_result return TestResult.TRUE def __init__(self, sim_info=None, situation=None, goal_id=0, count=0, locked=False, completed_time=None, secondary_sim_info=None, **kwargs): self._sim_info = sim_info self._secondary_sim_info = secondary_sim_info self._situation = situation self.id = goal_id self._on_goal_completed_callbacks = CallableList() self._completed_time = completed_time self._count = count self._locked = locked self._score_override = None self._goal_status_override = None self._setup = False def setup(self): self._setup = True def destroy(self): self.decommision() self._sim_info = None self._situation = None def decommision(self): if self._setup: self._decommision() def _decommision(self): self._on_goal_completed_callbacks.clear() def create_seedling(self): actor_id = 0 if self._sim_info is None else self._sim_info.sim_id target_sim_info = self.get_required_target_sim_info() target_id = 0 if target_sim_info is None else target_sim_info.sim_id secondary_target_id = 0 if self._secondary_sim_info is None else self._secondary_sim_info.sim_id seedling = situations.situation_serialization.GoalSeedling( type(self), actor_id, target_id, secondary_target_id, self._count, self._locked, self._completed_time) return seedling def register_for_on_goal_completed_callback(self, listener): self._on_goal_completed_callbacks.append(listener) def unregister_for_on_goal_completed_callback(self, listener): self._on_goal_completed_callbacks.remove(listener) def get_gsi_name(self): if self._iterations <= 1: return self.__class__.__name__ return '{} {}/{}'.format(self.__class__.__name__, self._count, self._iterations) def on_goal_offered(self): if self._situation is None: return for sim in self._situation.all_sims_in_situation_gen(): resolver = sim.get_resolver() for loots in self._pre_goal_loot_list: for loot in loots.goal_loot_actions: loot.apply_to_resolver(resolver) def _display_goal_completed_dialogs(self): actor_sim_info = services.active_sim_info() target_sim_info = None if self.goal_completion_notification_and_modal_target is not None: possible_sims = list( self._situation.all_sims_in_job_gen( self.goal_completion_notification_and_modal_target)) if possible_sims: target_sim_info = random.choice(possible_sims) if target_sim_info is None: return resolver = DoubleSimResolver(actor_sim_info, target_sim_info) if self.goal_completion_notification is not None: notification = self.goal_completion_notification(actor_sim_info, resolver=resolver) notification.show_dialog() if self.goal_completion_modal_dialog is not None: dialog = self.goal_completion_modal_dialog(actor_sim_info, resolver=resolver) dialog.show_dialog() def _on_goal_completed(self, start_cooldown=True): if start_cooldown: self._completed_time = services.time_service().sim_now loot_sims = (self._sim_info, ) if self._situation is None else tuple( self._situation.all_sims_in_situation_gen()) for loots in self._goal_loot_list: for loot in loots.goal_loot_actions: for sim in loot_sims: loot.apply_to_resolver(sim.get_resolver()) self._display_goal_completed_dialogs() with situations.situation_manager.DelayedSituationDestruction(): self._on_goal_completed_callbacks(self, True) def _on_iteration_completed(self): self._on_goal_completed_callbacks(self, False) def force_complete(self, target_sim=None, score_override=None, start_cooldown=True): self._score_override = score_override self._count = self._iterations self._on_goal_completed(start_cooldown=start_cooldown) def _valid_event_sim_of_interest(self, sim_info): return self._sim_info is None or self._sim_info is sim_info def handle_event(self, sim_info, event, resolver): if not self._valid_event_sim_of_interest(sim_info): return if self._run_goal_completion_tests(sim_info, event, resolver): self._count += 1 if self._count >= self._iterations: self._on_goal_completed() else: self._on_iteration_completed() def _run_goal_completion_tests(self, sim_info, event, resolver): return self._post_tests.run_tests(resolver) def should_autocomplete_on_load(self, previous_zone_id): if self._cancel_on_travel: zone_id = services.current_zone_id() if previous_zone_id != zone_id: return True return False def get_actual_target_sim_info(self): pass @property def sim_info(self): return self._sim_info def get_required_target_sim_info(self): pass def get_secondary_sim_info(self): return self._secondary_sim_info @property def created_time(self): pass @property def completed_time(self): return self._completed_time def is_on_cooldown(self): if self._completed_time is None: return False time_since_last_completion = services.time_service( ).sim_now - self._completed_time return time_since_last_completion < interval_in_sim_minutes( self._cooldown) def get_localization_tokens(self): target_sim_info = self.get_required_target_sim_info() return (self._numerical_token, self._sim_info, target_sim_info, self._secondary_sim_info) def get_display_name(self): display_name = self.display_name if display_name is not None: return display_name(*self.get_localization_tokens()) def get_display_tooltip(self): display_tooltip = self.display_tooltip if display_tooltip is not None: return display_tooltip(*self.get_localization_tokens()) @property def score(self): if self._score_override is not None: return self._score_override return self._score @property def goal_status_override(self): return self._goal_status_override @property def completed_iterations(self): return self._count @property def max_iterations(self): return self._iterations @property def _numerical_token(self): return self.max_iterations @property def locked(self): return self._locked def toggle_locked_status(self): self._locked = not self._locked def validate_completion(self): if self._completed_time is not None: return if self.completed_iterations < self.max_iterations: return self.force_complete() def show_goal_awarded_notification(self): if self.goal_awarded_notification is None: return icon_override = IconInfoData(icon_resource=self.display_icon) secondary_icon_override = IconInfoData(obj_instance=self._sim_info) notification = self.goal_awarded_notification(self._sim_info) notification.show_dialog( additional_tokens=self.get_localization_tokens(), icon_override=icon_override, secondary_icon_override=secondary_icon_override)
class SituationGoal(metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(sims4.resources.Types.SITUATION_GOAL)): __qualname__ = 'SituationGoal' INSTANCE_SUBCLASSES_ONLY = True IS_TARGETED = False INSTANCE_TUNABLES = {'_display_name': TunableLocalizedStringFactory(description='\n Display name for the Situation Goal. It takes one token, the\n target (0) of this situation goal.\n ', tuning_group=GroupNames.UI), '_icon': TunableResourceKey('PNG:missing_image', description='\n Icon to be displayed for the Situation Goal.\n ', needs_tuning=True, resource_types=sims4.resources.CompoundTypes.IMAGE), '_pre_tests': TunableSituationGoalPreTestSet(description='\n A set of tests on the player sim and environment that all must\n pass for the goal to be given to the player. e.g. Player Sim\n has cooking skill level 7.\n ', tuning_group=GroupNames.TESTS), '_post_tests': TunableSituationGoalPostTestSet(description='\n A set of tests that must all pass when the player satisfies the\n goal_test for the goal to be consider completed. e.g. Player\n has Drunk Buff when Kissing another sim at Night.\n ', tuning_group=GroupNames.TESTS), '_environment_pre_tests': TunableSituationGoalEnvironmentPreTestSet(description='\n A set of sim independent pre tests.\n e.g. There are five desks.\n ', tuning_group=GroupNames.TESTS), 'role_tags': TunableSet(TunableEnumEntry(Tag, Tag.INVALID), description='\n This goal will only be given to Sims in SituationJobs or Role\n States marked with one of these tags.\n '), '_cooldown': TunableSimMinute(description='\n The cooldown of this situation goal. Goals that have been\n completed will not be chosen again for the amount of time that\n is tuned.\n ', default=600, minimum=0), '_iterations': Tunable(description='\n Number of times the player must perform the action to complete the goal\n ', tunable_type=int, default=1), '_score': Tunable(description='\n The number of points received for completing the goal.\n ', tunable_type=int, default=10), '_goal_loot_list': TunableList(description='\n A list of pre-defined loot actions that will applied to every\n sim in the situation when this situation goal is completed.\n \n Do not use this loot list in an attempt to undo changes made by\n the RoleStates to the sim. For example, do not attempt\n to remove buffs or commodities added by the RoleState.\n ', tunable=SituationGoalLootActions.TunableReference()), '_tooltip': TunableLocalizedStringFactory(description='\n Tooltip for this situation goal. It takes one token, the\n actor (0) of this situation goal.'), 'noncancelable': Tunable(description='\n Checking this box will prevent the player from canceling this goal in the whim system.', tunable_type=bool, needs_tuning=True, default=False), 'time_limit': Tunable(description='\n Timeout (in Sim minutes) for Sim to complete this goal. The default state of 0 means\n time is unlimited. If the goal is not completed in time, any tuned penalty loot is applied.', tunable_type=int, default=0), 'penalty_loot_list': TunableList(description='\n A list of pre-defined loot actions that will applied to the Sim who fails\n to complete this goal within the tuned time limit.\n ', tunable=SituationGoalLootActions.TunableReference()), 'goal_completion_notification': OptionalTunable(tunable=UiDialogNotification.TunableFactory(description='\n A TNS that will fire when this situation goal is completed.\n ')), 'audio_sting_on_complete': TunableResourceKey(description='\n The sound to play when this goal is completed.\n ', default=None, resource_types=(sims4.resources.Types.PROPX,), tuning_group=GroupNames.AUDIO), 'goal_completion_modal_dialog': OptionalTunable(tunable=UiDialogOk.TunableFactory(description='\n A modal dialog that will fire when this situation goal is\n completed.\n '))} @classmethod def can_be_given_as_goal(cls, actor, situation, **kwargs): if actor is not None: resolver = event_testing.resolver.DataResolver(actor.sim_info, None) result = cls._pre_tests.run_tests(resolver) if not result: return result environment_test_result = cls._environment_pre_tests.run_tests(Resolver()) if not environment_test_result: return environment_test_result return TestResult.TRUE def __init__(self, sim_info=None, situation=None, goal_id=0, count=0, **kwargs): self._sim_info = sim_info self._situation = situation self.id = goal_id self._on_goal_completed_callbacks = CallableList() self._completed_time = None self._count = count def destroy(self): self.decommision() self._sim_info = None self._situation = None def decommision(self): self._on_goal_completed_callbacks.clear() def create_seedling(self): actor_id = 0 if self._sim_info is None else self._sim_info.sim_id seedling = situations.situation_serialization.GoalSeedling(type(self), actor_id, self._count) return seedling def register_for_on_goal_completed_callback(self, listener): self._on_goal_completed_callbacks.append(listener) def unregister_for_on_goal_completed_callback(self, listener): self._on_goal_completed_callbacks.remove(listener) def get_gsi_name(self): if self._iterations <= 1: return self.__class__.__name__ return '{} {}/{}'.format(self.__class__.__name__, self._count, self._iterations) def _on_goal_completed(self): self._completed_time = services.time_service().sim_now for loots in self._goal_loot_list: for loot in loots.goal_loot_actions: for sim in self._situation.all_sims_in_situation_gen(): loot.apply_to_resolver(sim.get_resolver()) client = services.client_manager().get_first_client() if client is not None: active_sim = client.active_sim resolver = SingleSimResolver(active_sim) if self.goal_completion_notification is not None: notification = self.goal_completion_notification(active_sim, resolver=resolver) notification.show_dialog() if self.goal_completion_modal_dialog is not None: dialog = self.goal_completion_modal_dialog(active_sim, resolver=resolver) dialog.show_dialog() self._on_goal_completed_callbacks(self, True) def _on_iteration_completed(self): self._on_goal_completed_callbacks(self, False) def debug_force_complete(self, target_sim): self._count = self._iterations self._on_goal_completed() def handle_event(self, sim_info, event, resolver): if self._sim_info is not None and self._sim_info is not sim_info: return if self._run_goal_completion_tests(sim_info, event, resolver): if self._count >= self._iterations: self._on_goal_completed() else: self._on_iteration_completed() def _run_goal_completion_tests(self, sim_info, event, resolver): return self._post_tests.run_tests(resolver) def _get_actual_target_sim_info(self): pass def get_required_target_sim_info(self): pass @property def created_time(self): pass @property def completed_time(self): return self._completed_time def is_on_cooldown(self): if self._completed_time is None: return False time_since_last_completion = services.time_service().sim_now - self._completed_time return time_since_last_completion < interval_in_sim_minutes(self._cooldown) def get_localization_tokens(self): if self._sim_info is None: return (self._numerical_token,) required_tgt_sim = self.get_required_target_sim_info() if required_tgt_sim is None: return (self._numerical_token, self._sim_info) return (self._numerical_token, self._sim_info, required_tgt_sim) @property def display_name(self): return self._display_name(*self.get_localization_tokens()) @property def tooltip(self): return self._tooltip(*self.get_localization_tokens()) @property def score(self): return self._score @property def completed_iterations(self): return self._count @property def max_iterations(self): return self._iterations @property def _numerical_token(self): return self.max_iterations
class CraftingComponent(Component, component_name=types.CRAFTING_COMPONENT, persistence_key=protocols.PersistenceMaster. PersistableData.CraftingComponent, allow_dynamic=True): def __init__(self, owner): super().__init__(owner) self._crafting_process = None self._use_base_recipe = False self.object_mutated_listeners = CallableList() self._servings_statistic_tracker_handle = None self._quality_change_callback_added = False self._spoil_listener_handle = None self._is_final_product = False self._last_spoiled_time = None self._spoil_timer_state_value = CraftingTuning.SPOILED_STATE_VALUE @property def is_final_product(self): return self._is_final_product def set_final_product(self, is_final_product): self._is_final_product = is_final_product @componentmethod_with_fallback(lambda: (DEFAULT, DEFAULT)) def get_template_content_overrides(self): is_final_product = self._crafting_process.phase is None or self._crafting_process.phase.object_info_is_final_product if is_final_product or self.owner.name_component is None: return (DEFAULT, DEFAULT) name_component = self._crafting_process.recipe.final_product_definition.cls.tuned_components.name if name_component is not None and name_component.templates: selected_template = random.choice(name_component.templates) return (selected_template.template_name, selected_template.template_description) return (DEFAULT, DEFAULT) def on_add(self, *_, **__): tracker = self.owner.get_tracker(CraftingTuning.SERVINGS_STATISTIC) self._servings_statistic_tracker_handle = tracker.add_watcher( self._on_servings_change) if tracker.has_statistic(CraftingTuning.SERVINGS_STATISTIC): current_value = tracker.get_value( CraftingTuning.SERVINGS_STATISTIC) if current_value is not None: self.owner.update_tooltip_field(TooltipFieldsComplete.servings, max(int(current_value), 0), should_update=True) if self.owner.state_component is not None: self.owner.add_state_changed_callback(self._on_object_state_change) self._quality_change_callback_added = True consumable_state_value = self.owner.get_state_value_from_stat_type( CraftingTuning.CONSUME_STATISTIC) if consumable_state_value is not None: self._on_object_state_change(self.owner, consumable_state_value.state, consumable_state_value, consumable_state_value) quality_state_value = self.owner.get_state_value_from_stat_type( CraftingTuning.QUALITY_STATISTIC) if quality_state_value is not None: self._on_object_state_change(self.owner, quality_state_value.state, quality_state_value, quality_state_value) freshness_state = CraftingTuning.LOCK_FRESHNESS_STATE_VALUE.state if self.owner.has_state(freshness_state): freshness_locked_state_value = self.owner.get_state( freshness_state) if freshness_locked_state_value is CraftingTuning.LOCK_FRESHNESS_STATE_VALUE: self._on_object_state_change( self.owner, freshness_locked_state_value.state, freshness_locked_state_value, freshness_locked_state_value) def on_remove(self, *_, **__): if self._servings_statistic_tracker_handle is not None: tracker = self.owner.get_tracker(CraftingTuning.SERVINGS_STATISTIC) if tracker.has_watcher(self._servings_statistic_tracker_handle): tracker.remove_watcher(self._servings_statistic_tracker_handle) self._servings_statistic_tracker_handle = None if self._quality_change_callback_added: self.owner.remove_state_changed_callback( self._on_object_state_change) self._quality_change_callback_added = False if self._spoil_listener_handle is not None: spoil_tracker = self.owner.get_tracker( self._spoil_timer_state_value.state.linked_stat) spoil_tracker.remove_listener(self._spoil_listener_handle) self._remove_hovertip() def on_mutated(self): self.object_mutated_listeners() self.object_mutated_listeners.clear() self.owner.remove_component(types.CRAFTING_COMPONENT) self.on_remove() @property def crafter_sim_id(self): if self._crafting_process.crafter_sim_id: return self._crafting_process.crafter_sim_id return 0 def _on_servings_change(self, stat_type, old_value, new_value): if stat_type is CraftingTuning.SERVINGS_STATISTIC: owner = self.owner self.owner.update_tooltip_field(TooltipFieldsComplete.servings, max(int(new_value), 0), should_update=True) current_inventory = owner.get_inventory() if current_inventory is not None: current_inventory.push_inventory_item_update_msg(owner) if new_value <= 0 and old_value != new_value: self.on_mutated() def _on_object_state_change(self, owner, state, old_value, new_value): state_value = None if state is CraftingTuning.QUALITY_STATE: state_value = new_value elif state is CraftingTuning.FRESHNESS_STATE: if new_value in CraftingTuning.QUALITY_STATE_VALUE_MAP: state_value = new_value else: self.owner.update_tooltip_field( TooltipFieldsComplete.quality_description, None) if self.owner.has_state(CraftingTuning.QUALITY_STATE): state_value = self.owner.get_state( CraftingTuning.QUALITY_STATE) if state_value is not None: value_quality = CraftingTuning.QUALITY_STATE_VALUE_MAP.get( state_value) if value_quality is not None: self.owner.update_tooltip_field( TooltipFieldsComplete.quality, value_quality.state_star_number) self.owner.update_tooltip_field( TooltipFieldsComplete.quality_description, None) if owner.has_state( CraftingTuning.SPOILED_STATE_VALUE.state) and owner.get_state( CraftingTuning.SPOILED_STATE_VALUE.state ) is CraftingTuning.SPOILED_STATE_VALUE: if self._crafting_process is not None: recipe = self._get_recipe() if recipe.show_spoiled_quality_description: self.owner.update_tooltip_field( TooltipFieldsComplete.quality_description, CraftingTuning.SPOILED_STRING) self.owner.update_tooltip_field( TooltipFieldsComplete.spoiled_time, 0) elif owner.has_state( CraftingTuning.MASTERWORK_STATE ) and owner.get_state( CraftingTuning.MASTERWORK_STATE ) is CraftingTuning.MASTERWORK_STATE_VALUE and self._crafting_process is not None: recipe = self._get_recipe() if recipe is not None and recipe.masterwork_name is not None: self.owner.update_tooltip_field( TooltipFieldsComplete.quality_description, recipe.masterwork_name) if state is CraftingTuning.CONSUMABLE_STATE: value_consumable = CraftingTuning.CONSUMABLE_STATE_VALUE_MAP.get( new_value) if value_consumable is not None: self.owner.update_tooltip_field( TooltipFieldsComplete.percentage_left, value_consumable) if new_value is CraftingTuning.CONSUMABLE_EMPTY_STATE_VALUE and old_value is not CraftingTuning.CONSUMABLE_EMPTY_STATE_VALUE: self._remove_hovertip() if new_value is CraftingTuning.LOCK_FRESHNESS_STATE_VALUE: self.owner.update_tooltip_field(TooltipFieldsComplete.spoiled_time, 0) if self._spoil_listener_handle is not None: spoil_tracker = self.owner.get_tracker( self._spoil_timer_state_value.state.linked_stat) spoil_tracker.remove_listener(self._spoil_listener_handle) self._spoil_listener_handle = None self.owner.update_object_tooltip() def _add_hovertip(self): if self._is_final_product and self._is_finished(): self._add_consumable_hovertip() def _is_finished(self): crafting_process = self._crafting_process tracker = None stat = CraftingTuning.PROGRESS_STATISTIC if crafting_process.current_ico is not None: tracker = crafting_process.current_ico.get_tracker(stat) if tracker is None: tracker = self.owner.get_tracker(stat) if not (tracker is None or not tracker.has_statistic(stat)): tracker = crafting_process.get_tracker(stat) if tracker.has_statistic( stat) and tracker.get_value(stat) != stat.max_value: return crafting_process.is_complete return True def _get_recipe(self): recipe = self._crafting_process.recipe if self._use_base_recipe: recipe = recipe.get_base_recipe() return recipe def _add_consumable_hovertip(self): owner = self.owner owner.hover_tip = ui_protocols.UiObjectMetadata.HOVER_TIP_CONSUMABLE_CRAFTABLE crafting_process = self._crafting_process recipe = self._get_recipe() if recipe is None: return self.owner.update_tooltip_field( TooltipFieldsComplete.recipe_name, recipe.get_recipe_name(crafting_process.crafter)) crafted_by_text = crafting_process.get_crafted_by_text( is_from_gallery=self.owner.is_from_gallery) crafted_with_text = crafting_process.get_crafted_with_text() if crafted_by_text is not None: if crafted_with_text is not None: crafted_by_text = LocalizationHelperTuning.NEW_LINE_LIST_STRUCTURE( crafted_by_text, crafted_with_text) crafter_sim_id = crafting_process.crafter_sim_id if crafter_sim_id is not None: self.owner.update_tooltip_field( TooltipFieldsComplete.crafter_sim_id, crafter_sim_id) elif crafted_with_text is not None: crafted_by_text = crafted_with_text self.owner.update_tooltip_field(TooltipFieldsComplete.crafted_by_text, crafted_by_text) if owner.has_state(CraftingTuning.QUALITY_STATE): value_quality = CraftingTuning.QUALITY_STATE_VALUE_MAP.get( owner.get_state(CraftingTuning.QUALITY_STATE)) if value_quality is not None: self.owner.update_tooltip_field( TooltipFieldsComplete.quality, value_quality.state_star_number) if owner.has_state( CraftingTuning.MASTERWORK_STATE ) and owner.get_state( CraftingTuning.MASTERWORK_STATE ) is CraftingTuning.MASTERWORK_STATE_VALUE and recipe.masterwork_name is not None: self.owner.update_tooltip_field( TooltipFieldsComplete.quality_description, recipe.masterwork_name) inscription = crafting_process.inscription if inscription is not None: self.owner.update_tooltip_field(TooltipFieldsComplete.inscription, inscription) self._add_spoil_listener( state_override=recipe.spoil_time_commodity_override) if recipe.time_until_spoiled_string_override is not None: self.owner.update_tooltip_field( TooltipFieldsComplete.spoiled_time_text, recipe.time_until_spoiled_string_override) subtext = self.owner.get_state_strings() if subtext is not None: self.owner.update_tooltip_field(TooltipFieldsComplete.subtext, subtext) recipe.update_hovertip(self.owner, crafter=crafting_process.crafter) current_inventory = owner.get_inventory() if current_inventory is not None: current_inventory.push_inventory_item_update_msg(owner) self.owner.on_hovertip_requested() self.owner.update_object_tooltip() def update_simoleon_tooltip(self): self._crafting_process.apply_simoleon_value(self.owner) recipe = self._get_recipe() recipe.update_hovertip(self.owner, crafter=self._crafting_process.crafter) self.owner.update_object_tooltip() def update_quality_tooltip(self): owner = self.owner recipe = self._get_recipe() if owner.has_state(CraftingTuning.QUALITY_STATE): value_quality = CraftingTuning.QUALITY_STATE_VALUE_MAP.get( owner.get_state(CraftingTuning.QUALITY_STATE)) if value_quality is not None: self.owner.update_tooltip_field( TooltipFieldsComplete.quality, value_quality.state_star_number) if owner.has_state(CraftingTuning.MASTERWORK_STATE): quality_description = recipe.masterwork_name if owner.get_state( CraftingTuning.MASTERWORK_STATE ) is CraftingTuning.MASTERWORK_STATE_VALUE else None if quality_description is not None: self.owner.update_tooltip_field( TooltipFieldsComplete.quality_description, quality_description) def _remove_hovertip(self): owner = self.owner owner.hover_tip = ui_protocols.UiObjectMetadata.HOVER_TIP_DISABLED self.owner.update_tooltip_field(TooltipFieldsComplete.recipe_name, None) self.owner.update_tooltip_field( TooltipFieldsComplete.recipe_description, None) self.owner.update_tooltip_field(TooltipFieldsComplete.crafter_sim_id, 0) self.owner.update_tooltip_field(TooltipFieldsComplete.crafted_by_text, None) self.owner.update_tooltip_field(TooltipFieldsComplete.quality, 0) self.owner.update_tooltip_field(TooltipFieldsComplete.servings, 0) self.owner.update_tooltip_field(TooltipFieldsComplete.spoiled_time, 0) self.owner.update_tooltip_field(TooltipFieldsComplete.percentage_left, None) self.owner.update_tooltip_field(TooltipFieldsComplete.style_name, None) if self.owner.get_tooltip_field( TooltipFieldsComplete.simoleon_value) is not None: self.owner.update_tooltip_field( TooltipFieldsComplete.simoleon_value, owner.current_value) self.owner.update_tooltip_field(TooltipFieldsComplete.main_icon, None) self.owner.update_tooltip_field(TooltipFieldsComplete.sub_icons, None) self.owner.update_tooltip_field( TooltipFieldsComplete.quality_description, None) self.owner.update_tooltip_field(TooltipFieldsComplete.subtext, None) self.owner.update_object_tooltip() def _on_spoil_time_changed(self, _, spoiled_time): if self._last_spoiled_time is None or self._last_spoiled_time != spoiled_time: self._last_spoiled_time = spoiled_time if RetailComponent.SOLD_STATE is not None and self.owner.has_state( RetailComponent.SOLD_STATE.state) and self.owner.get_state( RetailComponent.SOLD_STATE.state ) is RetailComponent.SOLD_STATE: self.owner.update_tooltip_field(TooltipFieldsComplete.spoiled_time, 0, should_update=True) return if spoiled_time is not None: time_in_ticks = spoiled_time.absolute_ticks() self.owner.update_tooltip_field(TooltipFieldsComplete.spoiled_time, time_in_ticks, should_update=True) logger.debug('{} will get spoiled at {}', self.owner, spoiled_time) else: self.owner.update_tooltip_field(TooltipFieldsComplete.spoiled_time, 0, should_update=True) def _on_spoiled(self, _): self._last_spoiled_time = None def _add_spoil_listener(self, state_override=None): check_operator = operator.lt if state_override is not None: self._spoil_timer_state_value = state_override.state_to_track check_operator = state_override.commodity_check_operator if self._spoil_listener_handle is None: if self.owner.has_state(self._spoil_timer_state_value.state): linked_stat = self._spoil_timer_state_value.state.linked_stat tracker = self.owner.get_tracker(linked_stat) if tracker is None: return threshold = sims4.math.Threshold() threshold.value = self._spoil_timer_state_value.range.upper_bound threshold.comparison = check_operator self._spoil_listener_handle = tracker.create_and_add_listener( linked_stat, threshold, self._on_spoiled, on_callback_alarm_reset=self._on_spoil_time_changed) def _on_crafting_process_updated(self): if self._crafting_process.recipe is not None: self._add_hovertip() self.owner.update_component_commodity_flags() @componentmethod def set_crafting_process(self, crafting_process, use_base_recipe=False, is_final_product=False, from_load=False): if is_final_product and (not from_load and self._crafting_process is not None ) and crafting_process.multiple_order_process: new_process = crafting_process.copy_for_serve_interaction( crafting_process.get_order_or_recipe()) self._crafting_process.linked_process = new_process self._crafting_process = new_process else: self._crafting_process = crafting_process self._use_base_recipe = use_base_recipe self._is_final_product = is_final_product self._on_crafting_process_updated() @componentmethod def get_crafting_process(self): return self._crafting_process @componentmethod def on_crafting_process_finished(self): self._add_hovertip() self.owner.update_component_commodity_flags() crafting_process = self._crafting_process if crafting_process is None: return crafting_process.clear_refundables() if crafting_process.current_ico is None: crafting_process.current_ico = self.owner recipe = crafting_process.recipe if self._use_base_recipe: recipe = recipe.get_base_recipe() skill_test = recipe.skill_test if crafting_process.crafter_sim_id is None: return sim_info = services.sim_info_manager().get( crafting_process.crafter_sim_id) created_object_quality = self.owner.get_state( CraftingTuning.QUALITY_STATE) if self.owner.has_state( CraftingTuning.QUALITY_STATE) else None created_object_masterwork = self.owner.get_state( CraftingTuning.MASTERWORK_STATE) if self.owner.has_state( CraftingTuning.MASTERWORK_STATE) else None services.get_event_manager().process_event( test_events.TestEvent.ItemCrafted, sim_info=sim_info, crafted_object=self.owner, skill=skill_test.skill if skill_test is not None else None, quality=created_object_quality, masterwork=created_object_masterwork) @componentmethod def get_recipe(self): if self._crafting_process.recipe is None: return if self.owner.definition is not self._crafting_process.recipe.final_product.definition: for linked_recipe in self._crafting_process.recipe.linked_recipes_map.values( ): if self.owner.definition is linked_recipe.final_product.definition: return linked_recipe return self._crafting_process.recipe @componentmethod def get_photo_definition(self): recipe = self.get_recipe() if recipe is None: return return recipe.photo_definition @componentmethod_with_fallback(lambda *_, **__: None) def get_craftable_property(self, property_type): recipe = self._get_recipe() crafting_process = self._crafting_process if crafting_process is None: return if property_type == GameObjectProperty.RECIPE_NAME: return recipe.get_recipe_name(crafting_process.crafter) if property_type == GameObjectProperty.RECIPE_DESCRIPTION: return recipe.recipe_description(crafting_process.crafter) logger.error( 'Requested crafting property_type {} not found on game_object'. format(property_type), owner='camilogarcia') @componentmethod def get_consume_affordance(self, context=None): if not self._is_final_product: logger.warn( "Attempting to get consume affordances for something that isn't final product." ) return error_dict = {} consumable_component = self.owner.consumable_component if consumable_component is not None: if context is not None: for consume_affordance in consumable_component.consume_affordances: result = consume_affordance.test(target=self.owner, context=context) if result: return consume_affordance error_dict[consume_affordance] = result else: consume_affordance = next( iter(consumable_component.consume_affordances), None) if consume_affordance is not None: return consume_affordance for affordance in self.owner.super_affordances(): from crafting.crafting_interactions import GrabServingSuperInteraction if issubclass(affordance, GrabServingSuperInteraction): if not affordance.consume_affordances_override: return affordance if error_dict: logger.error( 'Failed to find valid consume affordance. Consumable component affordances tested as follows:\n\n{}', error_dict) @componentmethod_with_fallback(lambda: None) def get_notebook_information(self, notebook_entry, notebook_sub_entries): recipe = self.get_recipe() if not recipe.use_ingredients: return sub_entries = (SubEntryData(recipe.guid64, False), ) return (notebook_entry(None, sub_entries=sub_entries), ) def _icon_override_gen(self): recipe = self.get_recipe() if recipe.icon_override is not None: yield recipe.icon_override @componentmethod def set_ready_to_serve(self): self._crafting_process.ready_to_serve = True def component_super_affordances_gen(self, **kwargs): recipe = self.get_recipe() recipe = recipe.get_base_recipe() if not self._use_base_recipe or recipe is None: return for sa in recipe.final_product.super_affordances: yield sa for linked_recipe in recipe.linked_recipes_map.values(): for sa in linked_recipe.final_product.super_affordances: yield sa if not (self._crafting_process.is_complete or self._crafting_process. ready_to_serve) or recipe.resume_affordance: yield recipe.resume_affordance else: yield CraftingTuning.DEFAULT_RESUME_AFFORDANCE def component_interactable_gen(self): yield self def on_client_connect(self, client): if self._crafting_process.recipe is not None: self._add_hovertip() @componentmethod_with_fallback(lambda: False) def has_servings_statistic(self): tracker = self.owner.get_tracker(CraftingTuning.SERVINGS_STATISTIC) if tracker is None or not tracker.has_statistic( CraftingTuning.SERVINGS_STATISTIC): return False return True def _on_households_loaded_update(self): self._add_hovertip() @componentmethod_with_fallback(lambda: None) def post_tooltip_save_data_stored(self): if self._last_spoiled_time is not None: self._on_spoil_time_changed(None, self._last_spoiled_time) def get_recipe_effect_overrides(self, effect_name): if self._crafting_process.recipe is not None and self._crafting_process.recipe: effect_override = self._crafting_process.recipe.vfx_overrides.get( effect_name) if effect_override is not None: return effect_override return effect_name def component_anim_overrides_gen(self): if self._crafting_process.recipe is not None and self._crafting_process.recipe.anim_overrides is not None: yield self._crafting_process.recipe.anim_overrides def save(self, persistence_master_message): logger.info( '[PERSISTENCE]: ----Start saving crafting component of {0}.', self.owner) self.owner.update_tooltip_field(TooltipFieldsComplete.spoiled_time, 0, should_update=True) persistable_data = protocols.PersistenceMaster.PersistableData() persistable_data.type = protocols.PersistenceMaster.PersistableData.CraftingComponent crafting_save = persistable_data.Extensions[ protocols.PersistableCraftingComponent.persistable_data] self._crafting_process.save(crafting_save.process) crafting_save.use_base_recipe = self._use_base_recipe crafting_save.is_final_product = self._is_final_product persistence_master_message.data.extend([persistable_data]) def load(self, crafting_save_message): logger.info( '[PERSISTENCE]: ----Start loading crafting component of {0}.', self.owner) crafting_component_data = crafting_save_message.Extensions[ protocols.PersistableCraftingComponent.persistable_data] crafting_process = CraftingProcess() crafting_process.load(crafting_component_data.process) self.set_crafting_process(crafting_process, crafting_component_data.use_base_recipe, crafting_component_data.is_final_product, from_load=True) recipe = self.get_recipe() if recipe is not None: if crafting_process.crafted_value is not None: self.owner.base_value = crafting_process.crafted_value services.current_zone().register_callback( zone_types.ZoneState.HOUSEHOLDS_AND_SIM_INFOS_LOADED, self._on_households_loaded_update) self.owner.append_tags(recipe.apply_tags)
class SituationGoal(metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager( sims4.resources.Types.SITUATION_GOAL)): __qualname__ = 'SituationGoal' INSTANCE_SUBCLASSES_ONLY = True IS_TARGETED = False INSTANCE_TUNABLES = { '_display_name': TunableLocalizedStringFactory( description= '\n Display name for the Situation Goal. It takes one token, the\n target (0) of this situation goal.\n ', tuning_group=GroupNames.UI), '_icon': TunableResourceKey( 'PNG:missing_image', description= '\n Icon to be displayed for the Situation Goal.\n ', needs_tuning=True, resource_types=sims4.resources.CompoundTypes.IMAGE), '_pre_tests': TunableSituationGoalPreTestSet( description= '\n A set of tests on the player sim and environment that all must\n pass for the goal to be given to the player. e.g. Player Sim\n has cooking skill level 7.\n ', tuning_group=GroupNames.TESTS), '_post_tests': TunableSituationGoalPostTestSet( description= '\n A set of tests that must all pass when the player satisfies the\n goal_test for the goal to be consider completed. e.g. Player\n has Drunk Buff when Kissing another sim at Night.\n ', tuning_group=GroupNames.TESTS), '_environment_pre_tests': TunableSituationGoalEnvironmentPreTestSet( description= '\n A set of sim independent pre tests.\n e.g. There are five desks.\n ', tuning_group=GroupNames.TESTS), 'role_tags': TunableSet( TunableEnumEntry(Tag, Tag.INVALID), description= '\n This goal will only be given to Sims in SituationJobs or Role\n States marked with one of these tags.\n ' ), '_cooldown': TunableSimMinute( description= '\n The cooldown of this situation goal. Goals that have been\n completed will not be chosen again for the amount of time that\n is tuned.\n ', default=600, minimum=0), '_iterations': Tunable( description= '\n Number of times the player must perform the action to complete the goal\n ', tunable_type=int, default=1), '_score': Tunable( description= '\n The number of points received for completing the goal.\n ', tunable_type=int, default=10), '_goal_loot_list': TunableList( description= '\n A list of pre-defined loot actions that will applied to every\n sim in the situation when this situation goal is completed.\n \n Do not use this loot list in an attempt to undo changes made by\n the RoleStates to the sim. For example, do not attempt\n to remove buffs or commodities added by the RoleState.\n ', tunable=SituationGoalLootActions.TunableReference()), '_tooltip': TunableLocalizedStringFactory( description= '\n Tooltip for this situation goal. It takes one token, the\n actor (0) of this situation goal.' ), 'noncancelable': Tunable( description= '\n Checking this box will prevent the player from canceling this goal in the whim system.', tunable_type=bool, needs_tuning=True, default=False), 'time_limit': Tunable( description= '\n Timeout (in Sim minutes) for Sim to complete this goal. The default state of 0 means\n time is unlimited. If the goal is not completed in time, any tuned penalty loot is applied.', tunable_type=int, default=0), 'penalty_loot_list': TunableList( description= '\n A list of pre-defined loot actions that will applied to the Sim who fails\n to complete this goal within the tuned time limit.\n ', tunable=SituationGoalLootActions.TunableReference()), 'goal_completion_notification': OptionalTunable(tunable=UiDialogNotification.TunableFactory( description= '\n A TNS that will fire when this situation goal is completed.\n ' )), 'audio_sting_on_complete': TunableResourceKey( description= '\n The sound to play when this goal is completed.\n ', default=None, resource_types=(sims4.resources.Types.PROPX, ), tuning_group=GroupNames.AUDIO), 'goal_completion_modal_dialog': OptionalTunable(tunable=UiDialogOk.TunableFactory( description= '\n A modal dialog that will fire when this situation goal is\n completed.\n ' )) } @classmethod def can_be_given_as_goal(cls, actor, situation, **kwargs): if actor is not None: resolver = event_testing.resolver.DataResolver( actor.sim_info, None) result = cls._pre_tests.run_tests(resolver) if not result: return result environment_test_result = cls._environment_pre_tests.run_tests( Resolver()) if not environment_test_result: return environment_test_result return TestResult.TRUE def __init__(self, sim_info=None, situation=None, goal_id=0, count=0, **kwargs): self._sim_info = sim_info self._situation = situation self.id = goal_id self._on_goal_completed_callbacks = CallableList() self._completed_time = None self._count = count def destroy(self): self.decommision() self._sim_info = None self._situation = None def decommision(self): self._on_goal_completed_callbacks.clear() def create_seedling(self): actor_id = 0 if self._sim_info is None else self._sim_info.sim_id seedling = situations.situation_serialization.GoalSeedling( type(self), actor_id, self._count) return seedling def register_for_on_goal_completed_callback(self, listener): self._on_goal_completed_callbacks.append(listener) def unregister_for_on_goal_completed_callback(self, listener): self._on_goal_completed_callbacks.remove(listener) def get_gsi_name(self): if self._iterations <= 1: return self.__class__.__name__ return '{} {}/{}'.format(self.__class__.__name__, self._count, self._iterations) def _on_goal_completed(self): self._completed_time = services.time_service().sim_now for loots in self._goal_loot_list: for loot in loots.goal_loot_actions: for sim in self._situation.all_sims_in_situation_gen(): loot.apply_to_resolver(sim.get_resolver()) client = services.client_manager().get_first_client() if client is not None: active_sim = client.active_sim resolver = SingleSimResolver(active_sim) if self.goal_completion_notification is not None: notification = self.goal_completion_notification( active_sim, resolver=resolver) notification.show_dialog() if self.goal_completion_modal_dialog is not None: dialog = self.goal_completion_modal_dialog(active_sim, resolver=resolver) dialog.show_dialog() self._on_goal_completed_callbacks(self, True) def _on_iteration_completed(self): self._on_goal_completed_callbacks(self, False) def debug_force_complete(self, target_sim): self._count = self._iterations self._on_goal_completed() def handle_event(self, sim_info, event, resolver): if self._sim_info is not None and self._sim_info is not sim_info: return if self._run_goal_completion_tests(sim_info, event, resolver): if self._count >= self._iterations: self._on_goal_completed() else: self._on_iteration_completed() def _run_goal_completion_tests(self, sim_info, event, resolver): return self._post_tests.run_tests(resolver) def _get_actual_target_sim_info(self): pass def get_required_target_sim_info(self): pass @property def created_time(self): pass @property def completed_time(self): return self._completed_time def is_on_cooldown(self): if self._completed_time is None: return False time_since_last_completion = services.time_service( ).sim_now - self._completed_time return time_since_last_completion < interval_in_sim_minutes( self._cooldown) def get_localization_tokens(self): if self._sim_info is None: return (self._numerical_token, ) required_tgt_sim = self.get_required_target_sim_info() if required_tgt_sim is None: return (self._numerical_token, self._sim_info) return (self._numerical_token, self._sim_info, required_tgt_sim) @property def display_name(self): return self._display_name(*self.get_localization_tokens()) @property def tooltip(self): return self._tooltip(*self.get_localization_tokens()) @property def score(self): return self._score @property def completed_iterations(self): return self._count @property def max_iterations(self): return self._iterations @property def _numerical_token(self): return self.max_iterations
class BuffComponent(objects.components.Component, component_name=objects.components.types.BUFF_COMPONENT): __qualname__ = 'BuffComponent' DEFAULT_MOOD = TunableReference(services.mood_manager(), description='The default initial mood.') UPDATE_INTENSITY_BUFFER = TunableRange(description="\n A buffer that prevents a mood from becoming active unless its intensity\n is greater than the current active mood's intensity plus this amount.\n \n For example, if this tunable is 1, and the Sim is in a Flirty mood with\n intensity 2, then a different mood would become the active mood only if\n its intensity is 3+.\n \n If the predominant mood has an intensity that is less than the active\n mood's intensity, that mood will become the active mood.\n ", tunable_type=int, default=1, minimum=0) EXCLUSIVE_SET = TunableList(description='\n A list of buff groups to determine which buffs are exclusive from each\n other within the same group. A buff cannot exist in more than one exclusive group.\n \n The following rule of exclusivity for a group:\n 1. Higher weight will always be added and remove any lower weight buffs\n 2. Lower weight buff will not be added if a higher weight already exist in component\n 3. Same weight buff will always be added and remove any buff with same weight.\n \n Example: Group 1:\n Buff1 with weight of 5 \n Buff2 with weight of 1\n Buff3 with weight of 1\n Group 2:\n Buff4 with weight of 6\n \n If sim has Buff1, trying to add Buff2 or Buff3 will not be added.\n If sim has Buff2, trying to add Buff3 will remove Buff2 and add Buff3\n If sim has Buff2, trying to add Buff1 will remove Buff 2 and add Buff3\n If sim has Buff4, trying to add Buff1, Buff2, or Buff3 will be added and Buff4 will stay \n on component \n ', tunable=TunableList(tunable=TunableTuple(buff_type=TunableReference(description='\n Buff in exclusive group\n ', manager=services.get_instance_manager(sims4.resources.Types.BUFF)), weight=Tunable(description='\n weight to determine if this buff should be added and\n remove other buffs in the exclusive group or not added at all.\n \n Example: Buff1 with weight of 5 \n Buff2 with weight of 1\n Buff3 with weight of 1\n \n If sim has Buff1, trying to add Buff2 or Buff3 will not be added.\n If sim has Buff2, trying to add Buff3 will remove Buff2 and add Buff3\n if sim has Buff2, trying to add Buff1 will remove Buff 2 and add Buff3\n ', tunable_type=int, default=1)))) def __init__(self, owner): super().__init__(owner) self._active_buffs = {} self._get_next_handle_id = UniqueIdGenerator() self._success_chance_modification = 0 self._active_mood = self.DEFAULT_MOOD self._active_mood_intensity = 0 self._active_mood_buff_handle = None self.on_mood_changed = CallableList() self.on_mood_changed.append(self._publish_mood_update) self.on_mood_changed.append(self._send_mood_changed_event) self.load_in_progress = False self.on_buff_added = CallableList() self.on_buff_removed = CallableList() self.buff_update_alarms = {} if self._active_mood is None: logger.error('No default mood tuned in buff_component.py') elif self._active_mood.buffs: initial_buff_ref = self._active_mood.buffs[0] if initial_buff_ref and initial_buff_ref.buff_type: self._active_mood_buff_handle = self.add_buff(initial_buff_ref.buff_type) def __iter__(self): return self._active_buffs.values().__iter__() def __len__(self): return len(self._active_buffs) def on_sim_ready_to_simulate(self): for buff in self: buff.on_sim_ready_to_simulate() self._publish_mood_update() def on_sim_removed(self, *args, **kwargs): for buff in self: buff.on_sim_removed(*args, **kwargs) def clean_up(self): for (buff_type, buff_entry) in tuple(self._active_buffs.items()): self.remove_auto_update(buff_type) buff_entry.clean_up() self._active_buffs.clear() self.on_mood_changed.clear() self.on_buff_added.clear() self.on_buff_removed.clear() @objects.components.componentmethod def add_buff_from_op(self, buff_type, buff_reason=None): (can_add, _) = self._can_add_buff_type(buff_type) if not can_add: return False buff_commodity = buff_type.commodity if buff_commodity is not None: if not buff_type.refresh_on_add and self.has_buff(buff_type): return False tracker = self.owner.get_tracker(buff_commodity) if buff_commodity.convergence_value == buff_commodity.max_value: tracker.set_min(buff_commodity) else: tracker.set_max(buff_commodity) self.set_buff_reason(buff_type, buff_reason, use_replacement=True) else: self.add_buff(buff_type, buff_reason=buff_reason) return True @objects.components.componentmethod def add_buff(self, buff_type, buff_reason=None, update_mood=True, commodity_guid=None, replacing_buff=None, timeout_string=None, transition_into_buff_id=0, change_rate=None, immediate=False): replacement_buff_type = self._get_replacement_buff_type(buff_type) if replacement_buff_type is not None: return self.owner.add_buff(replacement_buff_type, buff_reason=buff_reason, update_mood=update_mood, commodity_guid=commodity_guid, replacing_buff=buff_type, timeout_string=timeout_string, transition_into_buff_id=transition_into_buff_id, change_rate=change_rate, immediate=immediate) (can_add, conflicting_buff_type) = self._can_add_buff_type(buff_type) if not can_add: return buff = self._active_buffs.get(buff_type) if buff is None: buff = buff_type(self.owner, commodity_guid, replacing_buff, transition_into_buff_id) self._active_buffs[buff_type] = buff buff.on_add(self.load_in_progress) self._update_chance_modifier() if update_mood: self._update_current_mood() if self.owner.household is not None: services.get_event_manager().process_event(test_events.TestEvent.BuffBeganEvent, sim_info=self.owner, sim_id=self.owner.sim_id, buff=buff_type) self.register_auto_update(self.owner, buff_type) self.on_buff_added(buff_type) handle_id = self._get_next_handle_id() buff.add_handle(handle_id, buff_reason=buff_reason) self.send_buff_update_msg(buff, True, change_rate=change_rate, immediate=immediate) if conflicting_buff_type is not None: self.remove_buff_by_type(conflicting_buff_type) return handle_id def _get_replacement_buff_type(self, buff_type): if buff_type.trait_replacement_buffs is not None: trait_tracker = self.owner.trait_tracker for (trait, replacement_buff_type) in buff_type.trait_replacement_buffs.items(): while trait_tracker.has_trait(trait): return replacement_buff_type def register_auto_update(self, sim_info_in, buff_type_in): if buff_type_in in self.buff_update_alarms: self.remove_auto_update(buff_type_in) if sim_info_in.is_selectable and buff_type_in.visible: self.buff_update_alarms[buff_type_in] = alarms.add_alarm(self, create_time_span(minutes=15), lambda _, sim_info=sim_info_in, buff_type=buff_type_in: services.get_event_manager().process_event(test_events.TestEvent.BuffUpdateEvent, sim_info=sim_info, sim_id=sim_info.sim_id, buff=buff_type), True) def remove_auto_update(self, buff_type): if buff_type in self.buff_update_alarms: alarms.cancel_alarm(self.buff_update_alarms[buff_type]) del self.buff_update_alarms[buff_type] @objects.components.componentmethod def remove_buff(self, handle_id, update_mood=True, immediate=False, on_destroy=False): for (buff_type, buff_entry) in self._active_buffs.items(): while handle_id in buff_entry.handle_ids: should_remove = buff_entry.remove_handle(handle_id) if should_remove: del self._active_buffs[buff_type] buff_entry.on_remove(not self.load_in_progress and not on_destroy) if not on_destroy: if update_mood: self._update_current_mood() self._update_chance_modifier() self.send_buff_update_msg(buff_entry, False, immediate=immediate) services.get_event_manager().process_event(test_events.TestEvent.BuffEndedEvent, sim_info=self.owner, sim_id=self.owner.sim_id, buff=buff_type) if buff_type in self.buff_update_alarms: self.remove_auto_update(buff_type) self.on_buff_removed(buff_type) break @objects.components.componentmethod def get_buff_type(self, handle_id): for (buff_type, buff_entry) in self._active_buffs.items(): while handle_id in buff_entry.handle_ids: return buff_type @objects.components.componentmethod def has_buff(self, buff_type): return buff_type in self._active_buffs @objects.components.componentmethod def get_active_buff_types(self): return self._active_buffs.keys() @objects.components.componentmethod def get_buff_reason(self, handle_id): for buff_entry in self._active_buffs.values(): while handle_id in buff_entry.handle_ids: return buff_entry.buff_reason @objects.components.componentmethod def debug_add_buff_by_type(self, buff_type): (can_add, conflicting_buff_type) = self._can_add_buff_type(buff_type) if not can_add: return False if buff_type.commodity is not None: tracker = self.owner.get_tracker(buff_type.commodity) state_index = buff_type.commodity.get_state_index_matches_buff_type(buff_type) if state_index is not None: index = state_index + 1 if index < len(buff_type.commodity.commodity_states): commodity_to_value = buff_type.commodity.commodity_states[index].value - 1 else: commodity_to_value = buff_type.commodity.max_value tracker.set_value(buff_type.commodity, commodity_to_value) else: logger.error('commodity ({}) has no states with buff ({}), Buff will not be added.', buff_type.commodity, buff_type) return False else: self.add_buff(buff_type) if conflicting_buff_type is not None: self.remove_buff_by_type(conflicting_buff_type) return True @objects.components.componentmethod def remove_buff_by_type(self, buff_type, on_destroy=False): buff_entry = self._active_buffs.get(buff_type) self.remove_buff_entry(buff_entry, on_destroy=on_destroy) @objects.components.componentmethod def remove_buff_entry(self, buff_entry, on_destroy=False): if buff_entry is not None: if buff_entry.commodity is not None: tracker = self.owner.get_tracker(buff_entry.commodity) commodity_inst = tracker.get_statistic(buff_entry.commodity) if commodity_inst is not None and commodity_inst.core: if not on_destroy: logger.callstack('Attempting to explicitly remove the buff {}, which is given by a core commodity. This would result in the removal of a core commodity and will be ignored.', buff_entry, owner='tastle', level=sims4.log.LEVEL_ERROR) return tracker.remove_statistic(buff_entry.commodity, on_destroy=on_destroy) elif buff_entry.buff_type in self._active_buffs: buff_entry.on_remove(on_destroy) del self._active_buffs[buff_entry.buff_type] if not on_destroy: self._update_chance_modifier() self._update_current_mood() self.send_buff_update_msg(buff_entry, False) services.get_event_manager().process_event(test_events.TestEvent.BuffEndedEvent, sim_info=self.owner, buff=type(buff_entry), sim_id=self.owner.id) @objects.components.componentmethod def set_buff_reason(self, buff_type, buff_reason, use_replacement=False): if use_replacement: replacement_buff_type = self._get_replacement_buff_type(buff_type) if replacement_buff_type is not None: buff_type = replacement_buff_type buff_entry = self._active_buffs.get(buff_type) if buff_entry is not None and buff_reason is not None: buff_entry.buff_reason = buff_reason self.send_buff_update_msg(buff_entry, True) @objects.components.componentmethod def buff_commodity_changed(self, handle_id, change_rate=None): for (_, buff_entry) in self._active_buffs.items(): while handle_id in buff_entry.handle_ids: if buff_entry.show_timeout: self.send_buff_update_msg(buff_entry, True, change_rate=change_rate) break @objects.components.componentmethod def get_success_chance_modifier(self): return self._success_chance_modification @objects.components.componentmethod def get_actor_scoring_modifier(self, affordance): total = 0 for buff_entry in self._active_buffs.values(): total += buff_entry.effect_modification.get_affordance_scoring_modifier(affordance) return total @objects.components.componentmethod def get_actor_success_modifier(self, affordance): total = 0 for buff_entry in self._active_buffs.values(): total += buff_entry.effect_modification.get_affordance_success_modifier(affordance) return total @objects.components.componentmethod def get_mood(self): return self._active_mood @objects.components.componentmethod def get_mood_animation_param_name(self): param_name = self._active_mood.asm_param_name if param_name is not None: return param_name (mood, _, _) = self._get_largest_mood(predicate=lambda mood: return True if mood.asm_param_name else False) return mood.asm_param_name @objects.components.componentmethod def get_mood_intensity(self): return self._active_mood_intensity @objects.components.componentmethod def get_effective_skill_level(self, skill): if skill.stat_type == skill: skill = self.owner.get_stat_instance(skill) if skill is None: return 0 modifier = 0 for buff_entry in self._active_buffs.values(): modifier += buff_entry.effect_modification.get_effective_skill_modifier(skill) return skill.get_user_value() + modifier @objects.components.componentmethod def effective_skill_modified_buff_gen(self, skill): if skill.stat_type == skill: skill = self.owner.get_stat_instance(skill) for buff_entry in self._active_buffs.values(): modifier = buff_entry.effect_modification.get_effective_skill_modifier(skill) while modifier != 0: yield (buff_entry, modifier) @objects.components.componentmethod def is_appropriate(self, tags): final_appropriateness = Appropriateness.DONT_CARE for buff in self._active_buffs: appropriateness = buff.get_appropriateness(tags) while appropriateness > final_appropriateness: final_appropriateness = appropriateness if final_appropriateness == Appropriateness.NOT_ALLOWED: return False return True def get_additional_create_ops_gen(self): yield GenericProtocolBufferOp(Operation.SIM_MOOD_UPDATE, self._create_mood_update_msg()) for buff in self: while buff.visible: yield GenericProtocolBufferOp(Operation.SIM_BUFF_UPDATE, self._create_buff_update_msg(buff, True)) def _publish_mood_update(self): if self.owner.valid_for_distribution and self.owner.visible_to_client == True: Distributor.instance().add_op(self.owner, GenericProtocolBufferOp(Operation.SIM_MOOD_UPDATE, self._create_mood_update_msg())) def _send_mood_changed_event(self): if not self.owner.is_npc: self.owner.whim_tracker.refresh_emotion_whim() services.get_event_manager().process_event(test_events.TestEvent.MoodChange, sim_info=self.owner) def _create_mood_update_msg(self): mood_msg = Commodities_pb2.MoodUpdate() mood_msg.sim_id = self.owner.id mood_msg.mood_key = self._active_mood.guid64 mood_msg.mood_intensity = self._active_mood_intensity return mood_msg def _create_buff_update_msg(self, buff, equipped, change_rate=None): buff_msg = Sims_pb2.BuffUpdate() buff_msg.buff_id = buff.guid64 buff_msg.sim_id = self.owner.id buff_msg.equipped = equipped if buff.buff_reason is not None: buff_msg.reason = buff.buff_reason if equipped and buff.show_timeout: (timeout, rate_multiplier) = buff.get_timeout_time() buff_msg.timeout = timeout buff_msg.rate_multiplier = rate_multiplier if change_rate is not None: if change_rate == 0: progress_arrow = Sims_pb2.BUFF_PROGRESS_NONE elif change_rate > 0: progress_arrow = Sims_pb2.BUFF_PROGRESS_UP if not buff.flip_arrow_for_progress_update else Sims_pb2.BUFF_PROGRESS_DOWN else: progress_arrow = Sims_pb2.BUFF_PROGRESS_DOWN if not buff.flip_arrow_for_progress_update else Sims_pb2.BUFF_PROGRESS_UP buff_msg.buff_progress = progress_arrow buff_msg.is_mood_buff = buff.is_mood_buff buff_msg.commodity_guid = buff.commodity_guid or 0 if buff.mood_override is not None: buff_msg.mood_type_override = buff.mood_override.guid64 buff_msg.transition_into_buff_id = buff.transition_into_buff_id return buff_msg def send_buff_update_msg(self, buff, equipped, change_rate=None, immediate=False): if not buff.visible: return if self.owner.valid_for_distribution and self.owner.is_sim and self.owner.is_selectable: buff_msg = self._create_buff_update_msg(buff, equipped, change_rate=change_rate) if gsi_handlers.buff_handlers.sim_buff_log_archiver.enabled: gsi_handlers.buff_handlers.archive_buff_message(buff_msg, equipped, change_rate) Distributor.instance().add_op(self.owner, GenericProtocolBufferOp(Operation.SIM_BUFF_UPDATE, buff_msg)) def _can_add_buff_type(self, buff_type): if not buff_type.can_add(self.owner): return (False, None) mood = buff_type.mood_type if mood is not None and mood.excluding_traits is not None and self.owner.trait_tracker.has_any_trait(mood.excluding_traits): return (False, None) if buff_type.exclusive_index is None: return (True, None) for conflicting_buff_type in self._active_buffs: while conflicting_buff_type.exclusive_index == buff_type.exclusive_index: if buff_type.exclusive_weight < conflicting_buff_type.exclusive_weight: return (False, None) return (True, conflicting_buff_type) return (True, None) def _update_chance_modifier(self): positive_success_buff_delta = 0 negative_success_buff_delta = 1 for buff_entry in self._active_buffs.values(): if buff_entry.success_modifier > 0: positive_success_buff_delta += buff_entry.get_success_modifier else: negative_success_buff_delta *= 1 + buff_entry.get_success_modifier self._success_chance_modification = positive_success_buff_delta - (1 - negative_success_buff_delta) def _get_largest_mood(self, predicate=None, buffs_to_ignore=()): weights = {} polarity_to_changeable_buffs = collections.defaultdict(list) polarity_to_largest_mood_and_weight = {} for buff_entry in self._active_buffs.values(): current_mood = buff_entry.mood_type current_weight = buff_entry.mood_weight while not current_mood is None: if current_weight == 0: pass if not (predicate is not None and predicate(current_mood)): pass if buff_entry in buffs_to_ignore: pass current_polarity = current_mood.buff_polarity if buff_entry.is_changeable: polarity_to_changeable_buffs[current_polarity].append(buff_entry) total_current_weight = weights.get(current_mood, 0) total_current_weight += current_weight weights[current_mood] = total_current_weight (largest_mood, largest_weight) = polarity_to_largest_mood_and_weight.get(current_polarity, (None, None)) if largest_mood is None: polarity_to_largest_mood_and_weight[current_polarity] = (current_mood, total_current_weight) else: while total_current_weight > largest_weight: polarity_to_largest_mood_and_weight[current_polarity] = (current_mood, total_current_weight) all_changeable_buffs = [] for (buff_polarity, changeable_buffs) in polarity_to_changeable_buffs.items(): (largest_mood, largest_weight) = polarity_to_largest_mood_and_weight.get(buff_polarity, (None, None)) if largest_mood is not None: for buff_entry in changeable_buffs: if buff_entry.mood_override is not largest_mood: all_changeable_buffs.append((buff_entry, largest_mood)) largest_weight += buff_entry.mood_weight polarity_to_largest_mood_and_weight[buff_polarity] = (largest_mood, largest_weight) else: weights = {} largest_weight = 0 for buff_entry in changeable_buffs: if buff_entry.mood_override is not None: all_changeable_buffs.append((buff_entry, None)) current_mood = buff_entry.mood_type current_weight = buff_entry.mood_weight total_current_weight = weights.get(current_mood, 0) total_current_weight += current_weight weights[current_mood] = total_current_weight while total_current_weight > largest_weight: largest_weight = total_current_weight largest_mood = current_mood while largest_mood is not None and largest_weight != 0: polarity_to_largest_mood_and_weight[buff_polarity] = (largest_mood, largest_weight) largest_weight = 0 largest_mood = self.DEFAULT_MOOD active_mood = self._active_mood if polarity_to_largest_mood_and_weight: (mood, weight) = max(polarity_to_largest_mood_and_weight.values(), key=operator.itemgetter(1)) if weight > largest_weight or weight == largest_weight and mood is active_mood: largest_weight = weight largest_mood = mood return (largest_mood, largest_weight, all_changeable_buffs) def _update_current_mood(self): (largest_mood, largest_weight, changeable_buffs) = self._get_largest_mood() if largest_mood is not None: intensity = self._get_intensity_from_mood(largest_mood, largest_weight) if self._should_update_mood(largest_mood, intensity, changeable_buffs): if self._active_mood_buff_handle is not None: active_mood_buff_handle = self._active_mood_buff_handle self.remove_buff(active_mood_buff_handle, update_mood=False) if active_mood_buff_handle == self._active_mood_buff_handle: self._active_mood_buff_handle = None else: return self._active_mood = largest_mood self._active_mood_intensity = intensity if len(largest_mood.buffs) >= intensity: tuned_buff = largest_mood.buffs[intensity] if tuned_buff is not None and tuned_buff.buff_type is not None: self._active_mood_buff_handle = self.add_buff(tuned_buff.buff_type, update_mood=False) if gsi_handlers.buff_handlers.sim_mood_log_archiver.enabled and self.owner.valid_for_distribution and self.owner.visible_to_client == True: gsi_handlers.buff_handlers.archive_mood_message(self.owner.id, self._active_mood, self._active_mood_intensity, self._active_buffs, changeable_buffs) caches.clear_all_caches() self.on_mood_changed() for (changeable_buff, mood_override) in changeable_buffs: changeable_buff.mood_override = mood_override self.send_buff_update_msg(changeable_buff, True) def _get_intensity_from_mood(self, mood, weight): intensity = 0 for threshold in mood.intensity_thresholds: if weight >= threshold: intensity += 1 else: break return intensity def _should_update_mood(self, mood, intensity, changeable_buffs): active_mood = self._active_mood active_mood_intensity = self._active_mood_intensity if mood is active_mood: return intensity != active_mood_intensity total_weight = sum(buff_entry.mood_weight for buff_entry in self._active_buffs.values() if buff_entry.mood_type is active_mood) active_mood_intensity = self._get_intensity_from_mood(active_mood, total_weight) if changeable_buffs and not self._active_mood.is_changeable: buffs_to_ignore = [changeable_buff for (changeable_buff, _) in changeable_buffs] (largest_mood, largest_weight, _) = self._get_largest_mood(buffs_to_ignore=buffs_to_ignore) new_intensity = self._get_intensity_from_mood(largest_mood, largest_weight) if self._should_update_mood(largest_mood, new_intensity, None): active_mood = largest_mood active_mood_intensity = new_intensity if active_mood.is_changeable and mood.buff_polarity == active_mood.buff_polarity: return True if not intensity or intensity < active_mood_intensity: return True if intensity >= active_mood_intensity + self.UPDATE_INTENSITY_BUFFER: return True if mood is self.DEFAULT_MOOD or active_mood is self.DEFAULT_MOOD: return True return False
class PersistenceService(Service): def __init__(self): super().__init__() self._save_locks = [] self._read_write_locked = False self._save_game_data_proto = serialization.SaveGameData() self.save_timeline = None self._unlocked_callbacks = CallableList() self.once_per_session_telemetry_sent = False self.save_error_code = persistence_error_types.ErrorCodes.NO_ERROR self._zone_data_pb_cache = {} self._world_id_to_region_id_cache = {} self._sim_data_pb_cache = None self._household_pb_cache = None self._world_ids = frozenset() def setup(self, **kwargs): self._time_of_last_save = None def build_caches(self): self._world_id_to_region_id_cache.clear() self._zone_data_pb_cache.clear() for zone in self._save_game_data_proto.zones: self._zone_data_pb_cache[zone.zone_id] = zone world_id = zone.world_id if world_id in self._world_id_to_region_id_cache: continue neighborhood_id = zone.neighborhood_id for neighborhood in self._save_game_data_proto.neighborhoods: if neighborhood.neighborhood_id == neighborhood_id: self._world_id_to_region_id_cache[world_id] = neighborhood.region_id self._world_ids = frozenset(z.world_id for z in self._zone_data_pb_cache.values()) self.dirty_sim_data_pb_cache() self._household_pb_cache = None def dirty_sim_data_pb_cache(self): self._sim_data_pb_cache = None def _get_sim_data_pb_cache(self): if self._sim_data_pb_cache is None: self._sim_data_pb_cache = {sim_pb.sim_id: index for (index, sim_pb) in enumerate(self._save_game_data_proto.sims)} return self._sim_data_pb_cache def _get_household_data_pb_cache(self): if self._household_pb_cache is not None: cache_len = len(self._household_pb_cache) actual_len = len(self._save_game_data_proto.households) if cache_len < actual_len: self._household_pb_cache = None elif cache_len > actual_len: logger.error('_household_pb_cache contains more than it should', owner='tingyul') if self._household_pb_cache is None: self._household_pb_cache = {household_pb.household_id: index for (index, household_pb) in enumerate(self._save_game_data_proto.households)} return self._household_pb_cache def is_save_locked(self): if self._read_write_locked: return True elif not self._save_locks: return False return True def is_save_locked_exclusively_by_holder(self, lock_holder): return self.is_save_locked() and (len(self._save_locks) == 1 and lock_holder in self._save_locks) def get_save_lock_tooltip(self): if self._read_write_locked: return PersistenceTuning.SAVE_FAILED_REASONS.generic elif self._save_locks: return self._save_locks[-1].get_lock_save_reason() def set_read_write_lock(self, is_locked, reference_id): changed_lock = self._read_write_locked != is_locked self._read_write_locked = is_locked if changed_lock: if is_locked: self._send_lock_save_message(lambda : PersistenceTuning.SAVE_FAILED_REASONS.generic) elif self.is_save_locked(): self._send_lock_save_message(self.get_save_lock_tooltip) else: self._send_unlock_save_message() self._try_invoke_unlocked_callbacks() def get_save_game_data_proto(self): return self._save_game_data_proto def lock_save(self, lock_holder): self._save_locks.append(lock_holder) self._send_lock_save_message(lock_holder.get_lock_save_reason) def unlock_save(self, lock_holder, send_event=True): if lock_holder in self._save_locks: self._save_locks.remove(lock_holder) self._try_invoke_unlocked_callbacks() if send_event: if not self.is_save_locked(): self._send_unlock_save_message() else: self._send_lock_save_message(self.get_save_lock_tooltip) def _send_lock_save_message(self, reason_provider): distributor = Distributor.instance() if distributor is not None and distributor.client is not None: msg = UI_pb2.GameSaveLockUnlock() msg.is_locked = True msg.lock_reason = reason_provider() distributor.add_event(MSG_GAME_SAVE_LOCK_UNLOCK, msg) def _send_unlock_save_message(self): distributor = Distributor.instance() if distributor is not None and distributor.client is not None: msg = UI_pb2.GameSaveLockUnlock() msg.is_locked = False distributor.add_event(MSG_GAME_SAVE_LOCK_UNLOCK, msg) def remove_save_locks(self): if self._save_locks: self._save_locks.clear() self._send_unlock_save_message() self._unlocked_callbacks.clear() def add_save_unlock_callback(self, callback): if self.is_save_locked(): self._unlocked_callbacks.register(callback) else: callback() def _try_invoke_unlocked_callbacks(self): if self._unlocked_callbacks and not self.is_save_locked(): self._unlocked_callbacks() self._unlocked_callbacks.clear() def _create_save_timeline(self): self._destroy_save_timeline(self.save_timeline) self.save_timeline = scheduling.Timeline(services.time_service().sim_now) def _destroy_save_timeline(self, timeline): if self.save_timeline is not timeline: raise RuntimeError('Attempting to destroy the wrong timeline!') if self.save_timeline is not None: self.save_timeline = None timeline.teardown() def save_using(self, save_generator, *args, **kwargs): def call_save_game_gen(timeline): result = yield from save_generator(timeline, *args, **kwargs) return result yield self._create_save_timeline() element = elements.GeneratorElement(call_save_game_gen) element = elements.WithFinallyElement(element, self._destroy_save_timeline) element_handle = self.save_timeline.schedule(element) return element_handle def save_to_scratch_slot_gen(self, timeline): save_game_data = SaveGameData(0, 'scratch', True, None) save_result_code = yield from self.save_game_gen(timeline, save_game_data, send_save_message=False, check_cooldown=False) return save_result_code yield def save_game_gen(self, timeline, save_game_data, send_save_message=True, check_cooldown=False, ignore_callback=False): (result_code, failure_reason) = yield from self._save_game_gen(timeline, save_game_data, check_cooldown=check_cooldown) if send_save_message: msg = UI_pb2.GameSaveComplete() msg.return_status = result_code msg.save_cooldown = self._get_cooldown() if failure_reason is not None: msg.failure_reason = failure_reason msg.slot_id = save_game_data.slot_id distributor = Distributor.instance() distributor.add_event(MSG_GAME_SAVE_COMPLETE, msg) return result_code yield def _save_game_gen(self, timeline, save_game_data, check_cooldown=True): save_lock_reason = self.get_save_lock_tooltip() if save_lock_reason is not None: return (SaveGameResult.FAILED_SAVE_LOCKED, save_lock_reason) yield current_time = services.server_clock_service().now() result_code = SaveGameResult.FAILED_ON_COOLDOWN if self._time_of_last_save is not None: cooldown = (current_time - self._time_of_last_save).in_real_world_seconds() else: cooldown = PersistenceTuning.SAVE_GAME_COOLDOWN + 1 if not check_cooldown or cooldown > PersistenceTuning.SAVE_GAME_COOLDOWN: result_code = SaveGameResult.SUCCESS error_code_string = None try: yield from self._fill_and_send_save_game_protobufs_gen(timeline, save_game_data.slot_id, save_game_data.slot_name, auto_save_slot_id=save_game_data.auto_save_slot_id) except Exception as e: result_code = SaveGameResult.FAILED_EXCEPTION_OCCURRED error_code_string = persistence_error_types.generate_exception_code(self.save_error_code, e) logger.exception('Save failed due to Exception', exc=e) with telemetry_helper.begin_hook(save_telemetry_writer, TELEMETRY_HOOK_SAVE_FAIL) as hook: hook.write_int(TELEMETRY_FIELD_ERROR_CODE, self.save_error_code) hook.write_int(TELEMETRY_FIELD_STACK_HASH, sims4.hash_util.hash64(error_code_string)) finally: self.save_error_code = persistence_error_types.ErrorCodes.NO_ERROR if check_cooldown: if result_code == SaveGameResult.SUCCESS: self._time_of_last_save = current_time failure_reason = self._get_failure_reason_for_result_code(result_code, error_code_string) return (result_code, failure_reason) yield def _get_failure_reason_for_result_code(self, result_code, exception_code_string): if result_code == SaveGameResult.SUCCESS: return if result_code == SaveGameResult.FAILED_ON_COOLDOWN: return PersistenceTuning.SAVE_FAILED_REASONS.on_cooldown if result_code == SaveGameResult.FAILED_EXCEPTION_OCCURRED: return PersistenceTuning.SAVE_FAILED_REASONS.exception_occurred(exception_code_string) return PersistenceTuning.SAVE_FAILED_REASONS.generic def _get_cooldown(self): if self._time_of_last_save is not None: current_time = services.server_clock_service().now() cooldown = PersistenceTuning.SAVE_GAME_COOLDOWN - (current_time - self._time_of_last_save).in_real_world_seconds() return cooldown return 0 def _fill_and_send_save_game_protobufs_gen(self, timeline, slot_id, slot_name, auto_save_slot_id=None): self.save_error_code = persistence_error_types.ErrorCodes.SETTING_SAVE_SLOT_DATA_FAILED save_slot_data_msg = self.get_save_slot_proto_buff() save_slot_data_msg.slot_id = slot_id save_slot_data_msg.slot_name = slot_name if services.active_household_id() is not None: save_slot_data_msg.active_household_id = services.active_household_id() sims4.core_services.service_manager.save_all_services(self, save_slot_data=save_slot_data_msg) self.save_error_code = persistence_error_types.ErrorCodes.SAVE_CAMERA_DATA_FAILED camera.serialize(save_slot_data=save_slot_data_msg) def on_save_complete(slot_id, success): wakeable_element.trigger_soft_stop() self.save_error_code = persistence_error_types.ErrorCodes.SAVE_TO_SLOT_FAILED wakeable_element = element_utils.soft_sleep_forever() persistence_module.run_persistence_operation(persistence_module.PersistenceOpType.kPersistenceOpSave, self._save_game_data_proto, slot_id, on_save_complete) yield from element_utils.run_child(timeline, wakeable_element) if auto_save_slot_id is not None: self.save_error_code = persistence_error_types.ErrorCodes.AUTOSAVE_TO_SLOT_FAILED wakeable_element = element_utils.soft_sleep_forever() persistence_module.run_persistence_operation(persistence_module.PersistenceOpType.kPersistenceOpSave, self._save_game_data_proto, auto_save_slot_id, on_save_complete) yield from element_utils.run_child(timeline, wakeable_element) self.save_error_code = persistence_error_types.ErrorCodes.NO_ERROR def get_world_ids(self): return self._world_ids def get_lot_proto_buff(self, lot_id): zone_id = self.resolve_lot_id_into_zone_id(lot_id) if zone_id is not None: neighborhood_data = self.get_neighborhood_proto_buff(services.current_zone().neighborhood_id) if neighborhood_data is not None: for lot_owner_data in neighborhood_data.lots: if zone_id == lot_owner_data.zone_instance_id: return lot_owner_data def get_neighborhood_agnostic_lot_proto_buff(self, lot_id): (zone_id, neighborhood_id) = self.resolve_lot_id_into_zone_and_neighborhood_id(lot_id) if zone_id is None: return neighborhood_data = self.get_neighborhood_proto_buff(neighborhood_id) if neighborhood_data is None: return for lot_owner_data in neighborhood_data.lots: if zone_id == lot_owner_data.zone_instance_id: return lot_owner_data def get_zone_proto_buff(self, zone_id): if zone_id in self._zone_data_pb_cache: return self._zone_data_pb_cache[zone_id] def get_house_description_id(self, zone_id): zone_data = self.get_zone_proto_buff(zone_id) house_description_id = services.get_house_description_id(zone_data.lot_template_id, zone_data.lot_description_id, zone_data.active_plex) return house_description_id def get_lot_data_from_zone_data(self, zone_data): neighborhood_data = self.get_neighborhood_proto_buff(zone_data.neighborhood_id) if neighborhood_data is None: return for lot_data in neighborhood_data.lots: if zone_data.zone_id == lot_data.zone_instance_id: return lot_data def get_region_id_from_world_id(self, world_id): if world_id in self._world_id_to_region_id_cache: return self._world_id_to_region_id_cache[world_id] def get_world_id_from_zone(self, zone_id): zone_proto = self.get_zone_proto_buff(zone_id) if zone_proto is None: return 0 return zone_proto.world_id def zone_proto_buffs_gen(self): if self._save_game_data_proto is not None: for zone in self._save_game_data_proto.zones: yield zone def get_open_street_proto_buff(self, world_id): if self._save_game_data_proto is not None: for open_street in self._save_game_data_proto.streets: if open_street.world_id == world_id: return open_street def add_open_street_proto_buff(self, open_street_proto): if self._save_game_data_proto is not None: self._save_game_data_proto.streets.append(open_street_proto) def get_lot_id_from_zone_id(self, zone_id): for zone in self._save_game_data_proto.zones: if zone.zone_id == zone_id: return zone.lot_id logger.error('lot id does not exist for zone with id ({}).', zone_id) def get_household_id_from_lot_id(self, lot_id): lot_owner_info = self.get_lot_proto_buff(lot_id) if lot_owner_info is not None: for household in lot_owner_info.lot_owner: return household.household_id def get_household_id_from_zone_id(self, zone_id): zone_data = self.get_zone_proto_buff(zone_id) if zone_data is not None: return zone_data.household_id def resolve_lot_id_into_zone_id(self, lot_id, neighborhood_id=None, ignore_neighborhood_id=False): if neighborhood_id is None: neighborhood_id = services.current_zone().neighborhood_id if self._save_game_data_proto is not None: for zone in self._save_game_data_proto.zones: if zone.lot_id == lot_id: if not ignore_neighborhood_id: if zone.neighborhood_id == neighborhood_id: return zone.zone_id return zone.zone_id def resolve_lot_id_into_zone_and_neighborhood_id(self, lot_id): if self._save_game_data_proto is not None: for zone in self._save_game_data_proto.zones: if zone.lot_id == lot_id: return (zone.zone_id, zone.neighborhood_id) return (None, None) def get_save_slot_proto_guid(self): if self._save_game_data_proto is not None: return self._save_game_data_proto.guid def get_save_slot_proto_buff(self): if self._save_game_data_proto is not None: return self._save_game_data_proto.save_slot def get_account_proto_buff(self): if self._save_game_data_proto is not None: return self._save_game_data_proto.account def add_sim_proto_buff(self, sim_id): sim_data_pb_cache = self._get_sim_data_pb_cache() sim_data_pb_cache[sim_id] = len(self._save_game_data_proto.sims) sim_pb = self._save_game_data_proto.sims.add() return sim_pb def del_sim_proto_buff(self, sim_id): if self._save_game_data_proto is None: return for (index, sim_msg) in enumerate(self._save_game_data_proto.sims): if sim_msg.sim_id == sim_id: del self._save_game_data_proto.sims[index] break else: logger.error('Attempting to delete Sim {} that is absent in the save file.', sim_id) self.dirty_sim_data_pb_cache() def get_sim_proto_buff(self, sim_id): sim_data_pb_cache = self._get_sim_data_pb_cache() index = sim_data_pb_cache.get(sim_id) if index is not None and index < len(self._save_game_data_proto.sims): return self._save_game_data_proto.sims[index] def add_household_proto_buff(self, household_id): household_pb_cache = self._get_household_data_pb_cache() household_pb_cache[household_id] = len(self._save_game_data_proto.households) household_pb = self._save_game_data_proto.households.add() return household_pb def del_household_proto_buff(self, household_id): if self._save_game_data_proto is None: return for (index, household_msg) in enumerate(self._save_game_data_proto.households): if household_msg.household_id == household_id: del self._save_game_data_proto.households[index] break else: logger.error('Attempting to delete Household {} that is absent in the save file.', household_id) self._household_pb_cache = None def get_household_proto_buff(self, household_id): household_pb_cache = self._get_household_data_pb_cache() index = household_pb_cache.get(household_id) if index is not None and index < len(self._save_game_data_proto.households): return self._save_game_data_proto.households[index] def all_household_protos(self): if self._save_game_data_proto is not None: return tuple(self._save_game_data_proto.households) return tuple() def get_neighborhood_proto_buff(self, neighborhood_id): if self._save_game_data_proto is not None: for neighborhood in self._save_game_data_proto.neighborhoods: if neighborhood.neighborhood_id == neighborhood_id: return neighborhood def get_neighborhoods_proto_buf_gen(self): for neighborhood_proto_buf in self._save_game_data_proto.neighborhoods: yield neighborhood_proto_buf def get_neighborhood_proto_buf_from_zone_id(self, zone_id): zone_proto = self.get_zone_proto_buff(zone_id) if zone_proto is None: return neighborhood_proto = self.get_neighborhood_proto_buff(zone_proto.neighborhood_id) return neighborhood_proto def add_mannequin_proto_buff(self): return self._save_game_data_proto.mannequins.add() def get_mannequin_proto_buff(self, mannequin_id): if self._save_game_data_proto is not None: for mannequin_data in self._save_game_data_proto.mannequins: if mannequin_data.mannequin_id == mannequin_id: return mannequin_data def del_mannequin_proto_buff(self, mannequin_id): if self._save_game_data_proto is not None: for (index, mannequin_data) in enumerate(self._save_game_data_proto.mannequins): if mannequin_data.mannequin_id == mannequin_id: del self._save_game_data_proto.mannequins[index] break def prepare_mannequin_for_cas(self, outfit_data): self.del_mannequin_proto_buff(outfit_data.sim_id) sim_info_data_proto = self.add_mannequin_proto_buff() sim_info_data_proto.mannequin_id = outfit_data.sim_id outfit_data.save_sim_info(sim_info_data_proto) current_zone_id = services.current_zone_id() sim_info_data_proto.zone_id = current_zone_id sim_info_data_proto.world_id = self.get_world_id_from_zone(current_zone_id) return sim_info_data_proto def all_travel_group_proto_gen(self): if self._save_game_data_proto is not None: for travel_group in self._save_game_data_proto.travel_groups: yield travel_group def get_travel_group_proto_buff(self, travel_group_id): if self._save_game_data_proto is not None: for travel_group in self._save_game_data_proto.travel_groups: if travel_group.travel_group_id == travel_group_id: return travel_group def add_travel_group_proto_buff(self): return self._save_game_data_proto.travel_groups.add() def del_travel_group_proto_buff(self, travel_group_id): if self._save_game_data_proto is not None: for (index, travel_group) in enumerate(self._save_game_data_proto.travel_groups): if travel_group.travel_group_id == travel_group_id: del self._save_game_data_proto.travel_groups[index] break def try_send_once_per_session_telemetry(self): if not self.once_per_session_telemetry_sent: try: HouseholdRegionTelemetryData.send_household_region_telemetry() except Exception: logger.exception('Exception thrown inside try_send_once_per_session_telemetry()', owner='jwilkinson') finally: self.once_per_session_telemetry_sent = True