def __init__(self, **kwargs): super().__init__( position=TunableVector2( description= '\n Position of this actor in the scene.\n ', default=sims4.math.Vector2.ZERO()), rotation=TunableAngle( description= '\n Angle of this actor in the scene.\n ', default=0), **kwargs)
class _PortalLocation(_PortalLocationBase): FACTORY_TUNABLES = { 'translation': TunableVector2(default=TunableVector2.DEFAULT_ZERO) } def __init__(self, obj, translation, *args, **kwargs): self._translation = translation super().__init__(obj, *args, **kwargs) def get_translation(self, obj): return obj.transform.transform_point( Vector3(self._translation.x, 0, self._translation.y))
class Skill(HasTunableReference, statistics.continuous_statistic_tuning.TunedContinuousStatistic, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager( sims4.resources.Types.STATISTIC)): __qualname__ = 'Skill' SKILL_LEVEL_LIST = TunableMapping( key_type=TunableEnumEntry(SkillLevelType, SkillLevelType.MAJOR), value_type=TunableList( Tunable(int, 0), description= 'The level boundaries for skill type, specified as a delta from the previous value' ), export_modes=ExportModes.All) SKILL_EFFECTIVENESS_GAIN = TunableMapping( key_type=TunableEnumEntry(SkillEffectiveness, SkillEffectiveness.STANDARD), value_type=TunableCurve(), description='Skill gain points based on skill effectiveness.') DYNAMIC_SKILL_INTERVAL = TunableRange( description= '\n Interval used when dynamic loot is used in a\n PeriodicStatisticChangeElement.\n ', tunable_type=float, default=1, minimum=1) INSTANCE_TUNABLES = { 'stat_name': TunableLocalizedString( description= '\n Localized name of this Statistic\n ', export_modes=ExportModes.All), 'ad_data': TunableList( description= '\n A list of Vector2 points that define the desire curve for this\n commodity.\n ', tunable=TunableVector2( description= '\n Point on a Curve\n ', default=sims4.math.Vector2(0, 0))), 'weight': Tunable( description= "\n The weight of the Skill with regards to autonomy. It's ignored \n for the purposes of sorting stats, but it's applied when scoring \n the actual statistic operation for the SI.\n ", tunable_type=float, default=0.5), 'skill_level_type': TunableEnumEntry( description='\n Skill level list to use.\n ', tunable_type=SkillLevelType, default=SkillLevelType.MAJOR, export_modes=ExportModes.All), 'locked_description': TunableLocalizedString( description= "\n The skill description when it's locked.\n ", export_modes=ExportModes.All), 'skill_description': TunableLocalizedString( description= "\n The skill's normal description.\n ", export_modes=ExportModes.All), 'is_default': Tunable( description= '\n Whether Sim will default has this skill.\n ', tunable_type=bool, default=False), 'genders': TunableSet( description= '\n Skill allowed gender, empty set means not specified\n ', tunable=TunableEnumEntry(tunable_type=sim_info_types.Gender, default=None, export_modes=ExportModes.All)), 'ages': TunableSet( description= '\n Skill allowed ages, empty set means not specified\n ', tunable=TunableEnumEntry(tunable_type=sim_info_types.Age, default=None, export_modes=ExportModes.All)), 'entitlement': TunableEntitlement( description= '\n Entitlement required to use this skill.\n ' ), 'icon': TunableResourceKey( description= '\n Icon to be displayed for the Skill.\n ', default='PNG:missing_image', resource_types=sims4.resources.CompoundTypes.IMAGE, export_modes=ExportModes.All), 'tags': TunableList( description= '\n The associated categories of the skill\n ', tunable=TunableEnumEntry(tunable_type=tag.Tag, default=tag.Tag.INVALID)), 'priority': Tunable( description= '\n Skill priority. Higher priority skill will trump other skills when\n being displayed on the UI side. When a sim gains multiple skills at\n the same time only the highest priority one will display a progress\n bar over its head.\n ', tunable_type=int, default=1, export_modes=ExportModes.All), 'statistic_multipliers': TunableMapping( description= '\n Multipliers this skill applies to other statistics based on its\n value.\n ', key_type=TunableReference( description= '\n The statistic this multiplier will be applied to.\n ', manager=services.statistic_manager(), reload_dependent=True), value_type=TunableTuple( curve=TunableCurve( description= '\n Tunable curve where the X-axis defines the skill level, and\n the Y-axis defines the associated multiplier.\n ', x_axis_name='Skill Level', y_axis_name='Multiplier'), direction=TunableEnumEntry( description= "\n Direction where the multiplier should work on the\n statistic. For example, a tuned decrease for an object's\n brokenness rate will not also increase the time it takes to\n repair it.\n ", tunable_type=StatisticChangeDirection, default=StatisticChangeDirection.INCREASE), use_effective_skill=Tunable( description= '\n If checked, this modifier will look at the current\n effective skill value. If unchecked, this modifier will\n look at the actual skill value.\n ', tunable_type=bool, needs_tuning=True, default=True)), tuning_group=GroupNames.MULTIPLIERS), 'success_chance_multipliers': TunableList( description= '\n Multipliers this skill applies to the success chance of\n affordances.\n ', tunable=TunableSkillMultiplier(), tuning_group=GroupNames.MULTIPLIERS), 'monetary_payout_multipliers': TunableList( description= '\n Multipliers this skill applies to the monetary payout amount of\n affordances.\n ', tunable=TunableSkillMultiplier(), tuning_group=GroupNames.MULTIPLIERS), 'next_level_teaser': TunableList( description= '\n Tooltip which describes what the next level entails.\n ', tunable=TunableLocalizedString(), export_modes=(ExportModes.ClientBinary, )), 'level_data': TunableMapping( description= '\n Level-specific information, such as notifications to be displayed to\n level up.\n ', key_type=int, value_type=TunableTuple( level_up_notification=UiDialogNotification.TunableFactory( description= '\n The notification to display when the Sim obtains this level.\n The text will be provided two tokens: the Sim owning the\n skill and a number representing the 1-based skill level\n ', locked_args={ 'text_tokens': DEFAULT, 'icon': None, 'primary_icon_response': UiDialogResponse(text=None, ui_request=UiDialogResponse. UiDialogUiRequest.SHOW_SKILL_PANEL), 'secondary_icon': None }), level_up_screen_slam=OptionalTunable( description= '\n Screen slam to show when reaches this skill level.\n Localization Tokens: Sim - {0.SimFirstName}, Skill Name - \n {1.String}, Skill Number - {2.Number}\n ', tunable=ui.screen_slam.TunableScreenSlamSnippet(), tuning_group=GroupNames.UI))), 'mood_id': TunableReference( description= '\n When this mood is set and active sim matches mood, the UI will \n display a special effect on the skill bar to represent that this \n skill is getting a bonus because of the mood.\n ', manager=services.mood_manager(), export_modes=ExportModes.All), 'stat_asm_param': TunableStatAsmParam.TunableFactory(), 'tutorial': TunableReference( description= '\n Tutorial instance for this skill. This will be used to bring up the \n skill lesson from the first notification for Sim to know this skill.\n ', manager=services.get_instance_manager( sims4.resources.Types.TUTORIAL), class_restrictions=('Tutorial', )), 'skill_unlocks_on_max': TunableList( description= '\n A list of skills that become unlocked when this skill is maxed.\n ', tunable=TunableReference( description= '\n A skill to unlock.\n ', manager=services.get_instance_manager( sims4.resources.Types.STATISTIC), class_restrictions=('Skill', ))) } REMOVE_INSTANCE_TUNABLES = ('min_value_tuning', 'max_value_tuning', 'decay_rate', '_default_convergence_value') def __init__(self, tracker): super().__init__(tracker, self.initial_value) self._delta_enabled = True self._callback_handle = None if self.tracker.owner.is_simulating: self.on_initial_startup() self._max_level_update_sent = False def on_initial_startup(self): if self.tracker.owner.is_selectable: self.refresh_level_up_callback() def on_remove(self, on_destroy=False): super().on_remove(on_destroy=on_destroy) self._destory_callback_handle() def _apply_multipliers_to_continuous_statistics(self): for stat in self.statistic_multipliers: while stat.continuous: owner_stat = self.tracker.get_statistic(stat) if owner_stat is not None: owner_stat._recalculate_modified_decay_rate() @caches.cached def get_user_value(self): return super(Skill, self).get_user_value() def set_value(self, value, *args, from_load=False, interaction=None, **kwargs): old_value = self.get_value() super().set_value(value, *args, **kwargs) self.get_user_value.cache.clear() if not from_load: new_value = self.get_value() new_level = self.convert_to_user_value(value) if old_value == self.initial_value and old_value != new_value: sim_info = self._tracker._owner services.get_event_manager().process_event( test_events.TestEvent.SkillLevelChange, sim_info=sim_info, statistic=self.stat_type) old_level = self.convert_to_user_value(old_value) if old_level < new_level: self._apply_multipliers_to_continuous_statistics() def add_value(self, add_amount, interaction=None, **kwargs): old_value = self.get_value() if old_value == self.initial_value: telemhook = TELEMETRY_HOOK_SKILL_INTERACTION_FIRST_TIME else: telemhook = TELEMETRY_HOOK_SKILL_INTERACTION super().add_value(add_amount, interaction=interaction) self.get_user_value.cache.clear() if interaction is not None: self.on_skill_updated(telemhook, old_value, self.get_value(), interaction.affordance.__name__) def _update_value(self): old_value = self._value if gsi_handlers.sim_handlers_log.skill_change_archiver.enabled: last_update = self._last_update time_delta = super()._update_value() self.get_user_value.cache.clear() new_value = self._value if old_value == self.initial_value: telemhook = TELEMETRY_HOOK_SKILL_INTERACTION_FIRST_TIME self.on_skill_updated(telemhook, old_value, new_value, TELEMETRY_INTERACTION_NOT_AVAILABLE) sim_info = self._tracker._owner services.get_event_manager().process_event( test_events.TestEvent.SkillLevelChange, sim_info=sim_info, statistic=self.stat_type) old_level = self.convert_to_user_value(old_value) new_level = self.convert_to_user_value(new_value) if gsi_handlers.sim_handlers_log.skill_change_archiver.enabled and self.tracker.owner.is_sim: gsi_handlers.sim_handlers_log.archive_skill_change( self.tracker.owner, self, time_delta, old_value, new_value, new_level, last_update) if old_value < new_value and old_level < new_level: if self._tracker is not None: self._tracker.notify_watchers(self.stat_type, self._value, self._value) def on_skill_updated(self, telemhook, old_value, new_value, affordance_name): owner_sim = self._tracker._owner if owner_sim.is_selectable: with telemetry_helper.begin_hook(skill_telemetry_writer, telemhook, sim=owner_sim) as hook: hook.write_guid(TELEMETRY_FIELD_SKILL_ID, self.guid64) hook.write_string(TELEMETRY_FIELD_SKILL_AFFORDANCE, affordance_name) hook.write_bool(TELEMETRY_FIELD_SKILL_AFFORDANCE_SUCCESS, True) hook.write_int(TELEMETRY_FIELD_SKILL_AFFORDANCE_VALUE_ADD, new_value - old_value) if old_value == self.initial_value: skill_level = self.convert_to_user_value(old_value) self._show_level_notification(skill_level) def _destory_callback_handle(self): if self._callback_handle is not None: self.remove_callback(self._callback_handle) self._callback_handle = None def refresh_level_up_callback(self): self._destory_callback_handle() def _on_level_up_callback(stat_inst): new_level = stat_inst.get_user_value() old_level = new_level - 1 stat_inst.on_skill_level_up(old_level, new_level) stat_inst.refresh_level_up_callback() self._callback_handle = self.add_callback( Threshold(self._get_next_level_bound(), operator.ge), _on_level_up_callback) def on_skill_level_up(self, old_level, new_level): tracker = self.tracker sim_info = tracker._owner if self.reached_max_level: for skill in self.skill_unlocks_on_max: skill_instance = tracker.add_statistic(skill, force_add=True) skill_instance.set_value(skill.initial_value) with telemetry_helper.begin_hook(skill_telemetry_writer, TELEMETRY_HOOK_SKILL_LEVEL_UP, sim=sim_info) as hook: hook.write_guid(TELEMETRY_FIELD_SKILL_ID, self.guid64) hook.write_int(TELEMETRY_FIELD_SKILL_LEVEL, new_level) if sim_info.account is not None: services.social_service.post_skill_message(sim_info, self, old_level, new_level) self._show_level_notification(new_level) services.get_event_manager().process_event( test_events.TestEvent.SkillLevelChange, sim_info=sim_info, statistic=self.stat_type) def _show_level_notification(self, skill_level): sim_info = self._tracker._owner if not sim_info.is_npc: level_data = self.level_data.get(skill_level) if level_data is not None: tutorial_id = None if self.tutorial is not None and skill_level == 1: tutorial_id = self.tutorial.guid64 notification = level_data.level_up_notification( sim_info, resolver=SingleSimResolver(sim_info)) notification.show_dialog(icon_override=(self.icon, None), secondary_icon_override=(None, sim_info), additional_tokens=(skill_level, ), tutorial_id=tutorial_id) if level_data.level_up_screen_slam is not None: level_data.level_up_screen_slam.send_screen_slam_message( sim_info, sim_info, self.stat_name, skill_level) @classproperty def skill_type(cls): return cls @classproperty def remove_on_convergence(cls): return False @classmethod def can_add(cls, owner, force_add=False, **kwargs): if force_add: return True if cls.genders and owner.gender not in cls.genders: return False if cls.ages and owner.age not in cls.ages: return False if cls.entitlement is None: return True if owner.is_npc: return False return mtx.has_entitlement(cls.entitlement) @classmethod def get_level_list(cls): return cls.SKILL_LEVEL_LIST.get(cls.skill_level_type) @classmethod def get_max_skill_value(cls): level_list = cls.get_level_list() return sum(level_list) @classmethod def get_skill_value_for_level(cls, level): level_list = cls.get_level_list() if level > len(level_list): logger.error('Level {} out of bounds', level) return 0 return sum(level_list[:level]) @classmethod def get_skill_effectiveness_points_gain(cls, effectiveness_level, level): skill_gain_curve = cls.SKILL_EFFECTIVENESS_GAIN.get( effectiveness_level) if skill_gain_curve is not None: return skill_gain_curve.get(level) logger.error('{} does not exist in SKILL_EFFECTIVENESS_GAIN mapping', effectiveness_level) return 0 @classmethod def _tuning_loaded_callback(cls): super()._tuning_loaded_callback() level_list = cls.get_level_list() cls.max_level = len(level_list) cls.min_value_tuning = 0 cls.max_value_tuning = sum(level_list) cls._default_convergence_value = cls.min_value_tuning cls._build_utility_curve_from_tuning_data(cls.ad_data) for stat in cls.statistic_multipliers: multiplier = cls.statistic_multipliers[stat] curve = multiplier.curve direction = multiplier.direction use_effective_skill = multiplier.use_effective_skill stat.add_skill_based_statistic_multiplier(cls, curve, direction, use_effective_skill) for multiplier in cls.success_chance_multipliers: curve = multiplier.curve use_effective_skill = multiplier.use_effective_skill for affordance in multiplier.affordance_list: affordance.add_skill_multiplier( affordance.success_chance_multipliers, cls, curve, use_effective_skill) for multiplier in cls.monetary_payout_multipliers: curve = multiplier.curve use_effective_skill = multiplier.use_effective_skill for affordance in multiplier.affordance_list: affordance.add_skill_multiplier( affordance.monetary_payout_multipliers, cls, curve, use_effective_skill) @classmethod def _verify_tuning_callback(cls): success_multiplier_affordances = [] for multiplier in cls.success_chance_multipliers: success_multiplier_affordances.extend(multiplier.affordance_list) if len(success_multiplier_affordances) != len( set(success_multiplier_affordances)): logger.error( "The same affordance has been tuned more than once under {}'s success multipliers, and they will overwrite each other. Please fix in tuning.", cls, owner='tastle') monetary_payout_multiplier_affordances = [] for multiplier in cls.monetary_payout_multipliers: monetary_payout_multiplier_affordances.extend( multiplier.affordance_list) if len(monetary_payout_multiplier_affordances) != len( set(monetary_payout_multiplier_affordances)): logger.error( "The same affordance has been tuned more than once under {}'s monetary payout multipliers, and they will overwrite each other. Please fix in tuning.", cls, owner='tastle') @classmethod def convert_to_user_value(cls, value): if not cls.get_level_list(): return 0 current_value = value for (level, level_threshold) in enumerate(cls.get_level_list()): current_value -= level_threshold while current_value < 0: return level return level + 1 @classmethod def convert_from_user_value(cls, user_value): (level_min, _) = cls._get_level_bounds(user_value) return level_min @classmethod def _get_level_bounds(cls, level): level_list = cls.get_level_list() level_min = sum(level_list[:level]) if level < cls.max_level: level_max = sum(level_list[:level + 1]) else: level_max = sum(level_list) return (level_min, level_max) def _get_next_level_bound(self): level = self.convert_to_user_value(self._value) (_, level_max) = self._get_level_bounds(level) return level_max @property def reached_max_level(self): max_value = self.get_max_skill_value() if self.get_value() >= max_value: return True return False @property def should_send_update(self): if not self.reached_max_level: return True if not self._max_level_update_sent: self._max_level_update_sent = True return True return False @classproperty def is_skill(cls): return True @classproperty def autonomy_weight(cls): return cls.weight @classmethod def create_skill_update_msg(cls, sim_id, stat_value): if not cls.convert_to_user_value(stat_value) > 0: return skill_msg = Commodities_pb2.Skill_Update() skill_msg.skill_id = cls.guid64 skill_msg.curr_points = int(stat_value) skill_msg.sim_id = sim_id return skill_msg @property def is_initial_value(self): return self.initial_value == self.get_value() @classproperty def valid_for_stat_testing(cls): return True
class FormationTypeFollow(FormationTypeBase): ATTACH_NODE_COUNT = 3 ATTACH_NODE_RADIUS = 0.25 ATTACH_NODE_ANGLE = math.PI ATTACH_NODE_FLAGS = 4 RAYTRACE_HEIGHT = 1.5 RAYTRACE_RADIUS = 0.1 FACTORY_TUNABLES = { 'formation_offsets': TunableList( description= '\n A list of offsets, relative to the master, that define where slaved\n Sims are positioned.\n ', tunable=TunableVector2(default=Vector2.ZERO()), minlength=1), 'formation_constraints': TunableList( description= '\n A list of constraints that slaved Sims must satisfy any time they\n run interactions while in this formation. This can be a geometric\n constraint, for example, that ensures Sims are always placed within\n a radius or cone of their slaved position.\n ', tunable=TunableConstraintVariant( constraint_locked_args={'multi_surface': True}, circle_locked_args={'require_los': False}, disabled_constraints={'spawn_points', 'relative_circle'})), '_route_length_interval': TunableInterval( description= '\n Sims are slaved in formation only if the route is within this range\n amount, in meters.\n \n Furthermore, routes shorter than the minimum\n will not interrupt behavior (e.g. a socializing Sim will not force\n dogs to get up and move around).\n \n Also routes longer than the maximum will make the slaved sim \n instantly position next to their master\n (e.g. if a leashed dog gets too far from the owner, we place it next to the owner).\n ', tunable_type=float, default_lower=1, default_upper=20, minimum=0), 'fgl_on_routes': TunableTuple( description= '\n Data associated with the FGL Context on following slaves.\n ', slave_should_face_master=Tunable( description= '\n If enabled, the Slave should attempt to face the master at the end\n of routes.\n ', tunable_type=bool, default=False), height_tolerance=OptionalTunable( description= '\n If enabled than we will set the height tolerance in FGL.\n ', tunable=TunableRange( description= '\n The height tolerance piped to FGL.\n ', tunable_type=float, default=0.035, minimum=0, maximum=1))) } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._attachment_chain = [] formation_count = self.master.get_routing_slave_data_count( self._formation_cls) self._formation_offset = self.formation_offsets[formation_count] self._setup_right_angle_connections() self._offset = Vector3.ZERO() for attachment_info in self._attachment_chain: self._offset.x = self._offset.x + attachment_info.parent_offset.x - attachment_info.offset.x self._offset.z = self._offset.z + attachment_info.parent_offset.y - attachment_info.offset.y self._slave_constraint = None self._slave_lock = None self._final_transform = None @classproperty def routing_type(cls): return FormationRoutingType.FOLLOW @property def offset(self): return self._formation_offset @property def slave_attachment_type(self): return Routing_pb2.SlaveData.SLAVE_FOLLOW_ATTACHMENT @staticmethod def get_max_slave_count(tuned_factory): return len(tuned_factory._tuned_values.formation_offsets) @property def route_length_minimum(self): return self._route_length_interval.lower_bound @property def route_length_maximum(self): return self._route_length_interval.upper_bound def attachment_info_gen(self): yield from self._attachment_chain def on_master_route_start(self): self._build_routing_slave_constraint() self._lock_slave() if self._slave.is_sim: for si in self._slave.get_all_running_and_queued_interactions(): if si.transition is not None: if si.transition is not self.master.transition_controller: si.transition.derail(DerailReason.CONSTRAINTS_CHANGED, self._slave) def on_master_route_end(self): self._build_routing_slave_constraint() if self._slave.is_sim: for si in self._slave.get_all_running_and_queued_interactions(): if si.transition is not None: if si.transition is not self.master.transition_controller: si.transition.derail(DerailReason.CONSTRAINTS_CHANGED, self._slave) self._unlock_slave() self._final_transform = None def _lock_slave(self): self._slave_lock = self._slave.add_work_lock(self) def _unlock_slave(self): self._slave.remove_work_lock(self) def _build_routing_slave_constraint(self): self._slave_constraint = ANYWHERE for constraint in self.formation_constraints: constraint = constraint.create_constraint( self._slave, target=self._master, target_position=self._master.intended_position) self._slave_constraint = self._slave_constraint.intersect( constraint) def get_routing_slave_constraint(self): if self._slave_constraint is None or not self._slave_constraint.valid: self._build_routing_slave_constraint() return self._slave_constraint def _add_attachment_node(self, parent_offset: Vector2, offset: Vector2, radius, angle_constraint, flags, node_type): attachment_node = _RoutingFormationAttachmentNode( parent_offset, offset, radius, angle_constraint, flags, node_type) self._attachment_chain.append(attachment_node) def find_good_location_for_slave(self, master_location): restrictions = [] fgl_kwargs = {} fgl_flags = 0 fgl_tuning = self.fgl_on_routes slave_position = master_location.transform.transform_point( self._offset) orientation = master_location.transform.orientation routing_surface = master_location.routing_surface if routing_surface is None: master_parent = master_location.parent if master_parent: routing_surface = master_parent.routing_surface if self.slave.is_sim or isinstance(self.slave, StubActor): (min_water_depth, max_water_depth ) = OceanTuning.make_depth_bounds_safe_for_surface_and_sim( routing_surface, self.slave) else: min_water_depth = None max_water_depth = None (min_water_depth, max_water_depth ) = OceanTuning.make_depth_bounds_safe_for_surface_and_sim( routing_surface, self.master, min_water_depth=min_water_depth, max_water_depth=max_water_depth) fgl_kwargs.update({ 'min_water_depth': min_water_depth, 'max_water_depth': max_water_depth }) if fgl_tuning.height_tolerance is not None: fgl_kwargs['height_tolerance'] = fgl_tuning.height_tolerance if fgl_tuning.slave_should_face_master: restrictions.append( RelativeFacingRange(master_location.transform.translation, 0)) fgl_kwargs.update({ 'raytest_radius': self.RAYTRACE_RADIUS, 'raytest_start_offset': self.RAYTRACE_HEIGHT, 'raytest_end_offset': self.RAYTRACE_HEIGHT, 'ignored_object_ids': {self.master.id, self.slave.id}, 'raytest_start_point_override': master_location.transform.translation }) fgl_flags = FGLSearchFlag.SHOULD_RAYTEST orientation_offset = sims4.math.angle_to_yaw_quaternion( sims4.math.vector3_angle( sims4.math.vector_normalize(self._offset))) orientation = Quaternion.concatenate(orientation, orientation_offset) starting_location = placement.create_starting_location( position=slave_position, orientation=orientation, routing_surface=routing_surface) if self.slave.is_sim: fgl_flags |= FGLSearchFlagsDefaultForSim fgl_context = placement.create_fgl_context_for_sim( starting_location, self.slave, search_flags=fgl_flags, restrictions=restrictions, **fgl_kwargs) else: fgl_flags |= FGLSearchFlagsDefault footprint = self.slave.get_footprint() master_position = master_location.position if hasattr( master_location, 'position') else master_location.transform.translation fgl_context = FindGoodLocationContext( starting_location, object_id=self.slave.id, object_footprints=(footprint, ) if footprint is not None else None, search_flags=fgl_flags, restrictions=restrictions, connectivity_group_override_point=master_position, **fgl_kwargs) (new_position, new_orientation) = placement.find_good_location(fgl_context) if new_position is None or new_orientation is None: logger.warn( 'No good location found for {} after slaved in a routing formation headed to {}.', self.slave, starting_location, owner='rmccord') return sims4.math.Transform( Vector3(*starting_location.position), Quaternion(*starting_location.orientation)) new_position.y = services.terrain_service.terrain_object( ).get_routing_surface_height_at(new_position.x, new_position.z, master_location.routing_surface) final_transform = sims4.math.Transform(new_position, new_orientation) return final_transform def on_release(self): self._unlock_slave() def _setup_right_angle_connections(self): formation_offset_x = Vector2(self._formation_offset.x / 6.0, 0.0) formation_offset_y = Vector2(0.0, self._formation_offset.y) for _ in range(self.ATTACH_NODE_COUNT): self._add_attachment_node( formation_offset_x, formation_offset_x * -1, self.ATTACH_NODE_RADIUS, 0, self.ATTACH_NODE_FLAGS, RoutingFormationFollowType.NODE_TYPE_FOLLOW_LEADER) self._setup_direct_connections(formation_offset_y) def _setup_direct_connections(self, formation_offset): formation_vector_magnitude = formation_offset.magnitude() normalized_offset = formation_offset / formation_vector_magnitude attachment_node_step = formation_vector_magnitude / ( (self.ATTACH_NODE_COUNT - 1) * 2) attachment_vector = normalized_offset * attachment_node_step for i in range(0, self.ATTACH_NODE_COUNT - 1): flags = self.ATTACH_NODE_FLAGS if i == self.ATTACH_NODE_COUNT - 2: flags = 5 self._add_attachment_node( attachment_vector, attachment_vector * -1, self.ATTACH_NODE_RADIUS, self.ATTACH_NODE_ANGLE, flags, RoutingFormationFollowType.NODE_TYPE_CHAIN) def should_slave_for_path(self, path): path_length = path.length() if path is not None else MAX_INT32 final_path_node = path.nodes[-1] final_position = sims4.math.Vector3(*final_path_node.position) final_orientation = sims4.math.Quaternion(*final_path_node.orientation) routing_surface = final_path_node.routing_surface_id final_position.y = services.terrain_service.terrain_object( ).get_routing_surface_height_at(final_position.x, final_position.z, routing_surface) final_transform = sims4.math.Transform(final_position, final_orientation) slave_position = final_transform.transform_point(self._offset) slave_position.y = services.terrain_service.terrain_object( ).get_routing_surface_height_at(slave_position.x, slave_position.z, routing_surface) final_dist_sq = (slave_position - self.slave.position).magnitude_squared() if path_length >= self.route_length_minimum or final_dist_sq >= self.route_length_minimum * self.route_length_minimum: return True return False def build_routing_slave_pb(self, slave_pb, path=None): starting_location = path.final_location if path is not None else self.master.intended_location slave_transform = self.find_good_location_for_slave(starting_location) slave_loc = slave_pb.final_location_override (slave_loc.translation.x, slave_loc.translation.y, slave_loc.translation.z) = slave_transform.translation (slave_loc.orientation.x, slave_loc.orientation.y, slave_loc.orientation.z, slave_loc.orientation.w) = slave_transform.orientation self._final_transform = slave_transform def update_slave_position(self, master_transform, master_orientation, routing_surface, distribute=True, path=None, canceled=False): master_transform = sims4.math.Transform(master_transform.translation, master_orientation) if distribute and not canceled: slave_transform = self._final_transform if self._final_transform is not None else self.slave.transform slave_position = slave_transform.translation else: slave_position = master_transform.transform_point(self._offset) slave_transform = sims4.math.Transform(slave_position, master_orientation) slave_route_distance_sqrd = (self._slave.position - slave_position).magnitude_squared() if path is not None and path.length( ) < self.route_length_minimum and slave_route_distance_sqrd < self.route_length_minimum * self.route_length_minimum: return slave_too_far_from_master = False if slave_route_distance_sqrd > self.route_length_maximum * self.route_length_maximum: slave_too_far_from_master = True if distribute and not slave_too_far_from_master: self._slave.move_to(routing_surface=routing_surface, transform=slave_transform) else: location = self.slave.location.clone( routing_surface=routing_surface, transform=slave_transform) self.slave.set_location_without_distribution(location)
class DebugSetupLotInteraction(TerrainImmediateSuperInteraction): INSTANCE_TUNABLES = { 'setup_lot_destroy_old_objects': Tunable(bool, False, description= 'Destroy objects previously created by this interaction.'), 'setup_lot_objects': TunableList( TunableTuple( definition=TunableReference(definition_manager()), position=TunableVector2(Vector2.ZERO()), angle=TunableRange(int, 0, -360, 360), children=TunableList( TunableTuple( definition=TunableReference( definition_manager(), description= 'The child object to create. It will appear in the first available slot in which it fits, subject to additional restrictions specified in the other values of this tuning.' ), part_index=OptionalTunable( Tunable( int, 0, description= 'If specified, restrict slot selection to the given part index.' )), bone_name=OptionalTunable( Tunable( str, '_ctnm_chr_', description= 'If specified, restrict slot selection to one with this exact bone name.' )), slot_type=OptionalTunable( TunableReference( manager=services.get_instance_manager( Types.SLOT_TYPE), description= 'If specified, restrict slot selection to ones that support this type of slot.' )), init_state_values=TunableList( description= '\n List of states the children object will be set to.\n ', tunable=TunableStateValueReference()))), init_state_values=TunableList( description= '\n List of states the created object will be pushed to.\n ', tunable=TunableStateValueReference()))) } _zone_to_cls_to_created_objects = WeakKeyDictionary() @classproperty def destroy_old_objects(cls): return cls.setup_lot_destroy_old_objects @classproperty def created_objects(cls): created_objects = cls._zone_to_cls_to_created_objects.setdefault( services.current_zone(), {}) return setdefault_callable(created_objects, cls, WeakSet) def _run_interaction_gen(self, timeline): with supress_posture_graph_build(): if self.destroy_old_objects: while self.created_objects: obj = self.created_objects.pop() obj.destroy( source=self, cause='Destroying old objects in setup debug lot.') position = self.context.pick.location self.spawn_objects(position) return True yield def _create_object(self, definition_id, position=Vector3.ZERO(), orientation=Quaternion.IDENTITY(), level=0, owner_id=0): obj = objects.system.create_object(definition_id) if obj is not None: transform = Transform(position, orientation) location = Location(transform, self.context.pick.routing_surface) obj.location = location obj.set_household_owner_id(owner_id) self.created_objects.add(obj) return obj def spawn_objects(self, position): root = sims4.math.Vector3(position.x, position.y, position.z) zone = services.current_zone() lot = zone.lot owner_id = lot.owner_household_id if not self.contained_in_lot(lot, root): closest_point = self.find_nearest_point_on_lot(lot, root) if closest_point is None: return False radius = (self.top_right_pos - self.bottom_left_pos).magnitude_2d() / 2 root = closest_point + sims4.math.vector_normalize( sims4.math.vector_flatten(lot.center) - closest_point) * (radius + 1) if not self.contained_in_lot(lot, root): sims4.log.warn( 'Placement', "Placed the lot objects but the entire bounding box isn't inside the lot. This is ok. If you need them to be inside the lot run the interaction again at a diffrent location." ) def _generate_vector(offset_x, offset_z): ground_obj = services.terrain_service.terrain_object() ret_vector = sims4.math.Vector3(root.x + offset_x, root.y, root.z + offset_z) ret_vector.y = ground_obj.get_height_at(ret_vector.x, ret_vector.z) return ret_vector def _generate_quat(rot): return sims4.math.Quaternion.from_axis_angle( rot, sims4.math.Vector3(0, 1, 0)) for info in self.setup_lot_objects: new_pos = _generate_vector(info.position.x, info.position.y) new_rot = _generate_quat(sims4.math.PI / 180 * info.angle) new_obj = self._create_object(info.definition, new_pos, new_rot, owner_id=owner_id) if new_obj is None: sims4.log.error('SetupLot', 'Unable to create object: {}', info) else: for state_value in info.init_state_values: new_obj.set_state(state_value.state, state_value) for child_info in info.children: slot_owner = new_obj if child_info.part_index is not None: for obj_part in new_obj.parts: if obj_part.subroot_index == child_info.part_index: slot_owner = obj_part break bone_name_hash = None if child_info.bone_name is not None: bone_name_hash = hash32(child_info.bone_name) slot_type = None if child_info.slot_type is not None: slot_type = child_info.slot_type for runtime_slot in slot_owner.get_runtime_slots_gen( slot_types={slot_type}, bone_name_hash=bone_name_hash): if runtime_slot.is_valid_for_placement( definition=child_info.definition): break else: sims4.log.error( 'SetupLot', 'Unable to find slot for child object: {}', child_info) child = self._create_object(child_info.definition, owner_id=owner_id) if child is None: sims4.log.error('SetupLot', 'Unable to create child object: {}', child_info) else: runtime_slot.add_child(child) for state_value in child_info.init_state_values: child.set_state(state_value.state, state_value) def contained_in_lot(self, lot, root): self.find_corner_points(root) return True def find_corner_points(self, root): max_x = 0 min_x = 0 max_z = 0 min_z = 0 for info in self.setup_lot_objects: if info.position.x > max_x: max_x = info.position.x if info.position.x < min_x: min_x = info.position.x if info.position.y > max_z: max_z = info.position.y if info.position.y < min_z: min_z = info.position.y self.top_right_pos = sims4.math.Vector3(root.x + max_x, root.y, root.z + max_z) self.bottom_right_pos = sims4.math.Vector3(root.x + max_x, root.y, root.z + min_z) self.top_left_pos = sims4.math.Vector3(root.x + min_x, root.y, root.z + max_z) self.bottom_left_pos = sims4.math.Vector3(root.x + min_x, root.y, root.z + min_z) def find_nearest_point_on_lot(self, lot, root): lot_corners = lot.corners segments = [(lot_corners[0], lot_corners[1]), (lot_corners[1], lot_corners[2]), (lot_corners[2], lot_corners[3]), (lot_corners[3], lot_corners[1])] dist = 0 closest_point = None for segment in segments: new_point = sims4.math.get_closest_point_2D(segment, root) new_distance = (new_point - root).magnitude() if dist == 0: dist = new_distance closest_point = new_point elif new_distance < dist: dist = new_distance closest_point = new_point return closest_point
class Skill(HasTunableReference, ProgressiveStatisticCallbackMixin, statistics.continuous_statistic_tuning.TunedContinuousStatistic, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(sims4.resources.Types.STATISTIC)): SKILL_LEVEL_LIST = TunableMapping(description='\n A mapping defining the level boundaries for each skill type.\n ', key_type=SkillLevelType, value_type=TunableList(description='\n The level boundaries for skill type, specified as a delta from the\n previous value.\n ', tunable=Tunable(tunable_type=int, default=0)), tuple_name='SkillLevelListMappingTuple', export_modes=ExportModes.All) SKILL_EFFECTIVENESS_GAIN = TunableMapping(description='\n Skill gain points based on skill effectiveness.\n ', key_type=SkillEffectiveness, value_type=TunableCurve()) DYNAMIC_SKILL_INTERVAL = TunableRange(description='\n Interval used when dynamic loot is used in a\n PeriodicStatisticChangeElement.\n ', tunable_type=float, default=1, minimum=1) INSTANCE_TUNABLES = {'stat_name': TunableLocalizedString(description='\n The name of this skill.\n ', export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'skill_description': TunableLocalizedString(description="\n The skill's normal description.\n ", export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'locked_description': TunableLocalizedString(description="\n The skill description when it's locked.\n ", allow_none=True, export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'icon': TunableIcon(description='\n Icon to be displayed for the Skill.\n ', export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'tooltip_icon_list': TunableList(description='\n A list of icons to show in the tooltip of this\n skill.\n ', tunable=TunableIcon(description='\n Icon that is displayed what types of objects help\n improve this skill.\n '), export_modes=(ExportModes.ClientBinary,), tuning_group=GroupNames.UI), 'tutorial': TunableReference(description='\n Tutorial instance for this skill. This will be used to bring up the\n skill lesson from the first notification for Sim to know this skill.\n ', manager=services.get_instance_manager(sims4.resources.Types.TUTORIAL), allow_none=True, class_restrictions=('Tutorial',), tuning_group=GroupNames.UI), 'priority': Tunable(description="\n Skill priority. Higher priority skills will trump other skills when\n being displayed on the UI. When a Sim gains multiple skills at the\n same time, only the highest priority one will display a progress bar\n over the Sim's head.\n ", tunable_type=int, default=1, export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'next_level_teaser': TunableList(description='\n Tooltip which describes what the next level entails.\n ', tunable=TunableLocalizedString(), export_modes=(ExportModes.ClientBinary,), tuning_group=GroupNames.UI), 'mood_id': TunableReference(description='\n When this mood is set and active sim matches mood, the UI will\n display a special effect on the skill bar to represent that this\n skill is getting a bonus because of the mood.\n ', manager=services.mood_manager(), allow_none=True, export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'stat_asm_param': TunableStatAsmParam.TunableFactory(tuning_group=GroupNames.ANIMATION), 'hidden': Tunable(description='\n If checked, this skill will be hidden.\n ', tunable_type=bool, default=False, export_modes=ExportModes.All, tuning_group=GroupNames.AVAILABILITY), 'update_client_for_npcs': Tunable(description="\n Whether this skill will send update messages to the client\n for non-active household sims (NPCs).\n \n e.g. A toddler's communication skill determines the VOX they use, so\n the client needs to know the skill level for all toddlers in order\n for this work properly.\n ", tunable_type=bool, default=False, tuning_group=GroupNames.UI), 'is_default': Tunable(description='\n Whether Sim will default has this skill.\n ', tunable_type=bool, default=False, tuning_group=GroupNames.AVAILABILITY), 'ages': TunableSet(description='\n Allowed ages for this skill.\n ', tunable=TunableEnumEntry(tunable_type=Age, default=Age.ADULT, export_modes=ExportModes.All), tuning_group=GroupNames.AVAILABILITY), 'ad_data': TunableList(description='\n A list of Vector2 points that define the desire curve for this\n commodity.\n ', tunable=TunableVector2(description='\n Point on a Curve\n ', default=sims4.math.Vector2(0, 0)), tuning_group=GroupNames.AUTONOMY), 'weight': Tunable(description="\n The weight of the Skill with regards to autonomy. It's ignored for\n the purposes of sorting stats, but it's applied when scoring the\n actual statistic operation for the SI.\n ", tunable_type=float, default=0.5, tuning_group=GroupNames.AUTONOMY), 'statistic_multipliers': TunableMapping(description='\n Multipliers this skill applies to other statistics based on its\n value.\n ', key_type=TunableReference(description='\n The statistic this multiplier will be applied to.\n ', manager=services.statistic_manager(), reload_dependent=True), value_type=TunableTuple(curve=TunableCurve(description='\n Tunable curve where the X-axis defines the skill level, and\n the Y-axis defines the associated multiplier.\n ', x_axis_name='Skill Level', y_axis_name='Multiplier'), direction=TunableEnumEntry(description="\n Direction where the multiplier should work on the\n statistic. For example, a tuned decrease for an object's\n brokenness rate will not also increase the time it takes to\n repair it.\n ", tunable_type=StatisticChangeDirection, default=StatisticChangeDirection.INCREASE), use_effective_skill=Tunable(description='\n If checked, this modifier will look at the current\n effective skill value. If unchecked, this modifier will\n look at the actual skill value.\n ', tunable_type=bool, needs_tuning=True, default=True)), tuning_group=GroupNames.MULTIPLIERS), 'success_chance_multipliers': TunableList(description='\n Multipliers this skill applies to the success chance of\n affordances.\n ', tunable=TunableSkillMultiplier(), tuning_group=GroupNames.MULTIPLIERS), 'monetary_payout_multipliers': TunableList(description='\n Multipliers this skill applies to the monetary payout amount of\n affordances.\n ', tunable=TunableSkillMultiplier(), tuning_group=GroupNames.MULTIPLIERS), 'tags': TunableList(description='\n The associated categories of the skill\n ', tunable=TunableEnumEntry(tunable_type=tag.Tag, default=tag.Tag.INVALID, pack_safe=True), tuning_group=GroupNames.CORE), 'skill_level_type': TunableEnumEntry(description='\n Skill level list to use.\n ', tunable_type=SkillLevelType, default=SkillLevelType.MAJOR, export_modes=ExportModes.All, tuning_group=GroupNames.CORE), 'level_data': TunableMapping(description='\n Level-specific information, such as notifications to be displayed to\n level up.\n ', key_type=int, value_type=TunableTuple(level_up_notification=UiDialogNotification.TunableFactory(description='\n The notification to display when the Sim obtains this level.\n The text will be provided two tokens: the Sim owning the\n skill and a number representing the 1-based skill level\n ', locked_args={'text_tokens': DEFAULT, 'icon': None, 'primary_icon_response': UiDialogResponse(text=None, ui_request=UiDialogResponse.UiDialogUiRequest.SHOW_SKILL_PANEL), 'secondary_icon': None}), level_up_screen_slam=OptionalTunable(description='\n Screen slam to show when reaches this skill level.\n Localization Tokens: Sim - {0.SimFirstName}, Skill Name - \n {1.String}, Skill Number - {2.Number}\n ', tunable=ui.screen_slam.TunableScreenSlamSnippet()), skill_level_buff=OptionalTunable(tunable=TunableReference(description='\n The buff to place on a Sim when they reach this specific\n level of skill.\n ', manager=services.buff_manager())), rewards=TunableList(description='\n A reward to give for achieving this level.\n ', tunable=rewards.reward_tuning.TunableSpecificReward(pack_safe=True)), loot=TunableList(description='\n A loot to apply for achieving this level.\n ', tunable=TunableReference(manager=services.get_instance_manager(sims4.resources.Types.ACTION), class_restrictions=('LootActions',))), super_affordances=TunableSet(description='\n Super affordances this adds to the Sim.\n ', tunable=TunableReference(description='\n A super affordance added to this Sim.\n ', manager=services.get_instance_manager(sims4.resources.Types.INTERACTION), class_restrictions=('SuperInteraction',), pack_safe=True)), target_super_affordances=TunableProvidedAffordances(description='\n Super affordances this adds to the target.\n ', locked_args={'target': ParticipantType.Object, 'carry_target': ParticipantType.Invalid, 'is_linked': False, 'unlink_if_running': False}), actor_mixers=TunableMapping(description='\n Mixers this adds to an associated actor object. (When targeting\n something else.)\n ', key_type=TunableReference(description='\n The super affordance these mixers are associated with.\n ', manager=services.get_instance_manager(sims4.resources.Types.INTERACTION), class_restrictions=('SuperInteraction',), pack_safe=True), value_type=TunableSet(description='\n Set of mixer affordances associated with the super affordance.\n ', tunable=TunableReference(description='\n Linked mixer affordance.\n ', manager=services.get_instance_manager(sims4.resources.Types.INTERACTION), category='asm', class_restrictions=('MixerInteraction',), pack_safe=True)))), tuning_group=GroupNames.CORE), 'age_up_skill_transition_data': OptionalTunable(description='\n Data used to modify the value of a new skill based on the level\n of this skill.\n \n e.g. Toddler Communication skill transfers into Child Social skill.\n ', tunable=TunableTuple(new_skill=TunablePackSafeReference(description='\n The new skill.\n ', manager=services.get_instance_manager(sims4.resources.Types.STATISTIC)), skill_data=TunableMapping(description="\n A mapping between this skill's levels and the\n new skill's internal value.\n \n The keys are user facing skill levels.\n \n The values are the internal statistic value, not the user\n facing skill level.\n ", key_type=Tunable(description="\n This skill's level.\n \n This is the actual user facing skill level.\n ", tunable_type=int, default=0), value_type=Tunable(description='\n The new skill\'s value.\n \n This is the internal statistic\n value, not the user facing skill level."\n ', tunable_type=int, default=0))), tuning_group=GroupNames.SPECIAL_CASES), 'skill_unlocks_on_max': TunableList(description='\n A list of skills that become unlocked when this skill is maxed.\n ', tunable=TunableReference(description='\n A skill to unlock.\n ', manager=services.get_instance_manager(sims4.resources.Types.STATISTIC), class_restrictions=('Skill',), pack_safe=True), tuning_group=GroupNames.SPECIAL_CASES), 'trend_tag': OptionalTunable(description='\n If enabled, we associate this skill with a particular trend via tag\n which you can find in trend_tuning.\n ', tunable=TunableTag(description='\n The trend tag we associate with this skill\n ', filter_prefixes=('func_trend',)))} REMOVE_INSTANCE_TUNABLES = ('min_value_tuning', 'max_value_tuning', 'decay_rate', '_default_convergence_value') def __init__(self, tracker): self._skill_level_buff = None super().__init__(tracker, self.initial_value) self._delta_enabled = True self._max_level_update_sent = False @classmethod def _tuning_loaded_callback(cls): super()._tuning_loaded_callback() level_list = cls.get_level_list() cls.max_level = len(level_list) cls.min_value_tuning = 0 cls.max_value_tuning = sum(level_list) cls._default_convergence_value = cls.min_value_tuning cls._build_utility_curve_from_tuning_data(cls.ad_data) for stat in cls.statistic_multipliers: multiplier = cls.statistic_multipliers[stat] curve = multiplier.curve direction = multiplier.direction use_effective_skill = multiplier.use_effective_skill stat.add_skill_based_statistic_multiplier(cls, curve, direction, use_effective_skill) for multiplier in cls.success_chance_multipliers: curve = multiplier.curve use_effective_skill = multiplier.use_effective_skill for affordance in multiplier.affordance_list: affordance.add_skill_multiplier(affordance.success_chance_multipliers, cls, curve, use_effective_skill) for multiplier in cls.monetary_payout_multipliers: curve = multiplier.curve use_effective_skill = multiplier.use_effective_skill for affordance in multiplier.affordance_list: affordance.add_skill_multiplier(affordance.monetary_payout_multipliers, cls, curve, use_effective_skill) @classmethod def _verify_tuning_callback(cls): success_multiplier_affordances = [] for multiplier in cls.success_chance_multipliers: success_multiplier_affordances.extend(multiplier.affordance_list) if len(success_multiplier_affordances) != len(set(success_multiplier_affordances)): logger.error("The same affordance has been tuned more than once under {}'s success multipliers, and they will overwrite each other. Please fix in tuning.", cls, owner='tastle') monetary_payout_multiplier_affordances = [] for multiplier in cls.monetary_payout_multipliers: monetary_payout_multiplier_affordances.extend(multiplier.affordance_list) if len(monetary_payout_multiplier_affordances) != len(set(monetary_payout_multiplier_affordances)): logger.error("The same affordance has been tuned more than once under {}'s monetary payout multipliers, and they will overwrite each other. Please fix in tuning.", cls, owner='tastle') @classproperty def skill_type(cls): return cls @constproperty def is_skill(): return True @classproperty def autonomy_weight(cls): return cls.weight @constproperty def remove_on_convergence(): return False @classproperty def valid_for_stat_testing(cls): return True @classmethod def can_add(cls, owner, force_add=False, **kwargs): if force_add: return True if owner.age not in cls.ages: return False return super().can_add(owner, **kwargs) @classmethod def convert_to_user_value(cls, value): level_list = cls.get_level_list() if not level_list: return 0 current_value = value for (level, level_threshold) in enumerate(level_list): current_value -= level_threshold if current_value < 0: return level return level + 1 @classmethod def convert_from_user_value(cls, user_value): (level_min, _) = cls._get_level_bounds(user_value) return level_min @classmethod def create_skill_update_msg(cls, sim_id, stat_value): skill_msg = Commodities_pb2.Skill_Update() skill_msg.skill_id = cls.guid64 skill_msg.curr_points = int(stat_value) skill_msg.sim_id = sim_id return skill_msg @classmethod def get_level_list(cls): return cls.SKILL_LEVEL_LIST.get(cls.skill_level_type) @classmethod def get_skill_effectiveness_points_gain(cls, effectiveness_level, level): skill_gain_curve = cls.SKILL_EFFECTIVENESS_GAIN.get(effectiveness_level) if skill_gain_curve is not None: return skill_gain_curve.get(level) logger.error('{} does not exist in SKILL_EFFECTIVENESS_GAIN mapping', effectiveness_level) return 0 def _get_level_data_for_skill_level(self, skill_level): level_data = self.level_data.get(skill_level) if level_data is None: logger.debug('No level data found for skill [{}] at level [{}].', self, skill_level) return level_data @property def is_initial_value(self): return self.initial_value == self.get_value() def should_send_update(self, sim_info, stat_value): if sim_info.is_npc and not self.update_client_for_npcs: return False if self.hidden: return False if Skill.convert_to_user_value(stat_value) == 0: return False if self.reached_max_level: if self._max_level_update_sent: return False self._max_level_update_sent = True return True def on_initial_startup(self): super().on_initial_startup() skill_level = self.get_user_value() self._update_skill_level_buff(skill_level) def on_add(self): super().on_add() self._tracker.owner.add_modifiers_for_skill(self) level_data = self._get_level_data_for_skill_level(self.get_user_value()) if level_data is not None: provided_affordances = [] for provided_affordance in level_data.target_super_affordances: provided_affordance_data = ProvidedAffordanceData(provided_affordance.affordance, provided_affordance.object_filter, provided_affordance.allow_self) provided_affordances.append(provided_affordance_data) self._tracker.add_to_affordance_caches(level_data.super_affordances, provided_affordances) self._tracker.add_to_actor_mixer_cache(level_data.actor_mixers) sim = self._tracker._owner.get_sim_instance() apply_super_affordance_commodity_flags(sim, self, level_data.super_affordances) def on_remove(self, on_destroy=False): super().on_remove(on_destroy=on_destroy) self._destory_callback_handle() if not on_destroy: self._send_skill_delete_message() if self._skill_level_buff is not None: self._tracker.owner.remove_buff(self._skill_level_buff) self._skill_level_buff = None if not on_destroy: self._tracker.update_affordance_caches() sim = self._tracker._owner.get_sim_instance() remove_super_affordance_commodity_flags(sim, self) def on_zone_load(self): self._max_level_update_sent = False def _apply_multipliers_to_continuous_statistics(self): for stat in self.statistic_multipliers: if stat.continuous: owner_stat = self.tracker.get_statistic(stat) if owner_stat is not None: owner_stat._recalculate_modified_decay_rate() @classproperty def default_value(cls): return cls.initial_value @flexmethod @caches.cached def get_user_value(cls, inst): inst_or_cls = inst if inst is not None else cls return super(__class__, inst_or_cls).get_user_value() def _clear_user_value_cache(self): self.get_user_value.func.cache.clear() def set_value(self, value, *args, from_load=False, interaction=None, **kwargs): old_value = self.get_value() super().set_value(value, *args, **kwargs) if not caches.skip_cache: self._clear_user_value_cache() if from_load: return event_manager = services.get_event_manager() sim_info = self._tracker._owner new_value = self.get_value() new_level = self.convert_to_user_value(value) if old_value == self.initial_value or old_value != new_value: event_manager.process_event(test_events.TestEvent.SkillValueChange, sim_info=sim_info, skill=self, statistic=self.stat_type, custom_keys=(self.stat_type,)) old_level = self.convert_to_user_value(old_value) if old_level < new_level or old_value == self.initial_value: self._apply_multipliers_to_continuous_statistics() event_manager.process_event(test_events.TestEvent.SkillLevelChange, sim_info=sim_info, skill=self, new_level=new_level, custom_keys=(self.stat_type,)) def add_value(self, add_amount, interaction=None, **kwargs): old_value = self.get_value() if old_value == self.initial_value: telemhook = TELEMETRY_HOOK_SKILL_INTERACTION_FIRST_TIME else: telemhook = TELEMETRY_HOOK_SKILL_INTERACTION super().add_value(add_amount, interaction=interaction) if not caches.skip_cache: self._clear_user_value_cache() if interaction is not None: interaction_name = interaction.affordance.__name__ else: interaction_name = TELEMETRY_INTERACTION_NOT_AVAILABLE self.on_skill_updated(telemhook, old_value, self.get_value(), interaction_name) def _update_value(self): old_value = self._value if gsi_handlers.sim_handlers_log.skill_change_archiver.enabled: last_update = self._last_update time_delta = super()._update_value() if not caches.skip_cache: self._clear_user_value_cache() new_value = self._value if old_value < new_value: event_manager = services.get_event_manager() sim_info = self._tracker._owner if self._tracker is not None else None if old_value == self.initial_value: telemhook = TELEMETRY_HOOK_SKILL_INTERACTION_FIRST_TIME self.on_skill_updated(telemhook, old_value, new_value, TELEMETRY_INTERACTION_NOT_AVAILABLE) event_manager.process_event(test_events.TestEvent.SkillValueChange, sim_info=sim_info, skill=self, statistic=self.stat_type, custom_keys=(self.stat_type,)) old_level = self.convert_to_user_value(old_value) new_level = self.convert_to_user_value(new_value) if gsi_handlers.sim_handlers_log.skill_change_archiver.enabled and self.tracker.owner.is_sim: gsi_handlers.sim_handlers_log.archive_skill_change(self.tracker.owner, self, time_delta, old_value, new_value, new_level, last_update) if old_level < new_level or old_value == self.initial_value: if self._tracker is not None: self._tracker.notify_watchers(self.stat_type, self._value, self._value) event_manager.process_event(test_events.TestEvent.SkillLevelChange, sim_info=sim_info, skill=self, new_level=new_level, custom_keys=(self.stat_type,)) def _on_statistic_modifier_changed(self, notify_watcher=True): super()._on_statistic_modifier_changed(notify_watcher=notify_watcher) if not self.reached_max_level: return event_manager = services.get_event_manager() sim_info = self._tracker._owner if self._tracker is not None else None event_manager.process_event(test_events.TestEvent.SkillValueChange, sim_info=sim_info, skill=self, statistic=self.stat_type, custom_keys=(self.stat_type,)) def on_skill_updated(self, telemhook, old_value, new_value, affordance_name): owner_sim_info = self._tracker._owner if owner_sim_info.is_selectable: with telemetry_helper.begin_hook(skill_telemetry_writer, telemhook, sim_info=owner_sim_info) as hook: hook.write_guid(TELEMETRY_FIELD_SKILL_ID, self.guid64) hook.write_string(TELEMETRY_FIELD_SKILL_AFFORDANCE, affordance_name) hook.write_bool(TELEMETRY_FIELD_SKILL_AFFORDANCE_SUCCESS, True) hook.write_int(TELEMETRY_FIELD_SKILL_AFFORDANCE_VALUE_ADD, new_value - old_value) if old_value == self.initial_value: skill_level = self.convert_to_user_value(old_value) self._handle_skill_up(skill_level) def _send_skill_delete_message(self): if self.tracker.owner.is_npc: return skill_msg = Commodities_pb2.SkillDelete() skill_msg.skill_id = self.guid64 op = GenericProtocolBufferOp(Operation.SIM_SKILL_DELETE, skill_msg) Distributor.instance().add_op(self.tracker.owner, op) @staticmethod def _callback_handler(stat_inst): new_level = stat_inst.get_user_value() old_level = new_level - 1 stat_inst.on_skill_level_up(old_level, new_level) stat_inst.refresh_threshold_callback() def _handle_skill_up(self, skill_level): self._show_level_notification(skill_level) self._update_skill_level_buff(skill_level) self._try_give_skill_up_payout(skill_level) self._tracker.update_affordance_caches() sim = self._tracker._owner.get_sim_instance() remove_super_affordance_commodity_flags(sim, self) super_affordances = tuple(self._tracker.get_cached_super_affordances_gen()) apply_super_affordance_commodity_flags(sim, self, super_affordances) def _recalculate_modified_decay_rate(self): pass def refresh_level_up_callback(self): self._destory_callback_handle() def _on_level_up_callback(stat_inst): new_level = stat_inst.get_user_value() old_level = new_level - 1 stat_inst.on_skill_level_up(old_level, new_level) stat_inst.refresh_level_up_callback() self._callback_handle = self.create_and_add_callback_listener(Threshold(self._get_next_level_bound(), operator.ge), _on_level_up_callback) def on_skill_level_up(self, old_level, new_level): tracker = self.tracker sim_info = tracker._owner if self.reached_max_level: for skill in self.skill_unlocks_on_max: skill_instance = tracker.add_statistic(skill, force_add=True) skill_instance.set_value(skill.initial_value) with telemetry_helper.begin_hook(skill_telemetry_writer, TELEMETRY_HOOK_SKILL_LEVEL_UP, sim_info=sim_info) as hook: hook.write_guid(TELEMETRY_FIELD_SKILL_ID, self.guid64) hook.write_int(TELEMETRY_FIELD_SKILL_LEVEL, new_level) self._handle_skill_up(new_level) services.get_event_manager().process_event(test_events.TestEvent.SkillValueChange, sim_info=sim_info, statistic=self.stat_type, custom_keys=(self.stat_type,)) def _show_level_notification(self, skill_level, ignore_npc_check=False): sim_info = self._tracker._owner if not (ignore_npc_check or not sim_info.is_npc): if skill_level == 1: tutorial_service = services.get_tutorial_service() if tutorial_service is not None and tutorial_service.is_tutorial_running(): return level_data = self._get_level_data_for_skill_level(skill_level) if level_data is not None: tutorial_id = None if self.tutorial is not None: if skill_level == 1: tutorial_id = self.tutorial.guid64 notification = level_data.level_up_notification(sim_info, resolver=SingleSimResolver(sim_info)) notification.show_dialog(icon_override=IconInfoData(icon_resource=self.icon), secondary_icon_override=IconInfoData(obj_instance=sim_info), additional_tokens=(skill_level,), tutorial_id=tutorial_id) if level_data.level_up_screen_slam is not None: level_data.level_up_screen_slam.send_screen_slam_message(sim_info, sim_info, self.stat_name, skill_level) def _update_skill_level_buff(self, skill_level): level_data = self._get_level_data_for_skill_level(skill_level) new_buff = level_data.skill_level_buff if level_data is not None else None if self._skill_level_buff is not None: self._tracker.owner.remove_buff(self._skill_level_buff) self._skill_level_buff = None if new_buff is not None: self._skill_level_buff = self._tracker.owner.add_buff(new_buff) def _try_give_skill_up_payout(self, skill_level): level_data = self._get_level_data_for_skill_level(skill_level) if level_data is None: return if level_data.rewards: for reward in level_data.rewards: reward().open_reward(self._tracker.owner, reward_destination=RewardDestination.SIM, reward_source=self) if level_data.loot: resolver = SingleSimResolver(self._tracker.owner) for loot in level_data.loot: loot.apply_to_resolver(resolver) def force_show_level_notification(self, skill_level): self._show_level_notification(skill_level, ignore_npc_check=True) @classmethod def send_commodity_update_message(cls, sim_info, old_value, new_value): stat_instance = sim_info.get_statistic(cls.stat_type, add=False) if stat_instance is None or not stat_instance.should_send_update(sim_info, new_value): return msg = cls.create_skill_update_msg(sim_info.id, new_value) add_object_message(sim_info, MSG_SIM_SKILL_UPDATE, msg, False) change_rate = stat_instance.get_change_rate() hide_progress_bar = False if sim_info.is_npc or sim_info.is_skill_bar_suppressed(): hide_progress_bar = True op = distributor.ops.SkillProgressUpdate(cls.guid64, change_rate, new_value, hide_progress_bar) distributor.ops.record(sim_info, op) def save_statistic(self, commodities, skills, ranked_stats, tracker): current_value = self.get_saved_value() if current_value == self.initial_value: return message = protocols.Skill() message.name_hash = self.guid64 message.value = current_value if self._time_of_last_value_change: message.time_of_last_value_change = self._time_of_last_value_change.absolute_ticks() skills.append(message) def unlocks_skills_on_max(self): return True def can_decay(self): return False def get_skill_provided_affordances(self): level_data = self._get_level_data_for_skill_level(self.get_user_value()) if level_data is None: return ((), ()) return (level_data.super_affordances, level_data.target_super_affordances) def get_skill_provided_actor_mixers(self): level_data = self._get_level_data_for_skill_level(self.get_user_value()) if level_data is None: return return level_data.actor_mixers def get_actor_mixers(self, super_interaction): level_data = self._get_level_data_for_skill_level(self.get_user_value()) if level_data is None: return [] mixers = level_data.actor_mixers.get(super_interaction, tuple()) if level_data is not None else [] return mixers @flexmethod def populate_localization_token(cls, inst, token): inst_or_cls = inst if inst is not None else cls token.type = LocalizedStringToken.STRING token.text_string = inst_or_cls.stat_name
class _PartData(HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = {'part_definition': ObjectPart.TunableReference(description='\n The part definition associated with this part instance.\n \n The part definition defines supported postures and interactions,\n disallowed buffs and portal data.\n ', pack_safe=True), 'disabling_states': TunableList(description='\n A list of state values which, if active on this object, will\n disable this part.\n ', tunable=TunableStateValueReference(pack_safe=True)), 'adjacent_parts': TunableList(description='\n The parts that are adjacent to this part. You must reference a part\n that is tuned in this mapping.\n \n An empty list indicates that no part is adjacent to this part.\n ', tunable=Tunable(tunable_type=str, default=None), unique_entries=True), 'overlapping_parts': TunableList(description='\n The parts that are unusable when this part is in use. You must\n reference a part that is tuned in this mapping.\n ', tunable=Tunable(tunable_type=str, default=None), unique_entries=True), 'subroot_index': OptionalTunable(description='\n If enabled, this part will have a subroot index associated with it.\n This will affect the way Sims animate, i.e. they will animate\n relative to the position of the part, not relative to the object.\n ', tunable=Tunable(description='\n The subroot suffix associated with this part.\n ', tunable_type=int, default=0, needs_tuning=False), enabled_by_default=True), 'anim_overrides': TunableAnimationOverrides(description='\n Animation overrides for this part.\n '), 'is_mirrored': OptionalTunable(description='\n Specify whether or not solo animations played on this part\n should be mirrored or not.\n ', tunable=Tunable(description='\n If checked, mirroring is enabled. If unchecked,\n mirroring is disabled.\n ', tunable_type=bool, default=False)), 'forward_direction_for_picking': TunableVector2(description="\n When you click on the object this part belongs to, this offset will\n be applied to this part when determining which part is closest to\n where you clicked.\n \n By default, the object's forward vector will be used. It should only\n be necessary to tune this value if multiple parts overlap at the\n same location (e.g. the single bed).\n ", default=TunableVector2.DEFAULT_Z, x_axis_name='x', y_axis_name='z'), 'disable_sim_aop_forwarding': Tunable(description='\n If checked, Sims using this specific part will never forward\n AOPs.\n ', tunable_type=bool, default=False), 'disable_child_aop_forwarding': Tunable(description='\n If checked, objects parented to this specific part will\n never forward AOPs.\n ', tunable_type=bool, default=False), 'restrict_autonomy_preference': Tunable(description='\n If checked, this specific part can be used for use only autonomy preference\n restriction.\n ', tunable_type=bool, default=False), 'name': OptionalTunable(description='\n Name of this part. For use if the part name needs to be surfaced\n to the player. (i.e. when assigning sim to specific side of bed.)\n ', tunable=TunableLocalizedString())}
def __init__(self, description='Generate a privacy region for this object', callback=None, **kwargs): super().__init__(tests=TunableTestSet(description='\n Any Sim who passes these tests will be allowed to violate the\n privacy region.\n '), shoo_exempt_tests=TunableTestSet(description='\n Any violator who passes these tests will still be considered a\n violator, but ill be exempt from being shooed.\n i.e. A cat will get shooed when it breaks a privacy region, but\n cats will ignore the shoo behavior.\n '), max_line_of_sight_radius=Tunable(description='\n The maximum possible distance from this object than an\n interaction can reach.\n ', tunable_type=float, default=5), map_divisions=Tunable(description='\n The number of points around the object to check collision from.\n More points means higher accuracy.\n ', tunable_type=int, default=30), simplification_ratio=Tunable(description='\n A factor determining how much to combine edges in the line of\n sight polygon.\n ', tunable_type=float, default=0.25), boundary_epsilon=Tunable(description='\n The LOS origin is allowed to be outside of the boundary by this\n amount.\n ', tunable_type=float, default=0.01), facing_offset=Tunable(description='\n The LOS origin is offset from the object origin by this amount\n (mainly to avoid intersecting walls).\n ', tunable_type=float, default=0.1), routing_surface_only=Tunable(description="\n If this is checked, then the privacy constraint is generated on\n the surface defined by the interaction's target. If the\n interaction has no target or the target does not provide a\n routable surface, no privacy is generated.\n \n Furthermore, privacy that is exclusive to routing surface will\n only shoo Sims that are on the routable surface.\n \n e.g. A Sim cleaning a counter needs to shoo cats on the counter.\n \n The default behavior is for privacy to be generated on the\n surface the Sim is on, and for it to apply to Sims on all\n surfaces.\n \n e.g. A Sim using the toilet would shoo cats within the privacy\n region that happen to be on routable surfaces, such as counters.\n ", tunable_type=bool, default=False), shoo_constraint_radius=OptionalTunable(description='\n If enabled, you can tune a specific radius for the shoo\n constraint. If disabled, the values tuned in the Privacy module\n tuning will be used.\n ', tunable=Tunable(description='\n The radius of the constraint a Shooed Sim will attempt to\n route to.\n ', tunable_type=float, default=2.5), disabled_name=self.OPTIONAL_TUNABLE_DISABLED_NAME), unavailable_tooltip=OptionalTunable(description='\n If enabled, allows a custom tool tip to be displayed when the\n player tries to run an interaction on an object inside the\n privacy region. If disabled, the values tuned in the Privacy\n module tuning will be used.\n ', tunable=TunableLocalizedStringFactory(description='\n Tool tip displayed when an object is not accessible due to\n being inside a privacy region.\n '), disabled_name=self.OPTIONAL_TUNABLE_DISABLED_NAME), embarrassed_affordance=OptionalTunable(description='\n If enabled, a specific affordance can be tuned for a Sim to\n play when walking into the privacy region. If disabled, the\n values tuned in the Privacy module tuning will be used.\n ', tunable=TunableReference(description='\n The affordance a Sim will play when getting embarrassed by\n walking in on a privacy situation.\n ', manager=services.get_instance_manager(Types.INTERACTION)), disabled_name=self.OPTIONAL_TUNABLE_DISABLED_NAME), post_route_affordance=OptionalTunable(description='\n Optionally define an interaction that will run after the Sim\n routes away.\n ', tunable=TunableReference(description='\n The affordance a Sim will play when getting embarrassed by\n walking in on a privacy situation.\n ', manager=services.get_instance_manager(Types.INTERACTION))), privacy_cost_override=OptionalTunable(description='\n If set, override the cost of the privacy region.\n ', tunable=TunableRange(tunable_type=int, default=20, minimum=1), disabled_name=self.OPTIONAL_TUNABLE_DISABLED_NAME), additional_exit_offsets=TunableList(description="\n If set, adds additional exit goals to add to the satisfy shoo\n constraint. For most cases this isn't needed, since most\n privacy situations may kick a player out of a room through\n a door and there are few exit options. \n However for open-space privacy areas, default behavior\n (using zone's corners) can cause a Sim to always attempt to exit \n the privacy area in a consistent and often not optimal route, \n (e.g. an open cross-shaped hall with 4 ways out, with default\n behavior the Sim could consistently choose to exit using \n the same route even though other routes would yield \n a shorter distance out of the privacy region)\n ", tunable=TunableVector2(default=TunableVector2.DEFAULT_ZERO)), reserved_surface_space=OptionalTunable(description='\n If enabled privacy will generate an additional footprint around\n the target object surface (if routing_surface_only is enabled\n then this will happen on the object routable surface). \n This footprint will affect any Sim from routing through for the \n duration of the interaction.\n ', tunable=TunableTuple(description='\n Reserved space and blocking options for the created\n footprint.\n ', allow_routing=Tunable(description='\n If True, then the footprint will only discourage \n routing, instead of blocking the whole area from\n being used.\n ', tunable_type=bool, default=True), reserved_space=TunableReservedSpace(description='\n Defined space to generate the Jig that will block the \n routable surface space..\n ')), enabled_name='define_blocking_area'), vehicle_tests=OptionalTunable(description='\n If enabled, vehicles that pass through this privacy region will\n be tested to see if the vehicle is allowed in the privacy\n region. Otherwise, the vehicle will always be affected by\n privacy.\n Note: The Object Routing Component specifies what happens when\n the drone enters a privacy region.\n ', tunable=TunableTestSet(description='\n The tests that the vehicle must pass to be allowed in the\n privacy region. \n Note: The Object Routing Component specifies what happens\n when the drone enters a privacy region.\n ')), privacy_violators=TunableEnumFlags(description='\n Defines violators of privacy: currently, only SIM and VEHICLES are suppported.\n for example: if PrivacyViolators.SIM is false , but PrivacyViolators.VEHICLES is enabled\n sims need not obey privacy rules for the object at hand.\n ', enum_type=PrivacyViolators, allow_no_flags=True, default=PrivacyViolators.SIM | PrivacyViolators.VEHICLES), verify_tunable_callback=TunablePrivacy.verify_tunable_callback, description=description, **kwargs)
class RelationshipTrack(TunedContinuousStatistic, HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.statistic_manager()): __qualname__ = 'RelationshipTrack' FRIENDSHIP_TRACK = TunableReference( description= '\n A reference to the friendship track so that the client knows which\n track is the friendship one.\n ', manager=services.statistic_manager(), class_restrictions='RelationshipTrack', export_modes=sims4.tuning.tunable_base.ExportModes.All) FRIENDSHIP_TRACK_FILTER_THRESHOLD = Tunable( description= '\n Value that the client will use when filtering friendship on the Sim\n Picker. Sims that have a track value equal to or above this value will\n be shown with the friendship filter.\n ', tunable_type=int, default=0, export_modes=sims4.tuning.tunable_base.ExportModes.All) ROMANCE_TRACK = TunableReference( description= '\n A reference to the romance track so that the client knows which\n track is the romance one.\n ', manager=services.statistic_manager(), class_restrictions='RelationshipTrack', export_modes=sims4.tuning.tunable_base.ExportModes.All) ROMANCE_TRACK_FILTER_THRESHOLD = Tunable( description= '\n Value that the client will use when filtering romance on the Sim\n Picker. Sims that have a track value equal to or above this value will\n be shown with the romance filter.\n ', tunable_type=int, default=0, export_modes=sims4.tuning.tunable_base.ExportModes.All) ROMANCE_TRACK_FILTER_BITS = TunableSet( description= '\n A set of relationship bits that will be used in the Sim Picker for\n filtering based on romance. If a Sim has any of these bits then they\n will be displayed in the Sim Picker when filtering for romance.\n ', tunable=TunableReference( description= '\n A specific bit used for filtering romance in the Sim Picker.\n ', manager=services.get_instance_manager( sims4.resources.Types.RELATIONSHIP_BIT)), export_modes=sims4.tuning.tunable_base.ExportModes.All) REMOVE_INSTANCE_TUNABLES = ('stat_asm_param', 'persisted_tuning') INSTANCE_TUNABLES = { 'bit_data_tuning': TunableVariant(bit_set=TunableRelationshipBitData(), _2dMatrix=TunableRelationshipTrack2dLink()), 'ad_data': TunableList( TunableVector2(sims4.math.Vector2(0, 0), description='Point on a Curve'), description= 'A list of Vector2 points that define the desire curve for this relationship track.' ), 'relationship_obj_prefence_curve': TunableWeightedUtilityCurveAndWeight( description= "A curve that maps desire of a sim to interact with an object made by a sim of this relation to this relationship's actual score." ), '_add_bit_on_threshold': OptionalTunable( description= '\n If enabled, the referenced bit will be added this track reaches the threshold.\n ', tunable=TunableTuple( description= '\n The bit & threshold pair.\n ', bit=TunableReference( description= '\n The bit to add.\n ', manager=services.get_instance_manager( sims4.resources.Types.RELATIONSHIP_BIT)), threshold=TunableThreshold( description= '\n The threshold at which to add this bit.\n ' ))), 'display_priority': TunableRange( description= '\n The display priority of this relationship track. Tracks with a\n display priority greater than zero will be displayed in\n ascending order in the UI. So a relationship track with a\n display priority of 1 will show above a relationship track\n with a display priority of 2. Relationship tracks with the\n same display priority will show up in potentially\n non-deterministic ways. Relationship tracks with display\n priorities of 0 will not be shown.\n ', tunable_type=int, default=0, minimum=0, export_modes=sims4.tuning.tunable_base.ExportModes.All), 'display_popup_priority': TunableRange( description= '\n The display popup priority. This is the priority that the\n relationship score increases will display. If there are\n multiple relationship changes at the same time.\n ', tunable_type=int, default=0, minimum=0, export_modes=sims4.tuning.tunable_base.ExportModes.All), '_neutral_bit': TunableReference( description= "\n The neutral bit for this relationship track. This is the bit\n that is displayed when there are holes in the relationship\n track's bit data.\n ", manager=services.get_instance_manager( sims4.resources.Types.RELATIONSHIP_BIT)), 'decay_only_affects_selectable_sims': Tunable( description= '\n If this is True, the decay is only enabled if one or both of \n the sims in the relationship are selectable. \n ', tunable_type=bool, default=False), 'delay_until_decay_is_applied': OptionalTunable( description= '\n If enabled, the decay for this track will be disabled whenever\n the value changes by any means other than decay. It will then \n be re-enabled after this amount of time (in sim minutes) passes.\n ', tunable=TunableRange( description= '\n The amount of time, in sim minutes, that it takes before \n decay is enabled.\n ', tunable_type=int, default=10, minimum=1)), 'causes_delayed_removal_on_convergence': Tunable( description= '\n If True, this track may cause the relationship to get culled when \n it reaches convergence. This is not guaranteed, based on the \n culling rules. Sim relationships will NOT be culled if any of \n the folling conditions are met:\n - The sim has any relationship bits that are tuned to prevent this.\n - The Sims are in the same household\n ', tunable_type=bool, default=False), 'visible_test_set': OptionalTunable(event_testing.tests.TunableTestSet( description= '\n If set , tests whether relationship should be sent to client.\n If no test given, then as soon as track is added to relationship\n it will be visible to client.\n ' ), disabled_value=DEFAULT, disabled_name='always_visible', enabled_name='run_test') } bit_data = None def __init__(self, tracker): super().__init__(tracker, self.initial_value) self._per_instance_data = self.bit_data.get_track_instance_data(self) self.visible_to_client = True if self.visible_test_set is DEFAULT else False self._decay_alarm_handle = None self._convergence_callback_data = None self._first_same_sex_relationship_callback_data = None self._set_initial_decay() @classproperty def is_short_term_context(cls): return False def on_add(self): if not self.tracker.suppress_callback_setup_during_load: self._per_instance_data.setup_callbacks() self.update_instance_data() if self._add_bit_on_threshold is not None: self.add_callback(self._add_bit_on_threshold.threshold, self._on_add_bit_from_threshold_callback) if self._should_initialize_first_same_sex_relationship_callback(): self._first_same_sex_relationship_callback_data = self.add_callback( Threshold( sims.global_gender_preference_tuning. GlobalGenderPreferenceTuning. ENABLE_AUTOGENERATION_SAME_SEX_PREFERENCE_THRESHOLD, operator.ge), self._first_same_sex_relationship_callback) def on_remove(self, on_destroy=False): self.remove_callback(self._first_same_sex_relationship_callback_data) super().on_remove(on_destroy=on_destroy) self._destroy_decay_alarm() def get_statistic_multiplier_increase(self): sim_info_manager = services.sim_info_manager() target_sim_info = sim_info_manager.get( self.tracker.relationship.target_sim_id) target_sim_multiplier = 1 if target_sim_info is not None: target_sim_stat = target_sim_info.relationship_tracker.get_relationship_track( self.tracker.relationship.sim_id, track=self.stat_type) if target_sim_stat is not None: target_sim_multiplier = target_sim_stat._statistic_multiplier_increase return self._statistic_multiplier_increase * target_sim_multiplier def get_statistic_multiplier_decrease(self): sim_info_manager = services.sim_info_manager() target_sim_info = sim_info_manager.get( self.tracker.relationship.target_sim_id) target_sim_multiplier = 1 if target_sim_info is not None: target_sim_stat = target_sim_info.relationship_tracker.get_relationship_track( self.tracker.relationship.sim_id, track=self.stat_type) if target_sim_stat is not None: target_sim_multiplier = target_sim_stat._statistic_multiplier_decrease return self._statistic_multiplier_decrease * target_sim_multiplier def set_value(self, value, *args, **kwargs): self._update_value() old_value = self._value delta = value - old_value sim_info = self.tracker.relationship.find_sim_info() self.tracker.trigger_test_event( sim_info, event_testing.test_events.TestEvent.PrerelationshipChanged) super().set_value(value, *args, **kwargs) self._update_visiblity() self._reset_decay_alarm() self.tracker.relationship.send_relationship_info(deltas={self: delta}) self.tracker.trigger_test_event( sim_info, event_testing.test_events.TestEvent.RelationshipChanged) @property def is_visible(self): return self.visible_to_client def fixup_callbacks_during_load(self): super().fixup_callbacks_during_load() self._per_instance_data.setup_callbacks() def update_instance_data(self): self._per_instance_data.request_full_update() def apply_social_group_decay(self): pass def remove_social_group_decay(self): pass def _on_statistic_modifier_changed(self, notify_watcher=True): super()._on_statistic_modifier_changed(notify_watcher=notify_watcher) if self._statistic_modifier == 0: self._reset_decay_alarm() self.tracker.relationship.send_relationship_info() def _update_visiblity(self): if not self.visible_to_client: sim_info_manager = services.sim_info_manager() actor_sim_info = sim_info_manager.get( self.tracker.relationship.sim_id) if actor_sim_info is None: return target_sim_info = sim_info_manager.get( self.tracker.relationship.target_sim_id) if target_sim_info is None: return resolver = DoubleSimResolver(actor_sim_info, target_sim_info) self.visible_to_client = True if self.visible_test_set.run_tests( resolver) else False @classmethod def _tuning_loaded_callback(cls): super()._tuning_loaded_callback() cls.bit_data = cls.bit_data_tuning() cls.bit_data.build_track_data() cls._build_utility_curve_from_tuning_data(cls.ad_data) @classmethod def _verify_tuning_callback(cls): if cls._neutral_bit is None: logger.error('No Neutral Bit tuned for Relationship Track: {}', cls) @staticmethod def check_relationship_track_display_priorities(statistic_manager): if not __debug__: return relationship_track_display_priority = collections.defaultdict(list) for statistic in statistic_manager.types.values(): if not issubclass(statistic, RelationshipTrack): pass if statistic.display_priority == 0: pass relationship_track_display_priority[ statistic.display_priority].append(statistic) for relationship_priority_level in relationship_track_display_priority.values( ): if len(relationship_priority_level) <= 1: pass logger.warn( 'Multiple Relationship Tracks have the same display priority: {}', relationship_priority_level) @classmethod def type_id(cls): return cls.guid64 @classmethod def get_bit_track_node_for_bit(cls, relationship_bit): for node in cls.bit_data.bit_track_node_gen(): while node.bit is relationship_bit: return node @classmethod def bit_track_node_gen(cls): for node in cls.bit_data.bit_track_node_gen(): yield node @classmethod def get_bit_at_relationship_value(cls, value): for bit_node in reversed(tuple(cls.bit_track_node_gen())): while bit_node.min_rel <= value <= bit_node.max_rel: return bit_node.bit or cls._neutral_bit return cls._neutral_bit @classproperty def persisted(cls): return True def get_active_bit(self): return self._per_instance_data.get_active_bit() def get_bit_for_client(self): active_bit = self.get_active_bit() if active_bit is None: return self._neutral_bit return active_bit def _set_initial_decay(self): if self._should_decay(): self.decay_enabled = True self._convergence_callback_data = self.add_callback( Threshold(self.convergence_value, operator.eq), self._on_convergence_callback) logger.debug('Setting decay on track {} to {} for {}', self, self.decay_enabled, self.tracker.relationship) def _should_decay(self): if self.decay_rate == 0: return False if self.decay_only_affects_selectable_sims: sim_info = self.tracker.relationship.find_sim_info() target_sim_info = self.tracker.relationship.find_target_sim_info() if sim_info is None or target_sim_info is None: return False if sim_info.is_selectable or target_sim_info.is_selectable: return True else: return True return False def _reset_decay_alarm(self): self._destroy_decay_alarm() if self._should_decay( ) and self.delay_until_decay_is_applied is not None: logger.debug('Resetting decay alarm for track {} for {}.', self, self.tracker.relationship) delay_time_span = date_and_time.create_time_span( minutes=self.delay_until_decay_is_applied) self._decay_alarm_handle = alarms.add_alarm( self, delay_time_span, self._decay_alarm_callback) self.decay_enabled = False def _decay_alarm_callback(self, handle): logger.debug('Decay alarm triggered on track {} for {}.', self, self.tracker.relationship) self._destroy_decay_alarm() self.decay_enabled = True def _destroy_decay_alarm(self): if self._decay_alarm_handle is not None: alarms.cancel_alarm(self._decay_alarm_handle) self._decay_alarm_handle = None def _on_convergence_callback(self, _): logger.debug( 'Track {} reached convergence; rel might get culled for {}', self, self.tracker.relationship) self.tracker.relationship.track_reached_convergence(self) def _on_add_bit_from_threshold_callback(self, _): logger.debug('Track {} is adding its extra bit: {}'.format( self, self._add_bit_on_threshold.bit)) self.tracker.relationship.add_bit(self._add_bit_on_threshold.bit) def _should_initialize_first_same_sex_relationship_callback(self): if self.stat_type is not self.ROMANCE_TRACK: return False if sims.global_gender_preference_tuning.GlobalGenderPreferenceTuning.enable_autogeneration_same_sex_preference: return False sim_info_a = self.tracker.relationship.find_sim_info() sim_info_b = self.tracker.relationship.find_target_sim_info() if sim_info_a is None or sim_info_b is None: return False if sim_info_a.gender is not sim_info_b.gender: return False if sim_info_a.is_npc and sim_info_b.is_npc: return False return True def _first_same_sex_relationship_callback(self, _): sims.global_gender_preference_tuning.GlobalGenderPreferenceTuning.enable_autogeneration_same_sex_preference = True self.remove_callback(self._first_same_sex_relationship_callback_data)
class BaseRelationshipTrack: INSTANCE_TUNABLES = { 'bit_data_tuning': TunableVariant( description= '\n Bit tuning for all the bits that compose this relationship \n track.\n The structure tuned here, either 2d or simple track should include \n bits for all the possible range of the track.\n ', bit_set=TunableRelationshipBitData(), _2dMatrix=TunableRelationshipTrack2dLink()), '_neutral_bit': TunableReference( description= "\n The neutral bit for this relationship track. This is the bit\n that is displayed when there are holes in the relationship\n track's bit data.\n ", manager=services.get_instance_manager( sims4.resources.Types.RELATIONSHIP_BIT), tuning_group=GroupNames.CORE), 'ad_data': TunableList( description= '\n A list of Vector2 points that define the desire curve for this \n relationship track.\n ', tunable=TunableVector2( description= '\n Point on a Curve.\n ', default=sims4.math.Vector2(0, 0)), tuning_group=GroupNames.SPECIAL_CASES), '_add_bit_on_threshold': OptionalTunable( description= '\n If enabled, the referenced bit will be added this track reaches the\n threshold.\n ', tunable=TunableTuple( description= '\n The bit & threshold pair.\n ', bit=TunableReference( description= '\n The bit to add.\n ', manager=services.get_instance_manager( sims4.resources.Types.RELATIONSHIP_BIT)), threshold=TunableThreshold( description= '\n The threshold at which to add this bit.\n ' )), tuning_group=GroupNames.CORE), 'causes_delayed_removal_on_convergence': Tunable( description= '\n If True, this track may cause the relationship to get culled\n when it reaches convergence. This is not guaranteed, based on\n the culling rules. Sim relationships will NOT be culled if any\n of the folling conditions are met: \n - Sim has any relationship bits that are tuned to prevent this. \n - The sims are in the same household\n \n Note: This value is ignored by the Relationship Culling Story\n Progression Action.\n ', tunable_type=bool, default=False, tuning_group=GroupNames.CORE), 'tested_initial_modifier': OptionalTunable( description= '\n If enabled, a modifier will be applied to the initial value when\n the track is created.\n ', tunable=TestedSum.TunableFactory( description= '\n The test to run and the outcome if test passes.\n ' ), tuning_group=GroupNames.CORE), 'visible_test_set': OptionalTunable(event_testing.tests.TunableTestSet( description= '\n If set, tests whether relationship should be sent to client. If\n no test given, then as soon as track is added to the\n relationship, it will be visible to client.\n ' ), disabled_value=DEFAULT, disabled_name='always_visible', enabled_name='run_test', tuning_group=GroupNames.SPECIAL_CASES), 'delay_until_decay_is_applied': OptionalTunable( description= '\n If enabled, the decay for this track will be disabled whenever\n the value changes by any means other than decay. It will then \n be re-enabled after this amount of time (in sim minutes) passes.\n ', tunable=TunableRange( description= '\n The amount of time, in sim minutes, that it takes before \n decay is enabled.\n ', tunable_type=int, default=10, minimum=1), tuning_group=GroupNames.DECAY), 'display_priority': TunableRange( description= '\n The display priority of this relationship track. Tracks with a\n display priority greater than zero will be displayed in ascending\n order in the UI.\n \n So a relationship track with a display priority of 1 will show\n above a relationship track with a display priority of 2.\n Relationship tracks with the same display priority will show up\n in potentially non-deterministic ways. Relationship tracks\n with display priorities of 0 will not be shown.\n ', tunable_type=int, default=0, minimum=0, tuning_group=GroupNames.UI), 'headline': OptionalTunable( description= '\n If enabled when this relationship track updates we will display\n a headline update to the UI.\n ', tunable=TunableReference( description= '\n The headline that we want to send down.\n ', manager=services.get_instance_manager( sims4.resources.Types.HEADLINE)), tuning_group=GroupNames.UI), 'display_popup_priority': TunableRange( description= '\n The display popup priority. This is the priority that the\n relationship score increases will display if there are multiple\n relationship changes at the same time.\n ', tunable_type=int, default=0, minimum=0, tuning_group=GroupNames.UI), 'persist_at_convergence': Tunable( description= '\n If unchecked, this track will not be persisted if it is at\n convergence. This prevents a ton of tracks, in particular short\n term context tracks, from piling up on relationships with a value\n of 0.\n \n If checked, the track will be persisted even if it is at 0. This\n should only used on tracks where its presence matters.\n ', tunable_type=bool, default=False, tuning_group=GroupNames.SPECIAL_CASES) } def __init__(self, tracker): super().__init__(tracker, self.initial_value) self._per_instance_data = self.bit_data.get_track_instance_data(self) self.visible_to_client = True if self.visible_test_set is DEFAULT else False self._decay_alarm_handle = None self._cached_ticks_until_decay_begins = -1 self._convergence_callback_data = None self._set_initial_decay() if not self._tracker.suppress_callback_setup_during_load: self._create_convergence_callback() def on_add(self): if not self.tracker.suppress_callback_setup_during_load: self._per_instance_data.setup_callbacks() (old_bit, new_bit) = self.update_instance_data() if not self.tracker.load_in_progress: sim_id_a = self.tracker.rel_data.relationship.sim_id_a sim_id_b = self.tracker.rel_data.relationship.sim_id_b if old_bit is not None and old_bit is not new_bit: self.tracker.rel_data.relationship.remove_bit( sim_id_a, sim_id_b, old_bit) if new_bit is not None and not self.tracker.rel_data.relationship.has_bit( sim_id_a, new_bit): self.tracker.rel_data.relationship.add_relationship_bit( sim_id_a, sim_id_b, new_bit) if self._add_bit_on_threshold is not None: self.create_and_add_callback_listener( self._add_bit_on_threshold.threshold, self._on_add_bit_from_threshold_callback) def _set_initial_decay(self): if self._should_decay(): self.decay_enabled = True def _should_decay(self): if self.decay_rate == 0: return False if self.tracker.is_track_locked(self): return False if self.tracker.rel_data.relationship.is_object_rel(): return True if self.decay_only_affects_played_sims: if not services.sim_info_manager(): return False sim_info = self.tracker.rel_data.relationship.find_sim_info_a() target_sim_info = self.tracker.rel_data.relationship.find_sim_info_b( ) if sim_info is None or target_sim_info is None: return False active_household = services.active_household() if active_household is None: return False if sim_info in active_household or target_sim_info in active_household: return True if sim_info.is_player_sim or target_sim_info.is_player_sim: if not self.tracker.rel_data.relationship.can_cull_relationship( consider_convergence=False): return False current_value = self.get_value() if self.decay_affecting_played_sims.range_decay_threshold.lower_bound < current_value < self.decay_affecting_played_sims.range_decay_threshold.upper_bound: return True else: return True return False def _create_convergence_callback(self): if self._convergence_callback_data is None: self._convergence_callback_data = self.create_and_add_callback_listener( Threshold(self.convergence_value, operator.eq), self._on_convergence_callback) else: logger.error( 'Track {} attempted to create convergence callback twice.'. format(self)) def _on_convergence_callback(self, _): logger.debug( 'Track {} reached convergence; rel might get culled for {}', self, self.tracker.rel_data) self.tracker.rel_data.track_reached_convergence(self) @classmethod def _tuning_loaded_callback(cls): super()._tuning_loaded_callback() cls.bit_data = cls.bit_data_tuning() cls.bit_data.build_track_data() cls._build_utility_curve_from_tuning_data(cls.ad_data) def fixup_callbacks_during_load(self): self._create_convergence_callback() super().fixup_callbacks_during_load() self._per_instance_data.setup_callbacks() def update_instance_data(self): return self._per_instance_data.request_full_update() def reset_decay_alarm(self, use_cached_time=False): self._destroy_decay_alarm() if self.delay_until_decay_is_applied is not None: if self._should_decay(): delay_time_span = None if use_cached_time: if self._cached_ticks_until_decay_begins > 0: delay_time_span = date_and_time.TimeSpan( self._cached_ticks_until_decay_begins) elif self._cached_ticks_until_decay_begins == 0: self.decay_enabled = True return if delay_time_span is None: delay_time_span = date_and_time.create_time_span( minutes=self.delay_until_decay_is_applied) self._decay_alarm_handle = alarms.add_alarm( self, delay_time_span, self._decay_alarm_callback, cross_zone=True) self.decay_enabled = False def get_bit_for_client(self): active_bit = self.get_active_bit() if active_bit is None: return self._neutral_bit return active_bit def get_active_bit(self): return self._per_instance_data.get_active_bit() def _destroy_decay_alarm(self): if self._decay_alarm_handle is not None: alarms.cancel_alarm(self._decay_alarm_handle) self._decay_alarm_handle = None def get_saved_ticks_until_decay_begins(self): if self.decay_enabled: return 0 if self._decay_alarm_handle: return self._decay_alarm_handle.get_remaining_time().in_ticks() return self._cached_ticks_until_decay_begins def set_time_until_decay_begins(self, ticks_until_decay_begins): if self.delay_until_decay_is_applied is None: self._cached_ticks_until_decay_begins = ticks_until_decay_begins if self._cached_ticks_until_decay_begins != 0.0 and self._cached_ticks_until_decay_begins != -1.0: logger.error('Rel Track {} loaded with bad persisted value {}', self, self._cached_ticks_until_decay_begins) return max_tuning = date_and_time.create_time_span( minutes=self.delay_until_decay_is_applied).in_ticks() self._cached_ticks_until_decay_begins = min(ticks_until_decay_begins, max_tuning) if self._cached_ticks_until_decay_begins < -1.0: logger.error('Rel Track {} loaded with bad persisted value {}', self, self._cached_ticks_until_decay_begins) def update_track_index(self, relationship): self._per_instance_data.full_load_update(relationship)
class Street(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.street_manager()): WORLD_DESCRIPTION_TUNING_MAP = TunableMapping(description='\n A mapping between Catalog world description and street tuning instance.\n This way we can find out what world description the current zone\n belongs to at runtime then grab its street tuning instance.\n ', key_type=TunableWorldDescription(description='\n Catalog-side World Description.\n ', pack_safe=True), value_type=TunableReference(description="\n Street Tuning instance. This is retrieved at runtime based on what\n the active zone's world description is.\n ", pack_safe=True, manager=services.street_manager()), key_name='WorldDescription', value_name='Street') INSTANCE_TUNABLES = {'open_street_director': TunablePackSafeReference(description='\n The Scheduling Open Street Director to use for this world file.\n This open street director will be able to load object layers and\n spin up situations.\n ', manager=services.get_instance_manager(sims4.resources.Types.OPEN_STREET_DIRECTOR)), 'travel_lot': OptionalTunable(description='\n If enabled then this street will have a specific lot that it will\n want to travel to when we travel to this "street."\n ', tunable=TunableLotDescription(description='\n The specific lot that we will travel to when asked to travel to\n this street.\n ')), 'townie_demographics': TunableTuple(description='\n Townie population demographics for the street.\n ', target_population=OptionalTunable(description='\n If enabled, Sims created for other purposes will passively be\n assigned to live on this street, gaining the filter features.\n Sims are assigned out in round robin fashion up until all\n streets have reached their target, after which those streets\n will be assigned Sims in round robin fashion past their target.\n \n If disabled, this street will not passively be assigned townies\n unless the Lives On Street filter explicitly requires the\n Sim to be on the street.\n ', tunable=TunableRange(description="\n The ideal number of townies that live on the street.\n \n 0 is valid if you don't want Sims to live on this street\n while other streets haven't met their target population.\n ", tunable_type=int, default=1, minimum=0)), filter_features=TunableList(description='\n Sims created as townies living on this street, they will gain\n one set of features in this list. Features are applied as\n Sim creation tags and additional filter terms to use.\n ', tunable=TunableTuple(description='\n ', filter_terms=TunableList(description='\n Filter terms to inject into the filter.\n ', tunable=FilterTermVariant(conform_optional=True)), sim_creator_tags=TunableReference(description="\n Tags to inject into the filter's Sim template.\n ", manager=services.get_instance_manager(sims4.resources.Types.TAG_SET), allow_none=True, class_restrictions=('TunableTagSet',)), sim_name_type=TunableEnumEntry(description='\n What type of name the sim should have.\n ', tunable_type=SimNameType, default=SimNameType.DEFAULT), weight=TunableRange(description='\n Weighted chance.\n ', tunable_type=float, default=1, minimum=0)))), 'valid_conditional_layers': TunableSet(description='\n A list of all of the conditional_layers on this Street.\n ', tunable=TunableReference(description='\n A reference to a conditional layer that exists on this Street.\n ', manager=services.get_instance_manager(sims4.resources.Types.CONDITIONAL_LAYER), pack_safe=True)), 'tested_conditional_layers': TunableList(description='\n The list of conditional layers that will load in to the the street\n if its tests pass.\n \n NOTE: Only a subset of tests are registered to listen for updates.\n Check with your GPE if you expect to have the conditional layers update\n with test events. Otherwise, they will only be tested on zone-load.\n ', tunable=TunableTuple(description='\n A list of all of the conditional_layers to load into this Street.\n ', conditional_layer=TunableReference(description='\n A reference to a conditional layer that exists on this Street.\n ', manager=services.get_instance_manager(sims4.resources.Types.CONDITIONAL_LAYER), pack_safe=True), tests=TunableGlobalTestList(description='\n The tests that must pass in order for this conditional layer\n to be loaded in. If tests are empty, the conditional layer\n is considered valid.\n '), process_after_event_handled=Tunable(description='\n When True, conditional layer requests triggered by the test\n events will be ordered to prioritize client-layer requests\n first, then gameplay-layers.\n ', tunable_type=bool, default=False), test_on_managed_world_edit_mode_load=Tunable(description='\n By default, conditional layers are not tested and started when entering\n from Managed World Edit Mode. Those layers which are enabled via this\n option will be tested and started even in Managed World Edit Mode.\n Generally these should be Client Only layers, but no such restriction is\n enforced.\n ', tunable_type=bool, default=False))), 'beaches': TunableList(description='\n List of locations to place beaches.\n ', tunable=TunableTuple(description='\n Beach creation data.\n ', position=TunableVector3(description='\n The position to create the beach at.\n ', default=TunableVector3.DEFAULT_ZERO), forward=TunableVector2(description='\n The forward vector of the beach object.\n ', default=Vector2.Y_AXIS())), unique_entries=True), 'civic_policy': StreetProvider.TunableFactory(description='\n Tuning to control the civic policy voting and enactment process for\n a street.\n '), 'initial_street_eco_footprint_override': OptionalTunable(description='\n If enabled, overrides the initial value of the street eco footprint\n statistic.\n ', tunable=Tunable(description='\n The initial value of the street eco footprint statistic.\n ', tunable_type=float, default=0))} ZONE_IDS_BY_STREET = None street_to_lot_id_to_zone_ids = {} @classmethod def _cls_repr(cls): return "Street: <class '{}.{}'>".format(cls.__module__, cls.__name__) @classmethod def get_lot_to_travel_to(cls): if cls is services.current_street(): return import world.lot return world.lot.get_lot_id_from_instance_id(cls.travel_lot) @classmethod def has_conditional_layer(cls, conditional_layer): return conditional_layer in cls.valid_conditional_layers @classmethod def clear_caches(cls): Street.street_to_lot_id_to_zone_ids.clear() Street.ZONE_IDS_BY_STREET = None
class ScriptObject(BaseObject, HasStatisticComponent, HasFootprintComponent, metaclass=HashedTunedInstanceMetaclass, manager=services.definition_manager()): __qualname__ = 'ScriptObject' INSTANCE_TUNABLES = { '_super_affordances': TunableList( description= '\n Super affordances on this object.\n ', tunable=TunableReference( description= '\n A super affordance on this object.\n ', manager=services.affordance_manager(), class_restrictions=('SuperInteraction', ), pack_safe=True)), '_part_data': TunableList( description= '\n Use this to define parts for an object. Parts allow multiple Sims to\n use an object in different or same ways, at the same time. The model\n and the animations for this object will have to support parts.\n Ensure this is the case with animation and modeling.\n \n There will be one entry in this list for every part the object has.\n \n e.g. The bed has six parts (two sleep parts, and four sit parts).\n add two entries for the sleep parts add four entries for the sit\n parts\n ', tunable=TunableTuple( description= '\n Data that is specific to this part.\n ', part_definition=TunableReference( description= '\n The part definition data.\n ', manager=services.object_part_manager()), subroot_index=OptionalTunable( description= '\n If enabled, this part will have a subroot index associated\n with it. This will affect the way Sims animate, i.e. they\n will animate relative to the position of the part, not\n relative to the object.\n ', tunable=Tunable( description= '\n The subroot index/suffix associated to this part.\n ', tunable_type=int, default=0, needs_tuning=False), enabled_by_default=True), overlapping_parts=TunableList( description= "\n The indices of parts that are unusable when this part is in\n use. The index is the zero-based position of the part within\n the object's Part Data list.\n ", tunable=int), adjacent_parts=OptionalTunable( description= '\n Define adjacent parts. If disabled, adjacent parts will be\n generated automatically based on indexing. If enabled,\n adjacent parts must be specified here.\n ', tunable=TunableList( description= "\n The indices of parts that are adjacent to this part. The\n index is the zero-based position of the part within the\n object's Part Data list.\n \n An empty list indicates that no part is ajdacent to this\n part.\n ", tunable=int)), is_mirrored=OptionalTunable( description= '\n Specify whether or not solo animations played on this part\n should be mirrored or not.\n ', tunable=Tunable( description= '\n If checked, mirroring is enabled. If unchecked,\n mirroring is disabled.\n ', tunable_type=bool, default=False)), forward_direction_for_picking=TunableVector2( description= "\n When you click on the object this part belongs to, this\n offset will be applied to this part when determining which\n part is closest to where you clicked. By default, the\n object's forward vector will be used. It should only be\n necessary to tune this value if multiple parts overlap at\n the same location (e.g. the single bed).\n ", default=sims4.math.Vector2(0, 1), x_axis_name='x', y_axis_name='z'), disable_sim_aop_forwarding=Tunable( description= '\n If checked, Sims using this specific part will never forward\n AOPs.\n ', tunable_type=bool, default=False), disable_child_aop_forwarding=Tunable( description= '\n If checked, objects parented to this specific part will\n never forward AOPs.\n ', tunable_type=bool, default=False), anim_overrides=TunableAnimationOverrides( description='Animation overrides for this part.'))), 'custom_posture_target_name': Tunable( description= '\n An additional non-virtual actor to set for this object when used as\n a posture target.\n \n This tunable is used when the object has parts. In most cases, the\n state machines will only have one actor for the part that is\n involved in animation. In that case, this field should not be set.\n \n e.g. The Sit posture requires the sitTemplate actor to be set, but\n does not make a distinction between, for instance, Chairs and Sofas,\n because no animation ever involves the whole object.\n \n However, there may be cases when, although we are dealing with\n parts, the animation will need to also reference the entire object.\n In that case, the ASM will have an extra actor to account for the\n whole object, in addition to the part. Set this field to be that\n actor name.\n \n e.g. The Sleep posture on the bed animates the Sim on one part.\n However, the sheets and pillows need to animate on the entire bed.\n In that case, we need to set this field on Bed so that the state\n machine can have this actor set.\n ', tunable_type=str, default=None), 'posture_transition_target_tag': TunableEnumEntry( description= '\n A tag to apply to this script object so that it is taken into\n account for posture transition preference scoring. For example, you\n could tune this object (and others) to be a DINING_SURFACE. Any SI\n that is set up to have posture preference scoring can override the\n score for any objects that are tagged with DINING_SURFACE.\n \n For a more detailed description of how posture preference scoring\n works, see the posture_target_preference tunable field description\n in SuperInteraction.\n ', tunable_type=postures.PostureTransitionTargetPreferenceTag, default=postures.PostureTransitionTargetPreferenceTag.INVALID), '_anim_overrides': OptionalTunable( description= '\n If enabled, specify animation overrides for this object.\n ', tunable=TunableAnimationObjectOverrides()), '_focus_score': TunableEnumEntry( description= '\n Determines how likely a Sim is to look at this object when focusing\n ambiently. A higher value means this object is more likely to draw\n Sim focus.\n ', tunable_type=FocusInterestLevel, default=FocusInterestLevel.LOW, needs_tuning=True), 'social_clustering': OptionalTunable( description= '\n If enabled, specify how this objects affects clustering for\n preferred locations for socialization.\n ', tunable=TunableTuple(is_datapoint=Tunable( description= '\n Whether or not this object is a data point for social\n clusters.\n ', tunable_type=bool, default=True))), '_should_search_forwarded_sim_aop': Tunable( description= "\n If enabled, interactions on Sims using this object will appear in\n this object's pie menu as long as they are also tuned to allow\n forwarding.\n ", tunable_type=bool, default=False), '_should_search_forwarded_child_aop': Tunable( description= "\n If enabled, interactions on children of this object will appear in\n this object's pie menu as long as they are also tuned to allow\n forwarding.\n ", tunable_type=bool, default=False), '_disable_child_footprint_and_shadow': Tunable( description= "\n If checked, all objects parented to this object will have their\n footprints and dropshadows disabled.\n \n Example Use: object_sim has this checked so when a Sim picks up a\n plate of food, the plate's footprint and dropshadow turn off\n temporarily.\n ", tunable_type=bool, default=False), 'disable_los_reference_point': Tunable( description= '\n If checked, goal points for this interaction will not be discarded\n if a ray-test from the object fails to connect without intersecting\n walls or other objects. The reason for allowing this, is for\n objects like the door where we want to allow the sim to interact\n with the object, but since the object doesnt have a footprint we\n want to allow him to use the central point as a reference point and\n not fail the LOS test.\n ', tunable_type=bool, default=False), '_components': TunableTuple( description= '\n The components that instances of this object should have.\n ', tuning_group=GroupNames.COMPONENTS, affordance_tuning=OptionalTunable( AffordanceTuningComponent.TunableFactory()), autonomy=OptionalTunable(TunableAutonomyComponent()), canvas=OptionalTunable(CanvasComponent.TunableFactory()), carryable=OptionalTunable(TunableCarryableComponent()), censor_grid=OptionalTunable(TunableCensorGridComponent()), collectable=OptionalTunable(CollectableComponent.TunableFactory()), consumable=OptionalTunable(ConsumableComponent.TunableFactory()), crafting_station=OptionalTunable( CraftingStationComponent.TunableFactory()), fishing_location=OptionalTunable( FishingLocationComponent.TunableFactory()), flowing_puddle=OptionalTunable( FlowingPuddleComponent.TunableFactory()), game=OptionalTunable(TunableGameComponent()), gardening_component=TunableGardeningComponent(), idle_component=OptionalTunable(IdleComponent.TunableFactory()), inventory=OptionalTunable( ObjectInventoryComponent.TunableFactory()), inventory_item=OptionalTunable( InventoryItemComponent.TunableFactory()), lighting=OptionalTunable(LightingComponent.TunableFactory()), line_of_sight=OptionalTunable(TunableLineOfSightComponent()), live_drag_target=OptionalTunable( LiveDragTargetComponent.TunableFactory()), name=OptionalTunable(NameComponent.TunableFactory()), object_age=OptionalTunable(TunableObjectAgeComponent()), object_relationships=OptionalTunable( ObjectRelationshipComponent.TunableFactory()), object_teleportation=OptionalTunable( ObjectTeleportationComponent.TunableFactory()), ownable_component=OptionalTunable( OwnableComponent.TunableFactory()), proximity_component=OptionalTunable( ProximityComponent.TunableFactory()), spawner_component=OptionalTunable( SpawnerComponent.TunableFactory()), state=OptionalTunable(TunableStateComponent()), time_of_day_component=OptionalTunable( TimeOfDayComponent.TunableFactory()), tooltip_component=OptionalTunable( TooltipComponent.TunableFactory()), video=OptionalTunable(TunableVideoComponent()), welcome_component=OptionalTunable( WelcomeComponent.TunableFactory())), '_components_native': TunableTuple( description= '\n Tuning for native components, those that an object will have even\n if not tuned.\n ', tuning_group=GroupNames.COMPONENTS, Slot=OptionalTunable(SlotComponent.TunableFactory())), '_persists': Tunable( description= '\n Whether object should persist or not.\n ', tunable_type=bool, default=True, tuning_filter=FilterTag.EXPERT_MODE), '_world_file_object_persists': Tunable( description= "\n If object is from world file, check this if object state should\n persist. \n Example:\n If grill is dirty, but this is unchecked and it won't stay\n dirty when reloading the street. \n If Magic tree has this checked, all object relationship data\n will be saved.\n ", tunable_type=bool, default=False, tuning_filter=FilterTag.EXPERT_MODE), '_object_state_remaps': TunableList( description= '\n If this object is part of a Medator object suite, this list\n specifies which object tuning file to use for each catalog object\n state.\n ', tunable=TunableReference( description= '\n Current object state.\n ', manager=services.definition_manager(), tuning_filter=FilterTag.EXPERT_MODE)), 'environment_score_trait_modifiers': TunableMapping( description= '\n Each trait can put modifiers on any number of moods as well as the\n negative environment scoring.\n \n If tuning becomes a burden, consider making prototypes for many\n objects and tuning the prototype.\n \n Example: A Sim with the Geeky trait could have a modifier for the\n excited mood on objects like computers and tablets.\n \n Example: A Sim with the Loves Children trait would have a modifier\n for the happy mood on toy objects.\n \n Example: A Sim that has the Hates Art trait could get an Angry\n modifier, and should set modifiers like Happy to multiply by 0.\n ', key_type=TunableReference( description= '\n The Trait that the Sim must have to enable this modifier.\n ', manager=services.get_instance_manager( sims4.resources.Types.TRAIT)), value_type=TunableEnvironmentScoreModifiers.TunableFactory( description= '\n The Environmental Score modifiers for a particular trait.\n ' ), key_name='trait', value_name='modifiers'), 'slot_cost_modifiers': TunableMapping( description= "\n A mapping of slot types to modifier values. When determining slot\n scores in the transition sequence, if the owning object of a slot\n has a modifier for its type specified here, that slot will have the\n modifier value added to its cost. A positive modifier adds to the\n cost of a path using this slot and means that a slot will be less\n likely to be chosen. A negative modifier subtracts from the cost\n of a path using this slot and means that a slot will be more likely\n to be chosen.\n \n ex: Both bookcases and toilets have deco slots on them, but you'd\n rather a Sim prefer to put down an object in a bookcase than on the\n back of a toilet.\n ", key_type=SlotType.TunableReference( description= '\n A reference to the type of slot to be given a score modifier\n when considered for this object.\n ' ), value_type=Tunable( description= '\n A tunable float specifying the score modifier for the\n corresponding slot type on this object.\n ', tunable_type=float, default=0)), 'fire_retardant': Tunable( description= '\n If an object is fire retardant then not only will it not burn, but\n it also cannot overlap with fire, so fire will not spread into an\n area occupied by a fire retardant object.\n ', tunable_type=bool, default=False) } _commodity_flags = None additional_interaction_constraints = None def __init__(self, definition, **kwargs): super().__init__(definition, tuned_native_components=self._components_native, **kwargs) self._dynamic_commodity_flags_map = dict() for component_factory in self._components.values(): while component_factory is not None: self.add_component(component_factory(self)) self.item_location = ItemLocation.INVALID_LOCATION if self._persists: self._persistence_group = PersistenceGroups.OBJECT else: self._persistence_group = PersistenceGroups.NONE self._registered_transition_controllers = set() if self.definition.negative_environment_score != 0 or ( self.definition.positive_environment_score != 0 or self.definition.environment_score_mood_tags ) or self.environment_score_trait_modifiers: self.add_dynamic_component( objects.components.types.ENVIRONMENT_SCORE_COMPONENT. instance_attr) def on_reset_early_detachment(self, reset_reason): super().on_reset_early_detachment(reset_reason) for transition_controller in self._registered_transition_controllers: transition_controller.on_reset_early_detachment(self, reset_reason) def on_reset_get_interdependent_reset_records(self, reset_reason, reset_records): super().on_reset_get_interdependent_reset_records( reset_reason, reset_records) for transition_controller in self._registered_transition_controllers: transition_controller.on_reset_add_interdependent_reset_records( self, reset_reason, reset_records) def on_reset_internal_state(self, reset_reason): if reset_reason == ResetReason.BEING_DESTROYED: self._registered_transition_controllers.clear() else: if not (self.parent is not None and self.parent.is_sim and self.parent.posture_state.is_carrying(self)): if not CarryingObject.snap_to_good_location_on_floor( self, self.parent.transform, self.parent.routing_surface): self.clear_parent(self.parent.transform, self.parent.routing_surface) self.location = self.location super().on_reset_internal_state(reset_reason) def register_transition_controller(self, controller): self._registered_transition_controllers.add(controller) def unregister_transition_controller(self, controller): self._registered_transition_controllers.discard(controller) @classmethod def _verify_tuning_callback(cls): for (i, part_data) in enumerate(cls._part_data): while part_data.forward_direction_for_picking.magnitude() != 1.0: logger.warn( 'On {}, forward_direction_for_picking is {} on part {}, which is not a normalized vector.', cls, part_data.forward_direction_for_picking, i, owner='bhill') for sa in cls._super_affordances: if sa.allow_user_directed and not sa.display_name: logger.error( 'Interaction {} on {} does not have a valid display name.', sa.__name__, cls.__name__) while sa.consumes_object() or sa.contains_stat( CraftingTuning.CONSUME_STATISTIC): logger.error( 'ScriptObject: Interaction {} on {} is consume affordance, should tune on ConsumableComponent of the object.', sa.__name__, cls.__name__, owner='tastle/cjiang') @flexmethod def update_commodity_flags(cls, inst): commodity_flags = set() inst_or_cls = inst if inst is not None else cls for sa in inst_or_cls.super_affordances(): commodity_flags |= sa.commodity_flags if commodity_flags: cls._commodity_flags = frozenset(commodity_flags) else: cls._commodity_flags = EMPTY_SET @flexproperty def commodity_flags(cls, inst): if cls._commodity_flags is None: if inst is not None: inst.update_commodity_flags() else: cls.update_commodity_flags() if inst is not None: dynamic_commodity_flags = set() for dynamic_commodity_flags_entry in inst._dynamic_commodity_flags_map.values( ): dynamic_commodity_flags.update(dynamic_commodity_flags_entry) return frozenset(cls._commodity_flags | dynamic_commodity_flags) return cls._commodity_flags def add_dynamic_commodity_flags(self, key, commodity_flags): self._dynamic_commodity_flags_map[key] = commodity_flags def remove_dynamic_commodity_flags(self, key): if key in self._dynamic_commodity_flags_map: del self._dynamic_commodity_flags_map[key] @classproperty def tuned_components(cls): return cls._components @flexproperty def allowed_hands(cls, inst): if inst is not None: carryable = inst.carryable_component if carryable is not None: return carryable.allowed_hands return () carryable_tuning = cls._components.carryable if carryable_tuning is not None: return carryable_tuning.allowed_hands return () @flexproperty def holster_while_routing(cls, inst): if inst is not None: carryable = inst.carryable_component if carryable is not None: return carryable.holster_while_routing return False carryable_tuning = cls._components.carryable if carryable_tuning is not None: return carryable_tuning.holster_while_routing return False def is_surface(self, *args, **kwargs): return False @classproperty def _anim_overrides_cls(cls): if cls._anim_overrides is not None: return cls._anim_overrides(None) @property def object_routing_surface(self): pass @property def _anim_overrides_internal(self): params = { 'isParented': self.parent is not None, 'heightAboveFloor': slots.get_surface_height_parameter_for_object(self) } if self.is_part: params['subroot'] = self.part_suffix params['isMirroredPart'] = True if self.is_mirrored() else False overrides = AnimationOverrides(params=params) for component_overrides in self.component_anim_overrides_gen(): overrides = overrides(component_overrides()) if self._anim_overrides is not None: return overrides(self._anim_overrides()) return overrides @forward_to_components_gen def component_anim_overrides_gen(self): pass @property def parent(self): pass def ancestry_gen(self): obj = self while obj is not None: yield obj if obj.is_part: obj = obj.part_owner else: obj = obj.parent @property def parent_slot(self): pass def get_closest_parts_to_position(self, position, posture=None, posture_spec=None): best_parts = set() best_distance = MAX_FLOAT if position is not None and self.parts is not None: while True: for part in self.parts: while (posture is None or part.supports_posture_type(posture)) and ( posture_spec is None or part.supports_posture_spec(posture_spec)): dist = (part.position_with_forward_offset - position).magnitude_2d_squared() if dist < best_distance: best_parts.clear() best_parts.add(part) best_distance = dist elif dist == best_distance: best_parts.add(part) return best_parts def num_valid_parts(self, posture): raise RuntimeError( '[bhill] This function is believed to be dead code and is scheduled for pruning. If this exception has been raised, the code is not dead and this exception should be removed.' ) if self.parts is not None: return sum( part.supports_posture_type(posture.posture_type) for part in self.parts) return 0 def is_same_object_or_part(self, obj): if not isinstance(obj, ScriptObject): return False if obj is self: return True if obj.is_part and obj.part_owner is self or self.is_part and self.part_owner is obj: return True return False def is_same_object_or_part_of_same_object(self, obj): if not isinstance(obj, ScriptObject): return False if self.is_same_object_or_part(obj): return True if self.is_part and obj.is_part and self.part_owner is obj.part_owner: return True return False def get_compatible_parts(self, posture, interaction=None): if posture is not None and posture.target is not None and posture.target.is_part: return (posture.target, ) return self.get_parts_for_posture(posture, interaction) def get_parts_for_posture(self, posture, interaction=None): if self.parts is not None: return (part for part in self.parts if part.supports_posture_type( posture.posture_type, interaction)) return () def get_parts_for_affordance(self, affordance): raise RuntimeError( '[bhill] This function is believed to be dead code and is scheduled for pruning. If this exception has been raised, the code is not dead and this exception should be removed.' ) if self.parts is not None and affordance is not None: return (part for part in self.parts if part.supports_affordance(affordance)) return () def may_reserve(self, *args, **kwargs): return True def reserve(self, *args, **kwargs): pass def release(self, *args, **kwargs): pass @property def build_buy_lockout(self): return False @property def route_target(self): return (RouteTargetType.NONE, None) @flexmethod def super_affordances(cls, inst, context=None): from objects.base_interactions import BaseInteractionTuning inst_or_cls = inst if inst is not None else cls component_affordances_gen = inst.component_super_affordances_gen( ) if inst is not None else EMPTY_SET super_affordances = itertools.chain( inst_or_cls._super_affordances, BaseInteractionTuning.GLOBAL_AFFORDANCES, component_affordances_gen) super_affordances = list(super_affordances) shift_held = False if context is not None: shift_held = context.shift_held for sa in super_affordances: if shift_held: if sa.cheat: yield sa elif sa.debug and __debug__: yield sa elif sa.automation and paths.AUTOMATION_MODE: yield sa while not sa.debug and not sa.cheat: yield sa else: while not sa.debug and not sa.cheat: yield sa @forward_to_components_gen def component_super_affordances_gen(self): pass @caches.cached_generator def posture_interaction_gen(self): for affordance in self._super_affordances: while not affordance.debug: if affordance._provided_posture_type is not None: while True: for aop in affordance.potential_interactions( self, None): yield aop def supports_affordance(self, affordance): return True def potential_interactions(self, context, get_interaction_parameters=None, allow_forwarding=True, **kwargs): try: for affordance in self.super_affordances(context): if not self.supports_affordance(affordance): pass if get_interaction_parameters is not None: interaction_parameters = get_interaction_parameters( affordance, kwargs) else: interaction_parameters = kwargs for aop in affordance.potential_interactions( self, context, **interaction_parameters): yield aop for aop in self.potential_component_interactions(context): yield aop while allow_forwarding and ( self._should_search_forwarded_sim_aop or self._should_search_forwarded_child_aop): for aop in self._search_forwarded_interactions( context, self._should_search_forwarded_sim_aop, self._should_search_forwarded_child_aop, get_interaction_parameters=get_interaction_parameters, **kwargs): yield aop except Exception: logger.exception( 'Exception while generating potential interactions for {}:', self) def supports_posture_type(self, posture_type): for super_affordance in self._super_affordances: while super_affordance.provided_posture_type == posture_type: return True return False def _search_forwarded_interactions(self, context, search_sim_aops, search_child_aops, **kwargs): if search_sim_aops: for part_or_object in (self, ) if not self.parts else self.parts: user_list = part_or_object.get_users(sims_only=True) for user in user_list: if part_or_object.is_part and part_or_object.disable_child_aop_forwarding: pass for aop in user.potential_interactions(context, **kwargs): while aop.affordance.allow_forward: yield aop if not search_child_aops: return for child in self.children: if child.parent.is_part and child.parent.disable_child_aop_forwarding: pass for aop in child.potential_interactions(context, **kwargs): while aop.affordance.allow_forward: yield aop def add_dynamic_component(self, *args, **kwargs): result = super().add_dynamic_component(*args, **kwargs) if result: self.resend_interactable() return result @distributor.fields.Field(op=distributor.ops.SetInteractable, default=False) def interactable(self): if self.build_buy_lockout: return False if self._super_affordances: return True for _ in self.component_interactable_gen(): pass return False resend_interactable = interactable.get_resend() @forward_to_components_gen def component_interactable_gen(self): pass @caches.cached(maxsize=20) def check_line_of_sight(self, transform, verbose=False): top_level_parent = self while top_level_parent.parent is not None: top_level_parent = top_level_parent.parent if top_level_parent.wall_or_fence_placement: if verbose: return (routing.RAYCAST_HIT_TYPE_NONE, None) return (True, None) if self.is_in_inventory(): if verbose: return (routing.RAYCAST_HIT_TYPE_NONE, None) return (True, None) slot_routing_location = self.get_routing_location_for_transform( transform) if verbose: ray_test = routing.ray_test_verbose else: ray_test = routing.ray_test return ray_test(slot_routing_location, self.routing_location, self.raycast_context(), return_object_id=True) clear_check_line_of_sight_cache = check_line_of_sight.cache.clear def _create_raycast_context(self, *args, **kwargs): super()._create_raycast_context(*args, **kwargs) if not self.is_sim: self.clear_check_line_of_sight_cache() @property def connectivity_handles(self): routing_context = self.get_or_create_routing_context() return routing_context.connectivity_handles def _clear_connectivity_handles(self): if self._routing_context is not None: self._routing_context.connectivity_handles.clear() @property def focus_bone(self): return 0 @forward_to_components def on_state_changed(self, state, old_value, new_value): pass @forward_to_components def on_post_load(self): pass @forward_to_components def on_finalize_load(self): pass @property def attributes(self): pass @property def flammable(self): return False @attributes.setter def attributes(self, value): logger.debug('PERSISTENCE: Attributes property on {0} were set', self) try: object_data = ObjectData() object_data.ParseFromString(value) self.load_object(object_data) except: logger.exception('Exception applying attributes to object {0}', self) def load_object(self, object_data): save_data = protocols.PersistenceMaster() save_data.ParseFromString(object_data.attributes) self.load(save_data) self.on_post_load() def is_persistable(self): if self.persistence_group == objects.persistence_groups.PersistenceGroups.OBJECT: return True if self.item_location == ItemLocation.FROM_WORLD_FILE: return self._world_file_object_persists if self.item_location == ItemLocation.ON_LOT or self.item_location == ItemLocation.FROM_OPEN_STREET: return self._persists if self.persistence_group == objects.persistence_groups.PersistenceGroups.IN_OPEN_STREET and self.item_location == ItemLocation.INVALID_LOCATION: return self._persist return False def save_object(self, object_list, item_location=ItemLocation.ON_LOT, container_id=0): if not self.is_persistable(): return with ProtocolBufferRollback(object_list) as save_data: attribute_data = self.get_attribute_save_data() save_data.object_id = self.id if attribute_data is not None: save_data.attributes = attribute_data.SerializeToString() save_data.guid = self.definition.id save_data.loc_type = item_location save_data.container_id = container_id return save_data def get_attribute_save_data(self): attribute_data = protocols.PersistenceMaster() self.save(attribute_data) return attribute_data @forward_to_components def save(self, persistence_master_message): pass def load(self, persistence_master_message): component_priority_list = [] for persistable_data in persistence_master_message.data: component_priority_list.append( (get_component_priority_and_name_using_persist_id( persistable_data.type), persistable_data)) component_priority_list.sort(key=lambda priority: priority[0][0], reverse=True) for ((_, (_, inst_comp)), persistable_data) in component_priority_list: while inst_comp: self.add_dynamic_component(inst_comp) if self.has_component(inst_comp): getattr(self, inst_comp).load(persistable_data) def finalize(self, **kwargs): self.on_finalize_load() def clone(self, **kwargs): clone = objects.system.create_object(self.definition, **kwargs) object_list = file_serialization.ObjectList() save_data = self.save_object(object_list.objects) clone.load_object(save_data) return clone
class Commodity(HasTunableReference, TunedContinuousStatistic, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(sims4.resources.Types.STATISTIC)): __qualname__ = 'Commodity' REMOVE_INSTANCE_TUNABLES = ('initial_value',) INSTANCE_TUNABLES = {'stat_name': TunableLocalizedString(description='\n Localized name of this commodity.\n ', export_modes=ExportModes.All), 'min_value_tuning': Tunable(description='\n The minimum value for this stat.\n ', tunable_type=float, default=-100, export_modes=ExportModes.All), 'max_value_tuning': Tunable(description='\n The maximum value for this stat.', tunable_type=float, default=100, export_modes=ExportModes.All), 'ui_sort_order': TunableRange(description='\n Order in which the commodity will appear in the motive panel.\n Commodities sort from lowest to highest.\n ', tunable_type=int, default=0, minimum=0, export_modes=ExportModes.All), 'ui_visible_distress_threshold': Tunable(description='\n When current value of commodity goes below this value, commodity\n will appear in the motive panel tab.\n ', tunable_type=float, default=0, export_modes=ExportModes.All), 'ad_data': TunableList(description='\n A list of Vector2 points that define the desire curve for this\n commodity.\n ', tunable=TunableVector2(description='\n Point on a Curve\n ', default=sims4.math.Vector2(0, 0), export_modes=ExportModes.All)), 'auto_satisfy_curve_tuning': TunableList(description='\n A list of Vector2 points that define the auto-satisfy curve for\n this commodity.\n ', tunable=TunableVector2(description='\n Point on a Curve\n ', default=sims4.math.Vector2(0, 0))), 'auto_satisfy_curve_random_time_offset': TunableSimMinute(description='\n An amount of time that when auto satisfy curves are being used\n will modify the time current time being used to plus or minus\n a random number between this value.\n ', default=120), 'maximum_auto_satisfy_time': TunableSimMinute(description='\n The maximum amount of time that the auto satisfy curves will\n interpolate the values based on the current one before just\n setting to the maximum value.\n ', default=1440), 'initial_tuning': TunableTuple(description=' \n The Initial value for this commodity. Can either be a single\n value, range, or use auto satisfy curve to determine initial\n value. Use auto satisfy curve will take precedence over range\n value and range value will take precedence over single value\n range.\n ', _use_auto_satisfy_curve_as_initial_value=Tunable(description="\n If checked, when we first add this commodity to a sim (sims only),\n the initial value of the commodity will be set according to\n the auto-satisfy curves defined by this commodity's tuning as\n opposed to the tuned initial value. \n ", tunable_type=bool, needs_tuning=True, default=False), _value_range=OptionalTunable(description='\n If enabled then when we first add this commodity to a Sim the\n initial value of the commodity will be set to a random value\n within this interval.\n ', tunable=TunableInterval(description='\n An interval that will be used for the initial value of this\n commodity.\n ', tunable_type=int, default_lower=0, default_upper=100)), _value=Tunable(description='\n The initial value for this stat.', tunable_type=float, default=0.0)), 'weight': Tunable(description="\n The weight of the Skill with regards to autonomy. It's ignored \n for the purposes of sorting stats, but it's applied when scoring \n the actual statistic operation for the SI.\n ", tunable_type=float, default=0.5), 'states': TunableList(description='\n Commodity states based on thresholds. This should be ordered\n from worst state to best state.\n ', tunable=TunableCommodityState()), 'commodity_distress': OptionalTunable(TunableCommodityDistress()), 'commodity_failure': OptionalTunable(TunableCommodityFailure()), 'remove_on_convergence': Tunable(description='\n Commodity will be removed when convergence is met only if not\n a core commodity.\n ', tunable_type=bool, default=True), 'visible': Tunable(description='\n Whether or not commodity should be sent to client.\n ', tunable_type=bool, default=False, export_modes=ExportModes.All), '_add_if_not_in_tracker': Tunable(description="\n If True, when we try to add or set the commodity, we will add\n the commodity to the tracker if the tracker doesn't already have\n it.\n \n e.g If a sim uses the toilet and we update bladder when that sim\n doesn't have the bladder commodity in his/her tracker, we will\n add the bladder commodity to that sim. \n \n Set this to false for the case of NPC behavior commodities like\n Being a Maid or Being a Burglar.\n ", tunable_type=bool, default=True), 'initial_as_default': Tunable(description='\n Setting this to true will cause the default value returned during testing to be the \n initial value tuned. This happens when a test is run on this commodity on a Sim that\n does not have the commodity. Leaving this as false will instead return the convergence\n value.\n ', tunable_type=bool, default=False), 'arrow_data': TunableArrowData(description='\n Used to determine when positive or negative arrows should show\n up depending on the delta rate of the commodity.\n ', export_modes=(ExportModes.ClientBinary,)), '_categories': TunableSet(description='\n List of categories that this statistic is part of.\n ', tunable=StatisticCategory), '_off_lot_simulation': OptionalTunable(TunableTuple(threshold=TunableThreshold(description='\n The threshold that will activate the increase in value\n when the commodity hits it.\n ', value=Tunable(description='\n The value that this threshold will trigger on.\n ', tunable_type=int, default=-50)), value=Tunable(description='\n The value that this commodity will increase by once it hits\n the tuned threshold while the sim is offlot.\n ', tunable_type=int, default=100), description='\n Offlot simulation for this commodity. The commodity will be\n allowed to decay at a normal rate until it hits the tuned\n threshold. Once there it will then have its value added by the\n tuned value.\n ')), '_max_simulate_time_on_load': OptionalTunable(description="\n If enabled, this commodity will only simulate for a max amount\n of time when the player loads back into the lot with a new world\n game time.\n \n By default, this is disabled. When disabled, the commodity will\n simulate for however long between the lot's previous saved time\n and the current world time. (Note: this is capped by PersistenceTuning.MAX_LOT_SIMULATE_ELAPSED_TIME)\n ", tunable=TunableSimMinute(description="\n If set to > 0, on load, this object commodity will update its value to\n world time. And the commodity will simulate for the max amount of time\n specified in the tunable.\n EX: If tuned for the water commodity on plants to 6 hours --\n if the player leaves the lot for 4 hours and then comes back,\n the water commodity will update to what it should be 4 hours later.\n If the player leaves the lot for 8 hours and comes back, the water\n commodity will only update to what it should be 6 hours later.\n \n If set to 0, no matter how much time has elapsed since the\n player last visited the lot, this commodity's value will load\n to its last saved value.\n ", default=1440, minimum=0)), '_time_passage_fixup_type': TunableEnumEntry(description="\n This is for commodities on SIMS only.\n This option what we do with the commodity when the sim\n gets instanced after time has elapsed since the last time the sim\n was spawned.\n \n do not fixup: Means the commodity will stay the same value as it was\n when the sim was last instantiated\n \n fixup using autosatisfy curve: The commodity's value will be set\n based on its autosatisfy curve and the time between when the sim was\n last saved. Note, this fixup will not occur for active household sims\n if offlot simulation is enabled for this commodity.\n \n fixup using time elapsed: The commodity will decay linearly based on\n when the sim was last saved. Use this for things like commodities\n that control buff timers to make sure that the time remaining on\n a buff remains consistent.\n ", tunable_type=CommodityTimePassageFixupType, default=CommodityTimePassageFixupType.DO_NOT_FIXUP), 'use_stat_value_on_init': Tunable(description='\n When set the initial value for the commodity will be set from the\n commodity tuning.\n If unchecked, the initial stat value will not be set on \n initialization, but instead will use other systems (like the state)\n to set its initial value.\n ', tunable_type=bool, default=True), 'stat_asm_param': TunableStatAsmParam.TunableFactory(locked_args={'use_effective_skill_level': True})} initial_value = 0 _auto_satisfy_curve = None use_autosatisfy_curve = True commodity_states = None @classmethod def _tuning_loaded_callback(cls): super()._tuning_loaded_callback() cls.initial_value = cls.initial_tuning._value cls._build_utility_curve_from_tuning_data(cls.ad_data) if cls.auto_satisfy_curve_tuning: point_list = [(point.x, point.y) for point in cls.auto_satisfy_curve_tuning] cls._auto_satisfy_curve = sims4.math.CircularUtilityCurve(point_list, 0, date_and_time.HOURS_PER_DAY) if cls.states: state_zero = cls.states[0] if state_zero.value < cls.min_value: logger.error('Worst state should not be lower than min value of commodity. Please update tuning') cls.commodity_states = cls.states elif state_zero.value > cls.min_value: state = CommodityState(value=cls.min_value, buff=BuffReference()) cls.commodity_states = (state,) + cls.states else: cls.commodity_states = cls.states previous_value = cls.max_value index = len(cls.commodity_states) for state in reversed(cls.commodity_states): index -= 1 if state.value >= previous_value: logger.error('{0} has a lower bound value of state at index:{1} that is higher than the previous state. Please update tuning', cls, index) if state.buff_add_threshold is not None: threshold_value = state.buff_add_threshold.value if threshold_value < state.value or threshold_value > previous_value: logger.error('{0} add buff threshold is out of range for state at index:{1}. Please update tuning', cls, index) previous_value = state.value while state.buff is not None and state.buff.buff_type is not None: state.buff.buff_type.add_owning_commodity(cls) @classmethod def _verify_tuning_callback(cls): if cls.visible and (cls.ui_visible_distress_threshold < cls.min_value or cls.ui_visible_distress_threshold > cls.max_value): logger.error('{} visible distress value {} is outside the min{} / max {} range. Please update tuning', cls, cls.ui_visible_distress_threshold, cls.min_value, cls.max_value) def __init__(self, tracker, core=False): self._allow_convergence_callback_to_activate = False self._buff_handle = None super().__init__(tracker, self.get_initial_value()) self._core = core self._buff_handle = None self._buff_threshold_callback = None self._current_state_index = None self._current_state_ge_callback_data = None self._current_state_lt_callback_data = None self._off_lot_callback_data = None self._distress_buff_handle = None self._exit_distress_callback_data = None self._distress_callback_data = None self._failure_callback_data = None self._convergence_callback_data = None self._suppress_client_updates = False self.force_apply_buff_on_start_up = False self.force_buff_reason = None if getattr(self.tracker.owner, 'is_simulating', True): activate_convergence_callback = self.default_value != self.get_value() self.on_initial_startup(from_init=True, activate_convergence_callback=activate_convergence_callback) @classproperty def initial_value_range(cls): return cls.initial_tuning._value_range @classproperty def use_auto_satisfy_curve_as_initial_value(cls): return cls.initial_tuning._use_auto_satisfy_curve_as_initial_value @classmethod def get_initial_value(cls): if cls.initial_value_range is None: return cls.initial_value return random.uniform(cls.initial_value_range.lower_bound, cls.initial_value_range.upper_bound) @classproperty def use_stat_value_on_initialization(cls): return cls.use_stat_value_on_init @property def core(self): return self._core @core.setter def core(self, value): self._core = value @property def is_visible(self): return self.visible def _setup_commodity_distress(self): if self.commodity_distress is not None: self._distress_callback_data = self.create_callback(Threshold(self.commodity_distress.threshold_value, operator.le), self._enter_distress) def _setup_commodity_failure(self): if self.commodity_failure is not None: self._failure_callback_data = self.create_callback(self.commodity_failure.threshold, self._commodity_fail) def on_initial_startup(self, from_init=False, activate_convergence_callback=True): self._setup_commodity_distress() if self._distress_callback_data is not None: self.add_callback_data(self._distress_callback_data) self._setup_commodity_failure() if self._failure_callback_data is not None: self.add_callback_data(self._failure_callback_data) if self.commodity_states: self._remove_state_callback() current_value = self.get_value() new_state_index = self._find_state_index(current_value) if self._current_state_index != new_state_index: self._set_state(new_state_index, current_value, from_init=from_init, send_client_update=False) self._add_state_callback() self.decay_enabled = not self.tracker.owner.is_locked(self) self.force_apply_buff_on_start_up = False if self.force_buff_reason is not None and self._buff_handle is not None: current_state = self.commodity_states[self._current_state_index] self.tracker.owner.set_buff_reason(current_state.buff.buff_type, self.force_buff_reason, use_replacement=True) self.force_buff_reason = None if self.remove_on_convergence and self._convergence_callback_data is None: self._convergence_callback_data = self.create_callback(Threshold(self.convergence_value, operator.eq), self._remove_self_from_tracker) if activate_convergence_callback: self.add_callback_data(self._convergence_callback_data) self._allow_convergence_callback_to_activate = False else: self._allow_convergence_callback_to_activate = True @contextlib.contextmanager def _suppress_client_updates_context_manager(self, from_load=False, is_rate_change=True): if self._suppress_client_updates: yield None else: self._suppress_client_updates = True try: yield None finally: self._suppress_client_updates = False if not from_load: self.send_commodity_progress_msg(is_rate_change=is_rate_change) def _commodity_telemetry(self, hook, desired_state_index): if not self.tracker.owner.is_sim: return with telemetry_helper.begin_hook(writer, hook, sim=self.tracker.get_sim()) as hook: guid = getattr(self, 'guid64', None) if guid is not None: hook.write_guid('stat', self.guid64) else: logger.info('{} does not have a guid64', self) hook.write_int('oldd', self._current_state_index) hook.write_int('news', desired_state_index) def _update_state_up(self, stat_instance): with self._suppress_client_updates_context_manager(): current_value = self.get_value() desired_state_index = self._find_state_index(current_value) if desired_state_index == self._current_state_index: desired_state_index = self._find_state_index(current_value + EPSILON) if desired_state_index != self._current_state_index: self._remove_state_callback() self._commodity_telemetry(TELEMETRY_HOOK_STATE_UP, desired_state_index) while self._current_state_index < desired_state_index: next_index = self._current_state_index + 1 self._set_state(next_index, current_value, send_client_update=next_index == desired_state_index) self._update_state_callback(desired_state_index) else: logger.warn('{} update state up was called, but state did not change. current state_index:{}', self, self._current_state_index, owner='msantander') def _update_state_down(self, stat_instance): with self._suppress_client_updates_context_manager(): current_value = self.get_value() desired_state_index = self._find_state_index(self.get_value()) if desired_state_index == self._current_state_index: desired_state_index = self._find_state_index(current_value - EPSILON) if desired_state_index != self._current_state_index: self._remove_state_callback() self._commodity_telemetry(TELEMETRY_HOOK_STATE_DOWN, desired_state_index) while self._current_state_index > desired_state_index: prev_index = self._current_state_index - 1 self._set_state(prev_index, current_value, send_client_update=prev_index == desired_state_index) self._update_state_callback(desired_state_index) else: logger.warn('{} update state down was called, but state did not change. current state_index:{}', self, self._current_state_index, owner='msantander') def _update_state_callback(self, desired_state_index): new_state_index = self._find_state_index(self.get_value()) if new_state_index > desired_state_index: self._update_state_up(self) elif new_state_index < desired_state_index: self._update_state_down(self) else: self._add_state_callback() def _state_reset_callback(self, stat_instance, time): self._update_buff(self._get_change_rate_without_decay()) def _remove_self_from_tracker(self, _): tracker = self._tracker if tracker is not None: tracker.remove_statistic(self.stat_type) def _off_lot_simulation_callback(self, _): self.add_value(self._off_lot_simulation.value) def start_low_level_simulation(self): if self._off_lot_simulation is None: self.decay_enabled = False return self._off_lot_callback_data = self.add_callback(self._off_lot_simulation.threshold, self._off_lot_simulation_callback) self.decay_enabled = True def stop_low_level_simulation(self): self.decay_enabled = False if self._off_lot_callback_data is not None: self.remove_callback(self._off_lot_callback_data) def stop_regular_simulation(self): self._remove_state_callback() self.decay_enabled = False if self._convergence_callback_data is not None: self.remove_callback(self._convergence_callback_data) self._convergence_callback_data = None if self._distress_callback_data is not None: self.remove_callback(self._distress_callback_data) self._distress_callback_data = None if self.commodity_distress is not None: self._exit_distress(self, True) if self._failure_callback_data is not None: self.remove_callback(self._failure_callback_data) self._failure_callback_data = None def _find_state_index(self, current_value): index = len(self.commodity_states) - 1 while index >= 0: state = self.commodity_states[index] if current_value >= state.value: return index index -= 1 return 0 def _add_state_callback(self): next_state_index = self._current_state_index + 1 if next_state_index < len(self.commodity_states): self._current_state_ge_callback_data = self.add_callback(Threshold(self.commodity_states[next_state_index].value, operator.ge), self._update_state_up, on_callback_alarm_reset=self._state_reset_callback) if self.commodity_states[self._current_state_index].value > self.min_value: self._current_state_lt_callback_data = self.add_callback(Threshold(self.commodity_states[self._current_state_index].value, operator.lt), self._update_state_down, on_callback_alarm_reset=self._state_reset_callback) def _remove_state_callback(self): if self._current_state_ge_callback_data is not None: self.remove_callback(self._current_state_ge_callback_data) self._current_state_ge_callback_data = None if self._current_state_lt_callback_data is not None: self.remove_callback(self._current_state_lt_callback_data) self._current_state_lt_callback_data = None if self._buff_threshold_callback is not None: self.remove_callback(self._buff_threshold_callback) self._buff_threshold_callback = None def _get_next_buff_commodity_decaying_to(self): transition_into_buff_id = 0 if self._current_state_index is not None and self._current_state_index > 0: current_value = self.get_value() buff_tunable_ref = None if self.convergence_value <= current_value: buff_tunable_ref = self.commodity_states[self._current_state_index - 1].buff else: next_state_index = self._current_state_index + 1 if next_state_index < len(self.commodity_states): buff_tunable_ref = self.commodity_states[next_state_index].buff if buff_tunable_ref is not None: buff_type = buff_tunable_ref.buff_type if buff_type is not None and buff_type.visible: transition_into_buff_id = buff_type.guid64 return transition_into_buff_id def _add_buff_from_state(self, commodity_state): owner = self.tracker.owner if owner.is_sim: buff_tuning = commodity_state.buff transition_into_buff_id = self._get_next_buff_commodity_decaying_to() if buff_tuning.buff_type.visible else 0 self._buff_handle = owner.add_buff(buff_tuning.buff_type, buff_reason=buff_tuning.buff_reason, commodity_guid=self.guid64, change_rate=self._get_change_rate_without_decay(), transition_into_buff_id=transition_into_buff_id) def _add_buff_callback(self, _): current_state = self.commodity_states[self._current_state_index] self.remove_callback(self._buff_threshold_callback) self._buff_threshold_callback = None self._add_buff_from_state(current_state) def _set_state(self, new_state_index, current_value, from_init=False, send_client_update=True): new_state = self.commodity_states[new_state_index] old_state_index = self._current_state_index self._current_state_index = new_state_index if self._buff_threshold_callback is not None: self.remove_callback(self._buff_threshold_callback) self._buff_threshold_callback = None if self._buff_handle is not None: self.tracker.owner.remove_buff(self._buff_handle) self._buff_handle = None if new_state.buff.buff_type: if new_state.buff_add_threshold is not None and not self.force_apply_buff_on_start_up and not new_state.buff_add_threshold.compare(current_value): self._buff_threshold_callback = self.add_callback(new_state.buff_add_threshold, self._add_buff_callback) else: self._add_buff_from_state(new_state) if (old_state_index is not None or from_init) and new_state.loot_list_on_enter is not None and self.tracker.owner.is_sim: resolver = event_testing.resolver.SingleSimResolver(self.tracker.owner) while True: for loot_action in new_state.loot_list_on_enter: loot_action.apply_to_resolver(resolver) if send_client_update: self.send_commodity_progress_msg() def _enter_distress(self, stat_instance): if self.tracker.owner.get_sim_instance() is None: return if self.commodity_distress.buff.buff_type is not None: if self._distress_buff_handle is None: self._distress_buff_handle = self.tracker.owner.add_buff(self.commodity_distress.buff.buff_type, self.commodity_distress.buff.buff_reason, commodity_guid=self.guid64) else: logger.error('Distress Buff Handle is not none when entering Commodity Distress for {}.', self, owner='jjacobson') if self._exit_distress_callback_data is None: self._exit_distress_callback_data = self.add_callback(Threshold(self.commodity_distress.threshold_value, operator.gt), self._exit_distress) else: logger.error('Exit Distress Callback Data is not none when entering Commodity Distress for {}.', self, owner='jjacobson') self.tracker.owner.enter_distress(self) sim = self.tracker.owner.get_sim_instance() for si in itertools.chain(sim.si_state, sim.queue): while self.stat_type in si.commodity_flags: return context = interactions.context.InteractionContext(self.tracker.owner.get_sim_instance(), interactions.context.InteractionContext.SOURCE_AUTONOMY, interactions.priority.Priority.High, insert_strategy=QueueInsertStrategy.NEXT, bucket=interactions.context.InteractionBucketType.DEFAULT) self.tracker.owner.get_sim_instance().push_super_affordance(self.commodity_distress.distress_interaction, None, context) def _exit_distress(self, stat_instance, on_removal=False): if self._distress_buff_handle is not None: self.tracker.owner.remove_buff(self._distress_buff_handle) self._distress_buff_handle = None elif self.commodity_distress.buff.buff_type is not None and not on_removal: logger.error('Distress Buff Handle is none when exiting Commodity Distress for {}.', self, owner='jjacobson') if self._exit_distress_callback_data is not None: self.remove_callback(self._exit_distress_callback_data) self._exit_distress_callback_data = None elif not on_removal: logger.error('Exit distress called before exit distress callback has been setup for {}.', self, owner='jjacobson') self.tracker.owner.exit_distress(self) def _commodity_fail_object(self, stat_instance): context = interactions.context.InteractionContext(None, interactions.context.InteractionContext.SOURCE_SCRIPT, interactions.priority.Priority.Critical, bucket=interactions.context.InteractionBucketType.DEFAULT) owner = self.tracker.owner for failure_interaction in self.commodity_failure.failure_interactions: if not failure_interaction.immediate or not failure_interaction.simless: logger.error('Trying to use a non-immediate and/or non-simless\n interaction as a commodity failure on an object. Object\n commodity failures can only push immediate, simless\n interactions. - trevor') break aop = interactions.aop.AffordanceObjectPair(failure_interaction, owner, failure_interaction, None) while aop.test_and_execute(context): break def _commodity_fail(self, stat_instance): owner = self.tracker.owner if not owner.is_sim: return self._commodity_fail_object(stat_instance) sim = owner.get_sim_instance() if sim is None: return context = interactions.context.InteractionContext(sim, interactions.context.InteractionContext.SOURCE_SCRIPT, interactions.priority.Priority.Critical, bucket=interactions.context.InteractionBucketType.DEFAULT) for failure_interaction in self.commodity_failure.failure_interactions: while sim.push_super_affordance(failure_interaction, None, context): break def fixup_on_sim_instantiated(self): sim = self.tracker.owner if self.time_passage_fixup_type() == CommodityTimePassageFixupType.FIXUP_USING_TIME_ELAPSED: time_sim_was_saved = sim.time_sim_was_saved if time_sim_was_saved is not None: if not sim.is_locked(self): self.decay_enabled = True self._last_update = time_sim_was_saved self._update_value() self.decay_enabled = False elif self.time_passage_fixup_type() == CommodityTimePassageFixupType.FIXUP_USING_AUTOSATISFY_CURVE and (sim.is_npc or self._off_lot_simulation is None): self.set_to_auto_satisfy_value() def set_to_auto_satisfy_value(self): if self.use_autosatisfy_curve and self._auto_satisfy_curve: now = services.time_service().sim_now time_sim_was_saved = self.tracker.owner.time_sim_was_saved if time_sim_was_saved is None and not self.use_auto_satisfy_curve_as_initial_value or time_sim_was_saved == now: return False random_time_offset = random.uniform(-1*self.auto_satisfy_curve_random_time_offset, self.auto_satisfy_curve_random_time_offset) now += interval_in_sim_minutes(random_time_offset) current_hour = now.hour() + now.minute()/date_and_time.MINUTES_PER_HOUR auto_satisfy_value = self._auto_satisfy_curve.get(current_hour) maximum_auto_satisfy_time = interval_in_sim_minutes(self.maximum_auto_satisfy_time) if time_sim_was_saved is None or time_sim_was_saved + maximum_auto_satisfy_time <= now: self._last_update = services.time_service().sim_now self.set_user_value(auto_satisfy_value) return True if time_sim_was_saved >= now: return False interpolation_time = (now - time_sim_was_saved).in_ticks()/maximum_auto_satisfy_time.in_ticks() current_value = self.get_user_value() new_value = (auto_satisfy_value - current_value)*interpolation_time + current_value self._last_update = services.time_service().sim_now self.set_user_value(new_value) return True return False def on_remove(self, on_destroy=False): super().on_remove(on_destroy=on_destroy) self.stop_regular_simulation() self.stop_low_level_simulation() if self._buff_handle is not None: self.tracker.owner.remove_buff(self._buff_handle, on_destroy=on_destroy) self._buff_handle = None if self._distress_buff_handle is not None: self.tracker.owner.remove_buff(self._distress_buff_handle, on_destroy=on_destroy) self._distress_buff_handle = None def _activate_convergence_callback(self): if self._allow_convergence_callback_to_activate: if self._convergence_callback_data is not None: self.add_callback_data(self._convergence_callback_data) self._allow_convergence_callback_to_activate = False def set_value(self, value, from_load=False, **kwargs): with self._suppress_client_updates_context_manager(from_load=from_load, is_rate_change=False): if not from_load: change = value - self.get_value() self._update_buff(change) super().set_value(value, from_load=from_load, **kwargs) if not from_load and self.visible: self.send_commodity_progress_msg(is_rate_change=False) self._update_buff(0) self._activate_convergence_callback() def _on_statistic_modifier_changed(self, notify_watcher=True): super()._on_statistic_modifier_changed(notify_watcher=notify_watcher) self.send_commodity_progress_msg() self._update_buff(self._get_change_rate_without_decay()) self._update_callbacks() self._activate_convergence_callback() def _recalculate_modified_decay_rate(self): super()._recalculate_modified_decay_rate() if self._decay_rate_modifier > 1: self._update_buff(-self._decay_rate_modifier) else: self._update_buff(0) @property def buff_handle(self): return self._buff_handle def _update_buff(self, change_rate): if self._buff_handle is not None: self.tracker.owner.buff_commodity_changed(self._buff_handle, change_rate=change_rate) @property def state_index(self): return self._current_state_index @classmethod def get_state_index_matches_buff_type(cls, buff_type): if cls.commodity_states: for index in range(len(cls.commodity_states)): state = cls.commodity_states[index] if state.buff is None: pass while state.buff.buff_type is buff_type: return index @classproperty def max_value(cls): return cls.max_value_tuning @classproperty def min_value(cls): return cls.min_value_tuning @classproperty def autonomy_weight(cls): return cls.weight @classproperty def default_value(cls): if not cls.initial_as_default: return cls._default_convergence_value return cls.initial_value @classproperty def is_skill(cls): return False @classproperty def add_if_not_in_tracker(cls): return cls._add_if_not_in_tracker @classproperty def max_simulate_time_on_load(cls): return cls._max_simulate_time_on_load def time_passage_fixup_type(self): return self._time_passage_fixup_type @classmethod def get_categories(cls): return cls._categories def send_commodity_progress_msg(self, is_rate_change=True): commodity_msg = self.create_commmodity_update_msg(is_rate_change=is_rate_change) if commodity_msg is None: return send_sim_commodity_progress_update_message(self.tracker.owner, commodity_msg) def create_commmodity_update_msg(self, is_rate_change=True): if self.tracker is None or not self.tracker.owner.is_sim: return if not self.visible: return if not self.commodity_states: return if self.state_index is None: return if self._suppress_client_updates: return commodity_msg = Commodities_pb2.CommodityProgressUpdate() commodity_msg.commodity_id = self.guid64 commodity_msg.current_value = self.get_value() commodity_msg.rate_of_change = self.get_change_rate() commodity_msg.commodity_state_index = self.state_index commodity_msg.is_rate_change = is_rate_change return commodity_msg