class EffectiveSkillModifier(HasTunableSingletonFactory, BaseGameEffectModifier): FACTORY_TUNABLES = {'description': '\n The modifier to change the effective skill or skill_tag tuned in the\n modifier key The value of the modifier can be negative..\n ', 'modifier_key': TunableVariant(description='\n ', skill_type=Skill.TunableReference(description='\n What skill to apply the modifier on.', pack_safe=True), skill_tag=TunableEnumEntry(description='\n What skill tag to apply the modifier on.', tunable_type=tag.Tag, default=tag.Tag.INVALID)), 'modifier_value': Tunable(description='\n The value to change the effective skill. Can be negative.', tunable_type=int, default=0)} def __init__(self, modifier_key, modifier_value, **kwargs): super().__init__(GameEffectType.EFFECTIVE_SKILL_MODIFIER) self.modifier_key = modifier_key self.modifier_value = modifier_value def can_modify(self, skill): if self.modifier_key is skill.skill_type: return True return self.modifier_key in skill.tags def get_modifier_value(self, skill): if self.can_modify(skill): return self.modifier_value return 0
class Venue(metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(sims4.resources.Types.VENUE) ): __qualname__ = 'Venue' INSTANCE_TUNABLES = { 'display_name': TunableLocalizedString( description= '\n Name that will be displayed for the venue\n ', export_modes=ExportModes.All), 'display_name_incomplete': TunableLocalizedString( description= '\n Name that will be displayed for the incomplete venue\n ', export_modes=ExportModes.All), 'venue_description': TunableLocalizedString( description='Description of Venue that will be displayed', export_modes=ExportModes.All), 'venue_icon': TunableResourceKey(None, resource_types=sims4.resources.CompoundTypes.IMAGE, description='Venue Icon for UI', export_modes=ExportModes.All), 'venue_thumbnail': TunableResourceKey(None, resource_types=sims4.resources.CompoundTypes.IMAGE, description='Image of Venue that will be displayed', export_modes=ExportModes.All), 'allow_game_triggered_events': Tunable( description= '\n Whether this venue can have game triggered events. ex for careers\n ', tunable_type=bool, default=False), 'background_event_schedule': TunableSituationWeeklyScheduleFactory( description= '\n The Background Events that run on this venue. They run underneath\n any user facing Situations and there can only be one at a time. The\n schedule times and durations are windows in which background events\n can start.\n ' ), 'special_event_schedule': TunableSituationWeeklyScheduleFactory( description= '\n The Special Events that run on this venue. These run on top of\n Background Events. We run only one user facing event at a time, so\n if the player started something then this may run in the\n background, otherwise the player will be invited to join in on this\n Venue Special Event.\n ' ), 'required_objects': TunableList( description= '\n A list of objects that are required to be on a lot before\n that lot can be labeled as this venue.\n ', tunable=TunableVenueObject( description= "\n Specify object tag(s) that must be on this venue.\n Allows you to group objects, i.e. weight bench,\n treadmill, and basketball goals are tagged as\n 'exercise objects.'\n \n This is not the same as automatic objects tuning. \n Please read comments for both the fields.\n " ), export_modes=ExportModes.All), 'npc_summoning_behavior': sims4.tuning.tunable.TunableMapping( description= '\n Whenever an NPC is summoned to a lot by the player, determine\n which action to take based on the summoning purpose. The purpose\n is a dynamic enum: venues.venue_constants.NPCSummoningPurpose.\n \n The action will generally involve either adding a sim to an existing\n situation or creating a situation then adding them to it.\n \n \\depot\\Sims4Projects\\Docs\\Design\\Open Streets\\Open Street Invite Matrix.xlsx\n \n residential: This is behavior pushed on the NPC if this venue was a residential lot.\n create_situation: Place the NPC in the specified situation/job pair.\n add_to_background_situation: Add the NPC the currently running background \n situation in the venue.\n ', key_type=sims4.tuning.tunable.TunableEnumEntry( venues.venue_constants.NPCSummoningPurpose, venues.venue_constants.NPCSummoningPurpose.DEFAULT), value_type=TunableVariant( locked_args={'disabled': None}, residential=ResidentialLotArrivalBehavior.TunableFactory(), create_situation=CreateAndAddToSituation.TunableFactory(), add_to_background_situation=AddToBackgroundSituation. TunableFactory(), default='disabled'), tuning_group=GroupNames.TRIGGERS), 'player_requires_visitation_rights': OptionalTunable( description= 'If enabled, then lots of this venue type \n will require player Sims that are not on their home lot to go through \n the process of being greeted before they are\n given full rights to using the lot.\n ', tunable=TunableTuple( ungreeted=Situation.TunableReference( description= '\n The situation to create for ungreeted player sims on this lot.', display_name='Player Ungreeted Situation'), greeted=Situation .TunableReference( description= '\n The situation to create for greeted player sims on this lot.', display_name='Player Greeted Situation'))), 'zone_fixup': TunableVariant( description= '\n Specify what to do with a non resident NPC\n when the zone has to be fixed up on load. \n This fix up will occur if sim time or the\n active household has changed since the zone was last saved.\n ', residential=ResidentialZoneFixupForNPC.TunableFactory(), create_situation=CreateAndAddToSituation.TunableFactory(), add_to_background_situation=AddToBackgroundSituation. TunableFactory(), default='residential', tuning_group=GroupNames.SPECIAL_CASES), 'travel_interaction_name': TunableVariant( description= '\n Specify what name a travel interaction gets when this Venue is an\n adjacent lot.\n ', visit_residential=ResidentialTravelDisplayName.TunableFactory( description= '\n The interaction name for when the destination lot is a\n residence.\n ' ), visit_venue=TunableLocalizedStringFactory( description= '\n The interaction name for when the destination lot is a\n commercial venue.\n Tokens: 0:ActorSim\n Example: "Visit The Bar"\n ' ), tuning_group=GroupNames.SPECIAL_CASES), 'travel_with_interaction_name': TunableVariant( description= '\n Specify what name a travel interaction gets when this Venue is an\n adjacent lot.\n ', visit_residential=ResidentialTravelDisplayName.TunableFactory( description= '\n The interaction name for when the destination lot is a\n residence and the actor Sim is traveling with someone.\n ' ), visit_venue=TunableLocalizedStringFactory( description= '\n The interaction name for when the destination lot is a\n commercial venue and the actor is traveling with someone.\n Tokens: 0:ActorSim\n Example: "Visit The Bar With..."\n ' ), tuning_group=GroupNames.SPECIAL_CASES), 'venue_requires_front_door': Tunable( description= '\n True if this venue should run the front door generation code. \n If it runs, venue will have the ring doorbell interaction and \n its additional behavior.\n ', tunable_type=bool, default=False), 'automatic_objects': TunableList( description= '\n A list of objects that is required to exist on this venue (e.g. the\n mailbox). If any of these objects are missing from this venue, they\n will be auto-placed on zone load.', tunable=TunableTuple( description= "\n An item that is required to be present on this venue. The object's tag \n will be used to determine if any similar objects are present. If no \n similar objects are present, then the object's actual definition is used to \n create an object of this type.\n \n This is not the same as required objects tuning. Please read comments \n for both the fields.\n \n E.g. To require a mailbox to be present on a lot, tune a hypothetical basicMailbox \n here. The code will not trigger as long as a basicMailbox, fancyMailbox, or \n cheapMailbox are present on the lot. If none of them are, then a basicMailbox \n will be automatically created.\n ", default_value=TunableReference( manager=services.definition_manager(), description= 'The default object to use if no suitably tagged object is present on the lot.' ), tag=TunableEnumEntry(description='The tag to search for', tunable_type=tag.Tag, default=tag.Tag.INVALID))), 'hide_from_buildbuy_ui': Tunable( description= '\n If True, this venue type will not be available in the venue picker\n in build/buy.\n ', tunable_type=bool, default=False, export_modes=ExportModes.All), 'allows_fire': Tunable( description= '\n If True a fire can happen on this venue, \n otherwise fires will not spawn on this venue.\n ', tunable_type=bool, default=False), 'allow_rolestate_routing_on_navmesh': Tunable( description= '\n Allow all RoleStates routing permission on lot navmeshes of this\n venue type. This is particularly useful for outdoor venue types\n (lots with no walls), where it is awkward to have to "invite a sim\n in" before they may route on the lot, be called over, etc.\n \n This tunable overrides the "Allow Npc Routing On Active Lot"\n tunable of individual RoleStates.\n ', tunable_type=bool, default=False) } @classmethod def _verify_tuning_callback(cls): if cls.special_event_schedule is not None: for entry in cls.special_event_schedule.schedule_entries: while entry.situation.venue_situation_player_job is None: logger.error( 'Venue Situation Player Job {} tuned in Situation: {}', entry.situation.venue_situation_player_job, entry.situation) def __init__(self, **kwargs): self._active_background_event_id = None self._active_special_event_id = None self._background_event_schedule = None self._special_event_schedule = None def set_active_event_ids(self, background_event_id=None, special_event_id=None): self._active_background_event_id = background_event_id self._active_special_event_id = special_event_id @property def active_background_event_id(self): return self._active_background_event_id @property def active_special_event_id(self): return self._active_special_event_id def schedule_background_events(self, schedule_immediate=True): self._background_event_schedule = self.background_event_schedule( start_callback=self._start_background_event, schedule_immediate=False) if schedule_immediate: ( best_time_span, best_data_list ) = self._background_event_schedule.time_until_next_scheduled_event( services.time_service().sim_now, schedule_immediate=True) if best_time_span is not None and best_time_span == date_and_time.TimeSpan.ZERO: while True: for best_data in best_data_list: self._start_background_event( self._background_event_schedule, best_data) def schedule_special_events(self, schedule_immediate=True): self._special_event_schedule = self.special_event_schedule( start_callback=self._try_start_special_event, schedule_immediate=schedule_immediate) def _start_background_event(self, scheduler, alarm_data, extra_data=None): entry = alarm_data.entry situation = entry.situation situation_manager = services.get_zone_situation_manager() if self._active_background_event_id is not None and self._active_background_event_id in situation_manager: situation_manager.destroy_situation_by_id( self._active_background_event_id) situation_id = services.get_zone_situation_manager().create_situation( situation, user_facing=False, spawn_sims_during_zone_spin_up=True) self._active_background_event_id = situation_id def _try_start_special_event(self, scheduler, alarm_data, extra_data): entry = alarm_data.entry situation = entry.situation situation_manager = services.get_zone_situation_manager() if self._active_special_event_id is None: client_manager = services.client_manager() client = next(iter(client_manager.values())) invited_sim = client.active_sim active_sim_available = situation.is_situation_available( invited_sim) def _start_special_event(dialog): guest_list = None if dialog.accepted: start_user_facing = True guest_list = SituationGuestList() guest_info = SituationGuestInfo.construct_from_purpose( invited_sim.id, situation.venue_situation_player_job, SituationInvitationPurpose.INVITED) guest_list.add_guest_info(guest_info) else: start_user_facing = False situation_id = situation_manager.create_situation( situation, guest_list=guest_list, user_facing=start_user_facing) self._active_special_event_id = situation_id if not situation_manager.is_user_facing_situation_running( ) and active_sim_available: dialog = situation.venue_invitation_message( invited_sim, SingleSimResolver(invited_sim)) dialog.show_dialog( on_response=_start_special_event, additional_tokens=( situation.display_name, situation.venue_situation_player_job.display_name)) else: situation_id = situation_manager.create_situation( situation, user_facing=False) self._active_special_event_id = situation_id def shut_down(self): if self._background_event_schedule is not None: self._background_event_schedule.destroy() if self._special_event_schedule is not None: self._special_event_schedule.destroy() situation_manager = services.get_zone_situation_manager() if self._active_background_event_id is not None: situation_manager.destroy_situation_by_id( self._active_background_event_id) self._active_background_event_id = None if self._active_special_event_id is not None: situation_manager.destroy_situation_by_id( self._active_special_event_id) self._active_special_event_id = None @classmethod def lot_has_required_venue_objects(cls, lot): failure_reasons = [] for required_object_tuning in cls.required_objects: object_test = required_object_tuning.object object_list = object_test() num_objects = len(object_list) while num_objects < required_object_tuning.number: pass failure_message = None failure = len(failure_reasons) > 0 if failure: failure_message = '' for message in failure_reasons: failure_message += message + '\n' return (not failure, failure_message) def summon_npcs(self, npc_infos, purpose, host_sim_info=None): if self.npc_summoning_behavior is None: return summon_behavior = self.npc_summoning_behavior.get(purpose) if summon_behavior is None: summon_behavior = self.npc_summoning_behavior.get( venues.venue_constants.NPCSummoningPurpose.DEFAULT) if summon_behavior is None: return summon_behavior(npc_infos, host_sim_info) @classproperty def requires_visitation_rights(cls): return cls.player_requires_visitation_rights is not None @classproperty def player_ungreeted_situation_type(cls): if cls.player_requires_visitation_rights is None: return return cls.player_requires_visitation_rights.ungreeted @classproperty def player_greeted_situation_type(cls): if cls.player_requires_visitation_rights is None: return return cls.player_requires_visitation_rights.greeted
class DistancePlacementMixin: INSTANCE_TUNABLES = { 'facing_radius': TunableRange( description= '\n Facing constraint radius that will be used for the Sim looking \n at the target position where the object will be placed.\n ', tunable_type=int, default=1, minimum=1, tuning_group=GroupNames.CONSTRAINTS), 'facing_range': TunableAngle( description= '\n The max angle offset (in radians), the Sim can face away from the\n object.\n ', default=sims4.math.PI / 8), 'placement_distance': TunableInterval( description= '\n Distance in meters where the object will be placed.\n ', tunable_type=float, default_lower=0, default_upper=10, minimum=0, tuning_group=GroupNames.CONSTRAINTS), 'raytest_radius': TunableAngle( description= '\n Radius of the ray test check that will be used to validate if the\n Sim can see the target position where the object will be placed.\n ', default=0.1, minimum=0.1, tuning_group=GroupNames.CONSTRAINTS), 'raytest_offset': TunableInterval( description= "\n Offset in meters from the ground from where the raytest should start\n and stop.\n i.e. If you're testing if the Sim should see the target position \n from its eye level, you may want a value around the 1.7 (for an \n adult).\n ", tunable_type=float, default_lower=1.5, default_upper=1.5, minimum=0, tuning_group=GroupNames.CONSTRAINTS), 'thrown_object_actor_name': Tunable( description= '\n Offset in meters from the ground from where the raytest should \n end.\n ', tunable_type=str, default='carryObject', tuning_group=GroupNames.ANIMATION), 'bounce_jig': SocialJigExplicit.TunableFactory( description= "\n The jig to use for the object's bounce, where actor b is the\n object's final resting position, and actor a is where the initial\n bounce occurs, relative to the final position.\n \n The actor offsets will setup the distance of the bounce. If the\n offsets are both 0, then there will be no bounce. The offset for\n actor a will determine where the first bounce occurs.\n ", tuning_group=GroupNames.CONSTRAINTS), 'minimum_requirement_jig': OptionalTunable( description= '\n If enabled, we will use a jig to guarantee that we find a place to\n throw the ball from. From there, we find the furthest throw.\n ', tunable=SocialJigExplicit.TunableFactory( description= "\n The jig to use so we can find a good place to start our throw\n from. Doesn't really matter which actor A and B are, as long as\n there is enough distance between them for a throw. After we\n place this jig, we FGL from that location to find the longest\n throw possible.\n " ), tuning_group=GroupNames.CONSTRAINTS) } CONSTRAINT_RADIUS_BUFFER = 0.1 def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._starting_location = None self._distance_placement_transform = None self._bounce_transform = None self._routing_surface = None @flexmethod def _get_distance_placement_constraint(cls, inst, sim, target, participant_type=ParticipantType. Actor): inst_or_cls = inst if inst is not None else cls if inst is None or participant_type != ParticipantType.Actor: return ANYWHERE constraint = ANYWHERE if inst._distance_placement_transform is None or inst._bounce_transform is None: return Nowhere( "Distance Placement couldn't find a good location for the carry target." ) if inst_or_cls._starting_location is not None: constraint = constraint.intersect( interactions.constraints.Position( inst_or_cls._starting_location.transform.translation, routing_surface=inst_or_cls._starting_location. routing_surface)) else: constraint = constraint.intersect( interactions.constraints.Circle( inst_or_cls._distance_placement_transform.translation, inst_or_cls.placement_distance.upper_bound + DistancePlacementMixin.CONSTRAINT_RADIUS_BUFFER, inst_or_cls.sim.routing_surface, los_reference_point=DEFAULT)) constraint = constraint.intersect( interactions.constraints.Facing( facing_range=inst_or_cls.facing_range, target_position=inst_or_cls._bounce_transform.translation)) if constraint.valid: return constraint return Nowhere( "Distance Placement couldn't find a good location for the carry target." ) def _update_water_depth_requirements(self, sim_a, sim_b, interval, **kwargs): min_water_depth = kwargs[ 'min_water_depth'] if 'min_water_depth' in kwargs else None max_water_depth = kwargs[ 'max_water_depth'] if 'max_water_depth' in kwargs else None constraint_a = WaterDepthIntervalConstraint.create_water_depth_interval_constraint( sim_a, interval) constraint_b = WaterDepthIntervalConstraint.create_water_depth_interval_constraint( sim_b, interval) def safe_min(*args): ret = None for x in args: if ret is None: ret = x elif x is not None: ret = min(ret, x) return ret def safe_max(*args): ret = None for x in args: if ret is None: ret = x elif x is not None: ret = min(ret, x) return ret if interval == WaterDepthIntervals.SWIM: min_water_depth = safe_min(min_water_depth, constraint_a.get_min_water_depth(), constraint_b.get_min_water_depth()) max_water_depth = safe_max(max_water_depth, constraint_a.get_max_water_depth(), constraint_b.get_max_water_depth()) else: min_water_depth = safe_max(min_water_depth, constraint_a.get_min_water_depth(), constraint_b.get_min_water_depth()) max_water_depth = safe_min(max_water_depth, constraint_a.get_max_water_depth(), constraint_b.get_max_water_depth()) kwargs['min_water_depth'] = min_water_depth kwargs['max_water_depth'] = max_water_depth return kwargs def find_starting_location(self): if self.carry_target is not None and self.minimum_requirement_jig is not None: fgl_flags = FGLSearchFlag.STAY_IN_CONNECTED_CONNECTIVITY_GROUP | FGLSearchFlag.SHOULD_TEST_ROUTING | FGLSearchFlag.CALCULATE_RESULT_TERRAIN_HEIGHTS | FGLSearchFlag.DONE_ON_MAX_RESULTS check_on_lot = self.sim.is_on_active_lot() if check_on_lot: fgl_flags = fgl_flags | FGLSearchFlag.STAY_IN_LOT fgl_kwargs = { 'ignored_object_ids': {sim.id for sim in self.required_sims()}, 'positioning_type': JigPositioning.RelativeToSimA } loc_a = self.sim.location.duplicate() fgl_kwargs.update({'search_flags': fgl_flags}) loc_b = loc_a.duplicate() lot = services.current_zone().lot def find_a_starting_location(): for (transform_a, transform_b, routing_surface, _) in self.minimum_requirement_jig.get_transforms_gen( self.sim, self.carry_target, actor_loc=loc_a, target_loc=loc_b, fgl_kwargs=fgl_kwargs): if not check_on_lot or lot.is_position_on_lot( transform_a.translation): if not lot.is_position_on_lot(transform_b.translation): continue return sims4.math.Location(transform_a, routing_surface) else: return else: return use_pool_surface = loc_a.routing_surface.type == SurfaceType.SURFACETYPE_POOL or 0 < get_water_depth_at_location( loc_a) if use_pool_surface: interval = WaterDepthIntervals.SWIM if loc_a.routing_surface.type != SurfaceType.SURFACETYPE_POOL: self._routing_surface = SurfaceIdentifier( loc_a.routing_surface.primary_id, loc_a.routing_surface.secondary_id, SurfaceType.SURFACETYPE_POOL) loc_a = loc_a.clone(routing_surface=self._routing_surface) else: interval = WaterDepthIntervals.WALK fgl_kwargs = self._update_water_depth_requirements( self.sim, self.target, interval, **fgl_kwargs) start_loc = find_a_starting_location() if start_loc is not None: return start_loc return self.sim.location.duplicate() def setup_final_transforms(self, start_location, **fgl_kwargs): loc_b = start_location.clone( routing_surface=DEFAULT if self._routing_surface is None else self. _routing_surface) for (transform_a, transform_b, _, _) in self.bounce_jig.get_transforms_gen(self.sim, self.carry_target, actor_loc=start_location, target_loc=loc_b, fgl_kwargs=fgl_kwargs): self._distance_placement_transform = transform_a self._bounce_transform = transform_b break def _entered_pipeline(self): if self.carry_target is not None: fgl_flags = FGLSearchFlag.STAY_IN_CURRENT_BLOCK | FGLSearchFlag.STAY_IN_SAME_CONNECTIVITY_GROUP | FGLSearchFlag.SHOULD_TEST_ROUTING | FGLSearchFlag.CALCULATE_RESULT_TERRAIN_HEIGHTS | FGLSearchFlag.DONE_ON_MAX_RESULTS | FGLSearchFlag.SHOULD_RAYTEST fgl_kwargs = { 'raytest_radius': self.raytest_radius, 'raytest_start_offset': self.raytest_offset.lower_bound, 'raytest_end_offset': self.raytest_offset.upper_bound, 'ignored_object_ids': {sim.id for sim in self.required_sims()}, 'positioning_type': JigPositioning.RelativeToSimA } pick = self.context.pick if pick is not None and pick.routing_surface.type == SurfaceType.SURFACETYPE_POOL: swim_constraint = WaterDepthIntervalConstraint.create_water_depth_interval_constraint( self.target, WaterDepthIntervals.SWIM) pick_location = sims4.math.Location( sims4.math.Transform(pick.location), pick.routing_surface) if swim_constraint.is_location_water_depth_valid( pick_location): loc_a = pick_location self._routing_surface = pick.routing_surface else: self._routing_surface = SurfaceIdentifier( pick.routing_surface.primary_id, pick.routing_surface.secondary_id, SurfaceType.SURFACETYPE_WORLD) loc_a = pick_location.clone( routing_surface=self._routing_surface) fgl_kwargs.update({ 'search_flags': fgl_flags, 'restrictions': (sims4.geometry.RelativeFacingRange(self.sim.position, 0), ) }) self.setup_final_transforms(loc_a, **fgl_kwargs) else: self._starting_location = self.find_starting_location() if self._starting_location is not None: check_on_lot = self.sim.is_on_active_lot() if check_on_lot: fgl_flags = fgl_flags | FGLSearchFlag.STAY_IN_LOT loc_a = self._starting_location fgl_flags |= FGLSearchFlag.SPIRAL_INWARDS fgl_kwargs.update({ 'max_distance': self.placement_distance.upper_bound, 'min_distance': self.placement_distance.lower_bound, 'restrictions': (sims4.geometry.RelativeFacingRange( loc_a.transform.translation, 0), ), 'search_flags': fgl_flags }) self.setup_final_transforms(loc_a, **fgl_kwargs) return super()._entered_pipeline() def setup_asm_default(self, asm, *args, **kwargs): if self._distance_placement_transform is None: return False if self.carry_target is None: return False result = super().setup_asm_default(asm, *args, **kwargs) if not result: return result if not asm.set_actor_parameter( self.thrown_object_actor_name, self.carry_target, animation_constants.ASM_TARGET_TRANSLATION, self._distance_placement_transform.translation): return False if not asm.set_actor_parameter( self.thrown_object_actor_name, self.carry_target, animation_constants.ASM_TARGET_ORIENTATION, self._distance_placement_transform.orientation): return False throw_distance = (self._distance_placement_transform.translation - self.sim.position).magnitude_2d() bounce_distance = (self._distance_placement_transform.translation - self._bounce_transform.translation).magnitude_2d() asm.set_actor_parameter(self.thrown_object_actor_name, self.carry_target, 'ThrowDistance', throw_distance) asm.set_actor_parameter(self.thrown_object_actor_name, self.carry_target, 'BounceDistance', bounce_distance) if self._routing_surface is not None and self._routing_surface.type == routing.SurfaceType.SURFACETYPE_POOL: asm.set_actor_parameter(self.thrown_object_actor_name, self.carry_target, 'LandingSurface', POOL_LANDING_SURFACE) return True def _exited_pipeline(self, *args, **kwargs): if self._routing_surface is not None: self.carry_target.location = self.carry_target.location.clone( routing_surface=self._routing_surface) return super()._exited_pipeline(*args, **kwargs)
class AggregateSuperInteraction(SuperInteraction): INSTANCE_TUNABLES = { 'aggregated_affordances': TunableList( description= '\n A list of affordances composing this aggregate. Distance\n estimation will be used to break ties if there are multiple\n valid interactions at the same priority level.\n ', tunable=TunableTuple( description= '\n An affordance and priority entry.\n ', priority=Tunable( description= '\n The relative priority of this affordance compared to\n other affordances in this aggregate.\n ', tunable_type=int, default=0), affordance=SuperInteraction.TunableReference( description= '\n The aggregated affordance.\n ', pack_safe=True)), tuning_group=GroupNames.GENERAL), 'sim_to_push_affordance_on': TunableEnumEntry( description= '\n The Sim to push the affordance on. If this is Actor, the\n affordance will be pushed as a continuation of this.\n ', tunable_type=ParticipantType, default=ParticipantType.Actor, tuning_group=GroupNames.TRIGGERS), 'use_aggregated_affordance_constraints': Tunable( description= "\n If enabled, this interaction will pull it's constraints from the\n interaction constraints of the aggregated affordances. The benefit\n is that we are compatible with interactions we intend to run, even\n if they have constraints different from one another. This prevents\n us from having to add a bunch of tests to those affordances and a\n generic constraint here.\n ", tunable_type=bool, default=False, tuning_group=GroupNames.CONSTRAINTS) } _allow_user_directed = True def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._valid_aops = None @classproperty def affordances(cls): return (a.affordance.get_interaction_type() for a in cls.aggregated_affordances) @classmethod def _aops_sorted_gen(cls, target, **interaction_parameters): affordances = [] for aggregated_affordance in cls.aggregated_affordances: aop = AffordanceObjectPair(aggregated_affordance.affordance, target, aggregated_affordance.affordance, None, **interaction_parameters) affordances.append((aggregated_affordance.priority, aop)) return sorted(affordances, key=operator.itemgetter(0), reverse=True) @flexmethod def _get_tested_aops(cls, inst, target, context, **interaction_parameters): inst_or_cls = inst if inst is not None else cls if inst is not None and inst._valid_aops is not None: return inst._valid_aops aops_valid = [] cls._allow_user_directed = False for (priority, aop) in inst_or_cls._aops_sorted_gen(target, **interaction_parameters): test_result = aop.test(context) if test_result: if aop.affordance.allow_user_directed: cls._allow_user_directed = True aops_valid.append((aop, priority)) if inst is not None: inst._valid_aops = aops_valid return aops_valid @flexmethod def test(cls, inst, target=DEFAULT, context=DEFAULT, super_interaction=None, skip_safe_tests=False, **interaction_parameters): inst_or_cls = inst if inst is not None else cls result = super(__class__, inst_or_cls).test(target=target, context=context, super_interaction=super_interaction, skip_safe_tests=skip_safe_tests, **interaction_parameters) if result: target = target if target is not DEFAULT else inst.target context = context if context is not DEFAULT else inst.context context = context.clone_for_sim( cls.get_participant( participant_type=cls.sim_to_push_affordance_on, sim=context.sim, target=target)) valid_aops = inst_or_cls._get_tested_aops(target, context, **interaction_parameters) result = TestResult.TRUE if valid_aops else TestResult( False, 'No sub-affordances passed their tests.') return result @classmethod def consumes_object(cls): for affordance_tuple in cls.aggregated_affordances: if affordance_tuple.affordance.consumes_object(): return True return False @classproperty def allow_user_directed(cls): return cls._allow_user_directed @flexmethod def _constraint_gen(cls, inst, sim, target, participant_type=ParticipantType.Actor, **kwargs): inst_or_cls = cls if inst is None else inst yield from super(SuperInteraction, inst_or_cls)._constraint_gen( sim, target, participant_type=participant_type, **kwargs) if inst_or_cls.use_aggregated_affordance_constraints: aggregated_constraints = [] affordances = [] affordances = [ aop.super_affordance for (aop, _) in inst._valid_aops ] affordances = affordances if not inst is not None or not inst._valid_aops is not None or affordances else [ affordance_tuple.affordance for affordance_tuple in inst_or_cls.aggregated_affordances ] if not affordances: yield Nowhere for aggregated_affordance in affordances: intersection = ANYWHERE constraint_gen = aggregated_affordance.constraint_gen constraint_gen = super(SuperInteraction, aggregated_affordance)._constraint_gen for constraint in constraint_gen( sim, inst_or_cls.get_constraint_target(target), participant_type=participant_type, **kwargs): intersection = constraint.intersect(intersection) if not intersection.valid: continue aggregated_constraints.append(intersection) if aggregated_constraints: yield create_constraint_set( aggregated_constraints, debug_name='AggregatedConstraintSet') def _do_perform_gen(self, timeline): sim = self.get_participant(self.sim_to_push_affordance_on) if sim == self.context.sim: context = self.context.clone_for_continuation(self) else: context = context.clone_for_sim(sim) max_priority = None aops_valid = [] self._valid_aops = None valid_aops = self._get_tested_aops(self.target, context, **self.interaction_parameters) for (aop, priority) in valid_aops: if max_priority is not None: if priority < max_priority: break aops_valid.append(aop) max_priority = priority if not aops_valid: logger.warn( 'Failed to find valid super affordance in AggregateSuperInteraction: {}, did we not run its test immediately before executing it?', self) return ExecuteResult.NONE yield compatible_interactions = [] for aop in aops_valid: interaction_result = aop.interaction_factory(context) if not interaction_result: raise RuntimeError( 'Failed to generate interaction from aop {}. {} [rmccord]'. format(aop, interaction_result)) interaction = interaction_result.interaction if self.use_aggregated_affordance_constraints: if interactions.si_state.SIState.test_compatibility( interaction, force_concrete=True): compatible_interactions.append(interaction) compatible_interactions.append(interaction) if not compatible_interactions: return ExecuteResult.NONE yield interactions_by_distance = [] for interaction in compatible_interactions: if len(compatible_interactions) == 1: distance = 0 else: (distance, _, _) = interaction.estimate_distance() if distance is not None: interactions_by_distance.append((distance, interaction)) else: interactions_by_distance.append( (sims4.math.MAX_INT32, interaction)) (_, interaction) = min(interactions_by_distance, key=operator.itemgetter(0)) return AffordanceObjectPair.execute_interaction(interaction) yield
def __init__( self, description='Holds information about carrying and putting down an object.', **kwargs): super().__init__( put_down_tuning=TunableVariant(reference=TunableReference( description= '\n Tuning for how to score where a Sim might want to set an\n object down.\n ', manager=services.get_instance_manager( sims4.resources.Types.STRATEGY)), literal=TunablePutDownStrategy(). TunableFactory(), default='literal'), state_based_put_down_tuning=TunableMapping( description= '\n A mapping from a state value to a putdownstrategy. If the\n owning object is in any of the states tuned here, it will use\n that state\'s associated putdownstrategy in place of the one\n putdownstrategy tuned in the "put_down_tuning" field. If the\n object is in multiple states listed in this mapping, the\n behavior is undefined.\n ', key_type=TunableReference( description= '\n The state value this object must be in in order to use the\n associated putdownstrategy.\n ', manager=services.get_instance_manager( sims4.resources.Types.OBJECT_STATE)), value_type=TunableVariant(reference=TunableReference( description= '\n Tuning for how to score where a Sim might want to set\n an object down.\n ', manager=services.get_instance_manager( sims4.resources.Types.STRATEGY)), literal=TunablePutDownStrategy(). TunableFactory()), key_name='State', value_name='PutDownStrategy'), carry_affordances=OptionalTunable(TunableList( TunableReference( description= '\n The versions of the HoldObject affordance that this object\n supports.\n ', manager=services.affordance_manager())), disabled_name= 'use_default_affordances', enabled_name= 'use_custom_affordances'), provided_affordances=TunableList( description= '\n A list of affordances that are generated when a Sim holding\n this object selects another Sim to interact with. The generated\n interactions will target the selected Sim but will have this\n object set as their carry target.\n ', tunable=TunableReference( manager=services.affordance_manager())), constraint_pick_up=OptionalTunable( description= '\n A list of constraints that must be fulfilled in order to\n interact with this object.\n ', tunable=TunableList( tunable=interactions.constraints.TunableConstraintVariant( description= '\n A constraint that must be fulfilled in order to\n interact with this object.\n ' ))), allowed_hands=TunableVariant(locked_args={ 'both': (Hand.LEFT, Hand.RIGHT), 'left_only': (Hand.LEFT, ), 'right_only': (Hand.RIGHT, ) }, default='both'), holster_while_routing=Tunable( description= '\n If True, the Sim will holster the object before routing and\n unholster when the route is complete.\n ', tunable_type=bool, default=False), holster_compatibility=TunableAffordanceFilterSnippet( description= '\n Define interactions for which holstering this object is\n explicitly disallowed.\n \n e.g. The Scythe is tuned to be holster-incompatible with\n sitting, meaning that Sims will holster the Sctyhe when sitting.\n ' ), unholster_on_long_route_only=Tunable( description= '\n If True, then the Sim will not unholster this object (assuming\n it was previously holstered) unless a transition involving a\n long route is about to happen.\n \n If False, then the standard holstering rules apply.\n ', tunable_type=bool, default=False), prefer_owning_sim_inventory_when_not_on_home_lot=Tunable( description= "\n If checked, this object will highly prefer to be put into the\n owning Sim's inventory when being put down by the owning Sim on\n a lot other than their home lot.\n \n Certain objects, like consumables, should be exempt from this.\n ", tunable_type=bool, default=True), description=description, **kwargs)
class DramaNodeTest(HasTunableSingletonFactory, AutoFactoryInit, event_testing.test_base.BaseTest): FACTORY_TUNABLES = {'drama_nodes': TunableList(description='\n The types of drama nodes that we want to check.\n ', tunable=TunableReference(description='\n A Drama node type we want to check.\n ', manager=services.get_instance_manager(sims4.resources.Types.DRAMA_NODE), pack_safe=True)), 'check_scheduled_nodes': Tunable(description='\n Check against nodes that are scheduled, but not actively running.\n ', tunable_type=bool, default=True), 'check_active_nodes': Tunable(description='\n Check against nodes that are actively running.\n ', tunable_type=bool, default=True), 'exists': Tunable(description='\n If checked then this drama node will pass if a node meeting the requirements exists.\n Otherwise it will pass if there is not a node meeting the requirements.\n ', tunable_type=bool, default=True), 'receiver_sim': OptionalTunable(description='\n If enabled we will check that the receiver Sim is the tuned Sim.\n ', tunable=TunableEnumEntry(description='\n The Sim that we will make sure is the receiver Sim.\n ', tunable_type=ParticipantTypeSingleSim, default=ParticipantTypeSingleSim.TargetSim)), 'time_to_run': OptionalTunable(description='\n If enabled then we will check against the remaining time until the the drama node is scheduled to run.\n ', tunable=TunableTuple(threshold=TunableThreshold(description='\n A threshold to compare the amount of time left for this drma node to be run.\n ', value=TunableTimeSpanSingleton(description='\n The amount of time to compare against.\n '), default=sims4.math.Threshold(TimeSpan.ZERO, sims4.math.Operator.GREATER_OR_EQUAL.function)), additional_threshold=OptionalTunable(description='\n If enabled then we will have a second threshold to compare against.\n ', tunable=TunableThreshold(description='\n A threshold to compare the amount of time left for this drma node to be run.\n ', value=TunableTimeSpanSingleton(description='\n The amount of time to compare against.\n '), default=sims4.math.Threshold(TimeSpan.ZERO, sims4.math.Operator.GREATER_OR_EQUAL.function)))))} def get_expected_args(self): if self.receiver_sim is None: return {} else: return {'receiver_sim': self.receiver_sim} @cached_test def __call__(self, receiver_sim=None): if not self.drama_nodes: if self.exists: return TestResult(False, 'No drama node exists meeting the requirements.', tooltip=self.tooltip) return TestResult.TRUE drama_scheduler = services.drama_scheduler_service() if self.check_scheduled_nodes and self.check_active_nodes: drama_node_gen = drama_scheduler.all_nodes_gen elif self.check_scheduled_nodes: drama_node_gen = drama_scheduler.scheduled_nodes_gen elif self.check_active_nodes: drama_node_gen = drama_scheduler.active_nodes_gen else: if self.exists: return TestResult(False, 'No drama node exists meeting the requirements.', tooltip=self.tooltip) return TestResult.TRUE if receiver_sim is not None: receiver_sim = next(iter(receiver_sim)) now = services.time_service().sim_now for drama_node in drama_node_gen(): if type(drama_node) not in self.drama_nodes: continue if receiver_sim is not None and drama_node.get_receiver_sim_info() is not receiver_sim: continue if self.time_to_run is not None: time_to_node = drama_node.selected_time - now if not self.time_to_run.threshold.compare(time_to_node): continue if self.time_to_run.additional_threshold is not None and not self.time_to_run.additional_threshold.compare(time_to_node): continue else: if self.exists: return TestResult.TRUE return TestResult(False, 'Drama node meeting the requirements exists when we are asking for non-existence.', tooltip=self.tooltip) if self.exists: return TestResult(False, 'No drama node exists meeting the requirements.', tooltip=self.tooltip) return TestResult.TRUE
class NextFestivalTest(HasTunableSingletonFactory, AutoFactoryInit, event_testing.test_base.BaseTest): FACTORY_TUNABLES = {'drama_node': OptionalTunable(description='\n If enabled then we will check a specific type of festival drama\n node otherwise we will look at all of the festival drama nodes.\n ', tunable=TunableReference(description='\n Reference to the festival drama node that we want to be the\n next one.\n ', manager=services.get_instance_manager(sims4.resources.Types.DRAMA_NODE), class_restrictions=('FestivalDramaNode',)), enabled_by_default=True), 'negate': Tunable(description='\n If enabled this test will pass if the next festival is not one of\n the tuned nodes.\n ', tunable_type=bool, default=False)} def get_expected_args(self): return {} @cached_test def __call__(self): drama_scheduler = services.drama_scheduler_service() best_time = None best_nodes = [type(node) for node in drama_scheduler.active_nodes_gen() if node.drama_node_type == DramaNodeType.FESTIVAL] if not best_nodes: for node in drama_scheduler.scheduled_nodes_gen(): if node.drama_node_type != DramaNodeType.FESTIVAL: continue new_time = node._selected_time - services.time_service().sim_now if best_time is None or new_time < best_time: best_nodes = [type(node)] best_time = new_time elif new_time == best_time: best_nodes.append(type(node)) if not best_nodes: if self.negate: return TestResult.TRUE return TestResult(False, 'No scheduled Festivals.') if self.drama_node is None or self.drama_node in best_nodes: if self.negate: return TestResult(False, 'Next scheduled Festival matches requested.') return TestResult.TRUE if self.negate: return TestResult.TRUE return TestResult(False, "Next scheduled Festival doesn't match requested.")
def __init__(self, **kwargs): super().__init__(threshold=TunableThreshold(value=Tunable(int, -100, description='The value of the threshold that the commodity is compared against'), description='Threshold for which the sim experiences motive failure'), failure_interactions=TunableList(description="\n A list of interactions to be pushed when the Sim's\n commodity fails. Only the first one whose test\n passes will run.\n ", tunable=TunableReference(services.get_instance_manager(sims4.resources.Types.INTERACTION), description='The interaction to be pushed on the sim when the commodity fails.')), description='The behaviors for the commodity failing.', **kwargs)
def __init__(self, **kwargs): super().__init__(positive_single_arrow=Tunable(float, 1, description='If the change rate for commodity is between this value and less than second arrow value, a single arrow will show up during commodity change.'), positive_double_arrow=Tunable(float, 20, description='If the change rate for commodity is between this value and less than triple arrow value, a double arrow will show up during commodity change.'), positive_triple_arrow=Tunable(float, 30, description='If the change rate for commodity is above this value then triple arrows will show up during commodity change.'), negative_single_arrow=Tunable(float, -1, description='If the change rate for commodity is between this value and less than second arrow value, a single arrow will show up during commodity change.'), negative_double_arrow=Tunable(float, -20, description='If the change rate for commodity is between this value and less than triple arrow value, a double arrow will show up during commodity change.'), negative_triple_arrow=Tunable(float, -30, description='If the change rate for commodity is above this value then triple arrows will show up during commodity change.'), **kwargs)
class DoCommand(XevtTriggeredElement, HasTunableFactory): ARG_TYPE_PARTICIPANT = 0 ARG_TYPE_LITERAL = 1 ARG_TYPE_TAG = 3 @staticmethod def _verify_tunable_callback(source, *_, command, **__): command_name = command.split(' ', 1)[0] command_restrictions = get_command_restrictions(command_name) command_type = get_command_type(command_name) if command_restrictions is None or command_type is None: logger.error('Command {} specified in {} does not exist.', command_name, source) else: if command_restrictions & CommandRestrictionFlags.RESTRICT_SAVE_UNLOCKED and source.allow_while_save_locked: logger.error( 'Command {} specified in {} is unavailable during save lock. The interaction should not be available during save lock either.', command_name, source) if command_type != CommandType.Live and not source.debug and not source.cheat: logger.error( 'Command {} is {} command tuned on non-debug interaction {}. The command type should be CommandType.Live.', command_name, command_type, source) if command_type < CommandType.Cheat and source.cheat: logger.error( 'Command {} is {} command tuned on cheat interaction {}. The command type should be CommandType.Cheat or above.', command_name, command_type, source) FACTORY_TUNABLES = { 'command': Tunable(description='\n The command to run.\n ', tunable_type=str, default=None), 'arguments': TunableList( description= "\n The arguments for this command. Arguments will be added after the\n command in the order they're listed here.\n ", tunable=TunableVariant( description= '\n The argument to use. In most cases, the ID of the participant\n will be used.\n ', participant=TunableTuple( description= '\n An argument that is a participant in the interaction. The\n ID will be used as the argument for the command.\n ', argument=TunableEnumEntry( description= '\n The participant argument. The ID will be used in the\n command.\n ', tunable_type=ParticipantType, default=ParticipantTypeSingle.Object), locked_args={'arg_type': ARG_TYPE_PARTICIPANT}), string=TunableTuple( description= "\n An argument that's a string.\n ", argument=Tunable( description= '\n The string argument.\n ', tunable_type=str, default=None), locked_args={'arg_type': ARG_TYPE_LITERAL}), number=TunableTuple( description= '\n An argument that is a number. This can be a float or an int.\n ', argument=Tunable( description= '\n The number argument.\n ', tunable_type=float, default=0), locked_args={'arg_type': ARG_TYPE_LITERAL}), tag=TunableTuple( description= '\n An argument that is a tag.\n ', argument=TunableTag( description= '\n The tag argument.\n ' ), locked_args={'arg_type': ARG_TYPE_TAG}), boolean=TunableTuple( description= '\n An argument that is a boolean.\n ', argument=Tunable( description= '\n The number argument.\n ', tunable_type=bool, default=True), locked_args={'arg_type': ARG_TYPE_LITERAL}))), 'verify_tunable_callback': _verify_tunable_callback } def _do_behavior(self): full_command = self.command for arg in self.arguments: if arg.arg_type == self.ARG_TYPE_PARTICIPANT: for participant in self.interaction.get_participants( arg.argument): if hasattr(participant, 'id'): full_command += ' {}'.format(participant.id) else: full_command += ' {}'.format(participant) else: if arg.arg_type == self.ARG_TYPE_LITERAL: full_command += ' {}'.format(arg.argument) elif arg.arg_type == self.ARG_TYPE_TAG: full_command += ' {}'.format(int(arg.argument)) else: logger.error( 'Trying to run the Do Command element with an invalid arg type, {}.', arg.arg_type, owner='trevor') return False elif arg.arg_type == self.ARG_TYPE_LITERAL: full_command += ' {}'.format(arg.argument) elif arg.arg_type == self.ARG_TYPE_TAG: full_command += ' {}'.format(int(arg.argument)) else: logger.error( 'Trying to run the Do Command element with an invalid arg type, {}.', arg.arg_type, owner='trevor') return False client_id = services.client_manager().get_first_client_id() commands.execute(full_command, client_id) return True
def __init__(self, **kwargs): super().__init__(threshold_value=Tunable(int, -80, description='Threshold for which below the sim is in commodity distress'), buff=TunableBuffReference(description='Buff that gets added to the sim when they are in the commodity distress state.'), distress_interaction=TunableReference(services.get_instance_manager(sims4.resources.Types.INTERACTION), description='The interaction to be pushed on the sim when the commodity reaches distress.'), incompatible_interactions=TunableAffordanceFilterSnippet(), replacement_affordance=TunableReference(services.get_instance_manager(sims4.resources.Types.INTERACTION), description='The affordance that will be pushed when the commodity '), priority=Tunable(int, 0, description='The relative priority of the override interaction being played over others.'), description='The behaviors that show that the commodity is in distress.', **kwargs)
def __init__(self, target_default=ParticipantType.Object, locked_args={}, carry_target_default=ParticipantType.Object, class_restrictions=(), **kwargs): super().__init__(tunable=TunableTuple( description= '\n A continuation entry.\n ', affordance=TunableReference( description= '\n The affordance to push as a continuation on the specified\n actor Sim.\n ', manager=services.affordance_manager(), class_restrictions=class_restrictions, pack_safe=True), si_affordance_override=TunableReference( description= "\n When the tuned affordance is a mixer for a different SI, use\n this to specify the mixer's appropriate SI. This is useful for\n pushing socials.\n ", manager=services.affordance_manager(), allow_none=True), actor=TunableEnumEntry( description= '\n The Sim on which the affordance is pushed.\n ', tunable_type=ParticipantType, default=ParticipantType.Actor), target=TunableEnumEntry( description= '\n The participant the affordance will target.\n ', tunable_type=ParticipantType, default=target_default), carry_target=OptionalTunable( description= '\n If enabled, specify a carry target for this continuation.\n ', tunable=TunableEnumEntry( description= '\n The participant the affordance will set as a carry target.\n ', tunable_type=ParticipantType, default=carry_target_default)), inventory_carry_target=TunableVariant( description= '\n Item in inventory (of continuations actor) to use as carry\n target for continuation if carry target is None\n ', object_with_tag=CraftTaggedItemFactory( locked_args={ 'check_type': TunableContinuation.TAGGED_ITEM }), object_with_definition=TunableTuple(definition=TunableReference( description= '\n The exact object definition to look for inside\n inventory.\n ', manager=services.definition_manager()), locked_args={ 'check_type': TunableContinuation. ITEM_DEFINITION }), object_with_base_definition= TunableTuple(definition=TunableReference( description= '\n The base definition to look for inside inventory.\n Objects that redirect (like counters) will match if base\n definition is the same.\n ', manager=services.definition_manager()), locked_args={ 'check_type': TunableContinuation.ITEM_TUNING_ID }), locked_args={'None': None}, default='None'), preserve_preferred_object=Tunable( description= "\n If checked, the pushed interaction's preferred objects are\n determined by the current preferred objects.\n \n If unchecked, the transition sequence would not award bonuses to\n any specific part.\n ", tunable_type=bool, default=True), locked_args=locked_args), **kwargs)
class JigGroup(SideGroup): __qualname__ = 'JigGroup' INSTANCE_TUNABLES = { 'jig': TunableReference( description= '\n The jig to use for finding a place to do the social.', manager=services.definition_manager()), 'participant_slot_map': TunableMapping( description= '\n The slot index mapping on the jig keyed by participant type', key_type=TunableEnumEntry(ParticipantType, ParticipantType.Actor), value_type=Tunable( description='The slot index for the participant type', tunable_type=int, default=0)), 'cancel_delay': TunableSimMinute( description= '\n Amount of time a jig group must be inactive before it will shut down.\n ', default=15) } DEFAULT_SLOT_INDEX_ACTOR = 1 DEFAULT_SLOT_INDEX_TARGET = 0 @classmethod def _get_jig_transforms(cls, initiating_sim, target_sim, picked_object=None, participant_slot_overrides=None): slot_map = cls.participant_slot_map if participant_slot_overrides is None else participant_slot_overrides actor_slot_index = slot_map.get(ParticipantType.Actor, cls.DEFAULT_SLOT_INDEX_ACTOR) target_slot_index = slot_map.get(ParticipantType.TargetSim, cls.DEFAULT_SLOT_INDEX_TARGET) if picked_object is not None and picked_object.carryable_component is None: try: return get_two_person_transforms_for_jig( picked_object.definition, picked_object.transform, picked_object.routing_surface, actor_slot_index, target_slot_index) except RuntimeError: pass return fgl_and_get_two_person_transforms_for_jig( cls.jig, initiating_sim, actor_slot_index, target_sim, target_slot_index) def __init__(self, *args, si=None, target_sim=None, participant_slot_overrides=None, **kwargs): super().__init__(si=si, target_sim=target_sim, *args, **kwargs) self._sim_transform_map = {} self.geometry = None initiating_sim = si.sim if initiating_sim is None or target_sim is None: logger.error( 'JigGroup {} cannot init with initial sim {()} or target sim {()}', self.__name__, initiating_sim, target_sim) return picked_object = si.picked_object self.participant_slot_overrides = participant_slot_overrides (sim_transform, target_transform, routing_surface) = self._get_jig_transforms( initiating_sim, target_sim, picked_object=picked_object, participant_slot_overrides=self.participant_slot_overrides) self._jig_transform = target_transform if target_transform is not None: self._jig_polygon = placement.get_placement_footprint_polygon( target_transform.translation, target_transform.orientation, routing_surface, self.jig.get_footprint(0)) else: self._jig_polygon = None self._sim_transform_map[initiating_sim] = sim_transform self._sim_transform_map[target_sim] = target_transform if target_transform is None: self._constraint = Nowhere() return target_forward = target_transform.transform_vector( sims4.math.FORWARD_AXIS) self._set_focus(target_transform.translation, target_forward, routing_surface) self._initialize_constraint(notify=True) @classmethod def _verify_tuning_callback(cls): if cls.jig is None: logger.error('JigGroup {} must have a jig tuned.', cls.__name__) @classmethod def make_constraint_default(cls, actor, target_sim, position, routing_surface, participant_type=ParticipantType.Actor, picked_object=None, participant_slot_overrides=None): (actor_transform, target_transform, routing_surface) = cls._get_jig_transforms( actor, target_sim, picked_object=picked_object, participant_slot_overrides=participant_slot_overrides) if actor_transform is None or target_transform is None: return Nowhere() if participant_type == ParticipantType.Actor: constraint_transform = actor_transform elif participant_type == ParticipantType.TargetSim: constraint_transform = target_transform else: return Anywhere() return interactions.constraints.Transform( constraint_transform, routing_surface=routing_surface, debug_name='JigGroupConstraint') def _relocate_group_around_focus(self, *args, **kwargs): return False @property def group_radius(self): if self._jig_polygon is not None: return self._jig_polygon.radius() return 0 @property def jig_polygon(self): return self._jig_polygon @property def jig_transform(self): return self._jig_transform def get_constraint(self, sim): transform = self._sim_transform_map.get(sim, None) if transform is not None: return interactions.constraints.Transform( transform, routing_surface=self.routing_surface) if sim in self._sim_transform_map: return Nowhere() return Anywhere() def _make_constraint(self, *args, **kwargs): if self._constraint is None: constraints = [ interactions.constraints.Transform( t, routing_surface=self.routing_surface) for t in self._sim_transform_map.values() ] self._constraint = create_constraint_set( constraints) if constraints else Anywhere() return self._constraint _create_adjustment_alarm = socials.group.SocialGroup._create_adjustment_alarm def _consider_adjusting_sim(self, sim=None, initial=False): if not initial: for sim in self: for _ in self.queued_mixers_gen(sim): pass if self.time_since_interaction().in_minutes() < self.cancel_delay: return self.shutdown(FinishingType.NATURAL)
class PlayEffect(distributor.ops.ElementDistributionOpMixin, HasTunableFactory, AutoFactoryInit): JOINT_NAME_CURRENT_POSITION = 1899928870 FACTORY_TUNABLES = { 'effect_name': Tunable(description= '\n The name of the effect to play.\n ', tunable_type=str, default=''), 'joint_name': OptionalTunable( description= '\n Specify if the visual effect is attached to a slot and, if so, which\n slot.\n ', tunable=TunableStringHash32( description= '\n The name of the slot this effect is attached to.\n ', default='_FX_'), enabled_by_default=True, enabled_name='Slot', disabled_name='Current_Position', disabled_value=JOINT_NAME_CURRENT_POSITION), 'play_immediate': Tunable( description= '\n If checked, this effect will be triggered immediately, nothing\n will block.\n\n ex. VFX will be played immediately while \n the Sim is routing or animating.\n ', tunable_type=bool, default=False) } def __init__(self, target, effect_name='', joint_name=0, target_actor_id=0, target_joint_name_hash=0, mirror_effect=False, auto_on_effect=False, target_joint_offset=None, play_immediate=False, callback_event_id=None, store_target_position=False, transform_override=None, **kwargs): super().__init__(effect_name=effect_name, joint_name=joint_name, play_immediate=play_immediate, immediate=play_immediate, **kwargs) self.target = target if target is not None: if target.inventoryitem_component is not None: forward_to_owner_list = target.inventoryitem_component.forward_client_state_change_to_inventory_owner if forward_to_owner_list: if StateChange.VFX in forward_to_owner_list: inventory_owner = target.inventoryitem_component.inventory_owner if inventory_owner is not None: self.target = inventory_owner if target.crafting_component is not None: effect_name = target.crafting_component.get_recipe_effect_overrides( effect_name) self.target_transform = target.transform if target is not None else transform_override self.effect_name = effect_name self.auto_on_effect = auto_on_effect self.target_actor_id = target_actor_id self.target_joint_name_hash = target_joint_name_hash self.mirror_effect = mirror_effect self._stop_type = SOFT_TRANSITION self.target_joint_offset = target_joint_offset self.immediate = play_immediate self.callback_event_id = callback_event_id self.store_target_position = store_target_position def __repr__(self): return standard_angle_repr(self, self.effect_name) @property def _is_relative_to_transform(self): return self.joint_name == self.JOINT_NAME_CURRENT_POSITION def _on_target_location_changed(self, *_, **__): self.stop(immediate=True) self.start() def start(self, *_, **__): if self.target is None: logger.error( 'Attempting to attach VFX without a target. Perhaps you mean to use start_one_shot()', owner='rmccord') if self._is_relative_to_transform: self.target.register_on_location_changed( self._on_target_location_changed) if not self._is_valid_target(): return if not self.is_attached: self.attach(self.target) logger.info('VFX {} on {} START'.format(self.effect_name, self.target)) def start_one_shot(self): if self.target is not None and not self.target.is_terrain: distributor.ops.record(self.target, self) else: Distributor.instance().add_op_with_no_owner(self) def stop(self, *_, immediate=False, **kwargs): if self.target is None or not self.target.valid_for_distribution: return if self._is_relative_to_transform: self.target.unregister_on_location_changed( self._on_target_location_changed) if self.is_attached: if immediate: self._stop_type = HARD_TRANSITION else: self._stop_type = SOFT_TRANSITION self.detach() def _is_valid_target(self): if not self.target.valid_for_distribution: zone = services.current_zone() if zone is not None: zone_spin_up_service = zone.zone_spin_up_service if zone_spin_up_service is None: logger.callstack( 'zone_spin_up_service was None in PlayEffect._is_valid_target(), for effect/target: {}/{}', self, self.target, owner='johnwilkinson', level=sims4.log.LEVEL_ERROR) return False elif not zone_spin_up_service.is_finished: return False return True def detach(self, *objects): super().detach(*objects) if services.current_zone().is_zone_shutting_down: return op = StopVFX(self.target.id, self.actor_id, stop_type=self._stop_type, immediate=self.immediate) distributor.ops.record(self.target, op) logger.info('VFX {} on {} STOP'.format(self.effect_name, self.target)) def write(self, msg): start_msg = VFXStart() if self.target is not None: start_msg.object_id = self.target.id start_msg.effect_name = self.effect_name start_msg.actor_id = self.actor_id start_msg.joint_name_hash = self.joint_name start_msg.target_actor_id = self.target_actor_id start_msg.target_joint_name_hash = self.target_joint_name_hash start_msg.mirror_effect = self.mirror_effect start_msg.auto_on_effect = self.auto_on_effect if self.target_joint_offset is not None: start_msg.target_joint_offset.x = self.target_joint_offset.x start_msg.target_joint_offset.y = self.target_joint_offset.y start_msg.target_joint_offset.z = self.target_joint_offset.z if self.callback_event_id is not None: start_msg.callback_event_id = self.callback_event_id if self._is_relative_to_transform: if self.store_target_position or self.target is None: transform = self.target_transform else: transform = self.target.transform start_msg.transform.translation.x = transform.translation.x start_msg.transform.translation.y = transform.translation.y start_msg.transform.translation.z = transform.translation.z start_msg.transform.orientation.x = transform.orientation.x start_msg.transform.orientation.y = transform.orientation.y start_msg.transform.orientation.z = transform.orientation.z start_msg.transform.orientation.w = transform.orientation.w self.serialize_op(msg, start_msg, protocols.Operation.VFX_START)
class DisplaySnippetPickerSuperInteraction(PickerSuperInteraction): INSTANCE_TUNABLES = { 'picker_dialog': TunablePickerDialogVariant( description='\n The item picker dialog.\n ', available_picker_flags=ObjectPickerTuningFlags.ITEM, tuning_group=GroupNames.PICKERTUNING), 'subject': TunableEnumFlags( description= "\n To whom 'loot on selected' should be applied.\n ", enum_type=ParticipantTypeSim, default=ParticipantTypeSim.Actor, tuning_group=GroupNames.PICKERTUNING), 'display_snippets': TunableList( description= '\n The list of display snippets available to select and paired loot actions\n that will run if selected.\n ', tunable=_PickerDisplaySnippet.TunableFactory( description= '\n Display snippet available to select.\n ' ), tuning_group=GroupNames.PICKERTUNING), 'display_snippet_text_tokens': LocalizationTokens.TunableFactory( description= '\n Localization tokens passed into the display snippet text fields.\n \n When acting on the individual items within the snippet list, the \n following text tokens will be appended to this list of tokens (in \n order):\n 0: snippet instance display name\n 1: snippet instance display description\n 2: snippet instance display tooltip\n 3: tokens tuned alongside individual snippets within the snippet list\n ', tuning_group=GroupNames.PICKERTUNING), 'display_snippet_text_overrides': OptionalTunable( description= '\n If enabled, display snippet text overrides for all snippets \n to be displayed in the picker. \n \n Can be used together with the display snippet text tokens to \n act as text wrappers around the existing snippet display data.\n ', tunable=_DisplaySnippetTextOverrides.TunableFactory( description= '\n Display snippet text overrides for all snippets to be displayed\n in the picker. \n \n Can be used together with the display snippet text tokens to \n act as text wrappers around the existing snippet display data.\n ' ), tuning_group=GroupNames.PICKERTUNING), 'continuations': TunableList( description= '\n List of continuations to push when a snippet is selected.\n \n ID of the snippet will be the PickedItemID participant in the \n continuation.\n ', tunable=TunableContinuation(), tuning_group=GroupNames.PICKERTUNING), 'run_continuations_on_no_selection': Tunable( description= '\n Checked, runs continuations regardless if anything is selected.\n Unchecked, continuations are only run if something is selected.\n ', tunable_type=bool, default=True, tuning_group=GroupNames.PICKERTUNING) } @classmethod def has_valid_choice(cls, target, context, **kwargs): snippet_count = 0 for _ in cls.picker_rows_gen(target, context, **kwargs): snippet_count += 1 if snippet_count >= cls.picker_dialog.min_selectable: return True return False def _run_interaction_gen(self, timeline): self._show_picker_dialog(self.sim, target_sim=self.sim) return True yield @flexmethod def picker_rows_gen(cls, inst, target, context, **kwargs): inst_or_cls = inst if inst is not None else cls target = target if target is not DEFAULT else inst.target context = context if context is not DEFAULT else inst.context resolver = InteractionResolver(cls, inst, target=target, context=context) general_tokens = inst_or_cls.display_snippet_text_tokens.get_tokens( resolver) overrides = inst_or_cls.display_snippet_text_overrides index = 0 for display_snippet_data in inst_or_cls.display_snippets: display_snippet = display_snippet_data.display_snippet resolver = InteractionResolver( cls, inst, target=target, context=context, picked_item_ids={display_snippet.guid64}) test_result = display_snippet_data.test(resolver) is_enable = test_result.result if is_enable or test_result.tooltip is not None: snippet_default_tokens = ( display_snippet.display_name(*general_tokens) if display_snippet.display_name is not None else None, display_snippet.display_description(*general_tokens) if display_snippet.display_description is not None else None, display_snippet.display_tooltip(*general_tokens) if display_snippet.display_tooltip is not None else None) snippet_additional_tokens = display_snippet_data.display_snippet_text_tokens.get_tokens( resolver) tokens = general_tokens + snippet_default_tokens + snippet_additional_tokens display_snippet = overrides( display_snippet_data.display_snippet) tooltip = None if not overrides is not None or test_result.tooltip is None else lambda *_, tooltip=test_result.tooltip: tooltip( *tokens) tooltip = None if display_snippet.display_tooltip is None else lambda *_, tooltip=display_snippet.display_tooltip: tooltip( *tokens) row = BasePickerRow( is_enable=is_enable, name=display_snippet.display_name(*tokens), icon=display_snippet.display_icon, tag=index, row_description=display_snippet.display_description( *tokens), row_tooltip=tooltip) yield row index += 1 def _on_display_snippet_selected(self, picked_choice, **kwargs): resolver = self.get_resolver(**kwargs) for loot_on_selected in self.display_snippets[ picked_choice].loot_on_selected: loot_on_selected.apply_to_resolver(resolver) def on_choice_selected(self, picked_choice, **kwargs): if picked_choice is None: if self.run_continuations_on_no_selection: for continuation in self.continuations: self.push_tunable_continuation(continuation) return display_snippet = self.display_snippets[picked_choice].display_snippet picked_item_set = {display_snippet.guid64} self._on_display_snippet_selected(picked_choice, picked_item_ids=picked_item_set) for continuation in self.continuations: self.push_tunable_continuation(continuation, picked_item_ids=picked_item_set)
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
class SocialMixerInteraction(SocialInteractionMixin, MixerInteraction): __qualname__ = 'SocialMixerInteraction' REMOVE_INSTANCE_TUNABLES = ('basic_reserve_object', 'basic_focus') basic_reserve_object = None GENDER_PREF_CONTENT_SCORE_PENALTY = Tunable( description= '\n Penalty applied to content score when the social fails the gender preference test.\n ', tunable_type=int, default=-1500) INSTANCE_TUNABLES = { 'base_score': Tunable( description= ' \n Base score when determining the content set value of this mixer\n based on other mixers of the super affordance. This is the base\n value used before any modification to content score.\n \n Modification to the content score for this affordance can come from\n topics and moods\n \n USAGE: If you would like this mixer to more likely show up no matter\n the topic and mood ons the sims tune this value higher.\n \n Formula being used to determine the autonomy score is Score =\n Avg(Uc, Ucs) * W * SW, Where Uc is the commodity score, Ucs is the\n content set score, W is the weight tuned the on mixer, and SW is the\n weight tuned on the super interaction.\n ', tuning_group=GroupNames.AUTONOMY, tunable_type=int, default=0), 'social_context_preference': TunableMapping( description= '\n A mapping of social contexts that will adjust the content score for\n this mixer interaction. This is used conjunction with base_score.\n ', tuning_group=GroupNames.AUTONOMY, key_type=SocialContextBit.TunableReference(), value_type=Tunable(tunable_type=float, default=0)), 'relationship_bit_preference': TunableMapping( description= '\n A mapping of relationship bits that will adjust the content score for\n this mixer interaction. This is used conjunction with base_score.\n ', tuning_group=GroupNames.AUTONOMY, key_type=RelationshipBit.TunableReference(), value_type=Tunable(tunable_type=float, default=0)), 'trait_preference': TunableMapping( description= '\n A mapping of traits that will adjust the content score for\n this mixer interaction. This is used conjunction with base_score.\n ', tuning_group=GroupNames.AUTONOMY, key_type=Trait.TunableReference(), value_type=Tunable(tunable_type=float, default=0)), 'buff_preference': TunableMapping( description= '\n A mapping of buffs that will adjust the content score for\n this mixer interaction. This is used conjunction with base_score.\n ', tuning_group=GroupNames.AUTONOMY, key_type=Buff.TunableReference(), value_type=Tunable(tunable_type=float, default=0)), 'test_gender_preference': Tunable( description= '\n If this is set, a gender preference test will be run between\n the actor and target sims. If it fails, the social score will be\n modified by a large negative penalty tuned with the tunable:\n GENDER_PREF_CONTENT_SCORE_PENALTY\n ', tuning_group=GroupNames.AUTONOMY, tunable_type=bool, default=False), 'outcome': TunableOutcome(allow_multi_si_cancel=True) } def __init__(self, target, context, *args, **kwargs): super().__init__(target, context, *args, **kwargs) @classmethod def _add_auto_constraint(cls, participant_type, auto_constraint): 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.' ) @classproperty def is_social(cls): return True @property def social_group(self): if self.super_interaction is not None: return self.super_interaction.social_group @staticmethod def _tunable_tests_enabled(): return tunable_tests_enabled @classmethod def get_base_content_set_score(cls): return cls.base_score @classmethod def _test(cls, target, context, *args, **kwargs): if context.sim is target: return TestResult(False, 'Social Mixer Interactions cannot target self!') pick_target = context.pick.target if context.source == context.SOURCE_PIE_MENU else None if target is None and context.sim is pick_target: return TestResult(False, 'Social Mixer Interactions cannot target self!') return MixerInteraction._test(target, context, *args, **kwargs) @classmethod def get_score_modifier(cls, sim, target): if cls.test_gender_preference: gender_pref_test = GenderPreferenceTest(ParticipantType.Actor, ParticipantType.TargetSim, ignore_reciprocal=True) resolver = DoubleSimResolver(sim.sim_info, target.sim_info) result = resolver(gender_pref_test) if not result: return cls.GENDER_PREF_CONTENT_SCORE_PENALTY social_context_preference = 0 relationship_bit_preference = 0 trait_preference = 0 buff_preference = 0 if target is not None: sims = set( itertools.chain.from_iterable( group for group in sim.get_groups_for_sim_gen() if target in group)) if sims: social_context = SocialContextTest.get_overall_short_term_context_bit( *sims) else: relationship_track = sim.relationship_tracker.get_relationship_prevailing_short_term_context_track( target.id) if relationship_track is not None: social_context = relationship_track.get_active_bit() else: social_context = None social_context_preference = cls.social_context_preference.get( social_context, 0) if cls.relationship_bit_preference: relationship_bit_preference = sum( cls.relationship_bit_preference.get(rel_bit, 0) for rel_bit in sim.relationship_tracker.get_all_bits( target_sim_id=target.id)) if cls.trait_preference: trait_preference = sum( cls.trait_preference.get(trait, 0) for trait in sim.trait_tracker.equipped_traits) if cls.buff_preference: buff_preference = sum( score for (buff, score) in cls.buff_preference.items() if sim.has_buff(buff)) score_modifier = super().get_score_modifier( sim, target ) + social_context_preference + relationship_bit_preference + trait_preference + buff_preference return score_modifier def should_insert_in_queue_on_append(self): if super().should_insert_in_queue_on_append(): return True if self.super_affordance is None: logger.error( '{} being added to queue without a super interaction or super affordance', self) return False ui_group_tag = self.super_affordance.visual_type_override_data.group_tag if ui_group_tag == tag.Tag.INVALID: return False for si in self.sim.si_state: while si.visual_type_override_data.group_tag == ui_group_tag: return True return False def get_asm(self, *args, **kwargs): return Interaction.get_asm(self, *args, **kwargs) def perform_gen(self, timeline): if self.social_group is None: raise AssertionError( 'Social mixer interaction {} has no social group. [bhill]'. format(self)) result = yield super().perform_gen(timeline) return result def build_basic_elements(self, sequence=()): sequence = super().build_basic_elements(sequence=sequence) if self.super_interaction.social_group is not None: listen_animation_factory = self.super_interaction.listen_animation else: listen_animation_factory = None for group in self.sim.get_groups_for_sim_gen(): si = group.get_si_registered_for_sim(self.sim) while si is not None: listen_animation_factory = si.listen_animation break if listen_animation_factory is not None: for sim in self.required_sims(): if sim is self.sim: pass sequence = listen_animation_factory(sim.animation_interaction, sequence=sequence) sequence = with_skippable_animation_time((sim, ), sequence=sequence) def defer_cancel_around_sequence_gen(s, timeline): deferred_sis = [] for sim in self.required_sims(): while not (sim is self.sim or self.social_group is None): if sim not in self.social_group: pass sis = self.social_group.get_sis_registered_for_sim(sim) while sis: deferred_sis.extend(sis) with self.super_interaction.cancel_deferred(deferred_sis): result = yield element_utils.run_child(timeline, s) return result sequence = functools.partial(defer_cancel_around_sequence_gen, sequence) if self.target_type & TargetType.ACTOR: return element_utils.build_element(sequence) if self.target_type & TargetType.TARGET and self.target is not None: sequence = self.social_group.with_target_focus( self.sim, self.sim, self.target, sequence) elif self.social_group is not None: sequence = self.social_group.with_social_focus( self.sim, self.sim, self.required_sims(), sequence) else: for social_group in self.sim.get_groups_for_sim_gen(): sequence = social_group.with_social_focus( self.sim, self.sim, self.required_sims(), sequence) communicable_buffs = collections.defaultdict(list) for sim in self.required_sims(): for buff in sim.Buffs: while buff.communicable: communicable_buffs_sim = communicable_buffs[sim] communicable_buffs_sim.append(buff) for (sim, communicable_buffs_sim) in communicable_buffs.items(): for other_sim in self.required_sims(): if other_sim is sim: pass resolver = DoubleSimResolver(sim.sim_info, other_sim.sim_info) for buff in communicable_buffs_sim: buff.communicable.apply_to_resolver(resolver) return element_utils.build_element(sequence) def cancel_parent_si_for_participant(self, participant_type, finishing_type, cancel_reason_msg, **kwargs): social_group = self.social_group if social_group is None: return participants = self.get_participants(participant_type) for sim in participants: while sim is not None: social_group.remove(sim) group_tag = self.super_interaction.visual_type_override_data.group_tag if group_tag != Tag.INVALID: for si in self.sim.si_state: while si is not self.super_interaction and si.visual_type_override_data.group_tag == group_tag: social_group = si.social_group if social_group is not None: while True: for sim in participants: while sim in social_group: social_group.remove(sim) @flexmethod def get_participants(cls, inst, participant_type, sim=DEFAULT, **kwargs) -> set: inst_or_cls = inst if inst is not None else cls result = super(MixerInteraction, inst_or_cls).get_participants(participant_type, sim=sim, **kwargs) result = set(result) sim = inst.sim if sim is DEFAULT else sim if inst is not None and inst.social_group is None and ( participant_type & ParticipantType.AllSims or participant_type & ParticipantType.Listeners): if inst is not None and inst.target_type & TargetType.GROUP: while True: for other_sim in itertools.chain( *list(sim.get_groups_for_sim_gen())): if other_sim is sim: pass if other_sim.ignore_group_socials( excluded_group=inst.social_group): pass result.add(other_sim) return tuple(result) def _trigger_interaction_start_event(self): super()._trigger_interaction_start_event() target_sim = self.get_participant(ParticipantType.TargetSim) if target_sim is not None: services.get_event_manager().process_event( test_events.TestEvent.InteractionStart, sim_info=target_sim.sim_info, interaction=self, custom_keys=self.get_keys_to_process_events()) self._register_target_event_auto_update() def required_resources(self): resources = super().required_resources() resources.add(self.social_group) return resources
def __init__(self, **kwargs): super().__init__(value=Tunable(description='\n lower bound value of the commodity state\n ', tunable_type=int, default=0, export_modes=ExportModes.All), buff=TunableBuffReference(description='\n Buff that will get added to sim when commodity is at\n this current state.\n ', reload_dependent=True), buff_add_threshold=OptionalTunable(TunableThreshold(description='\n When enabled, buff will not be added unless threshold\n has been met. Value for threshold must be within this\n commodity state.\n ')), icon=TunableResourceKey(description='\n Icon that is displayed for the current state of this\n commodity.\n ', default='PNG:missing_image', resource_types=sims4.resources.CompoundTypes.IMAGE, export_modes=ExportModes.All), fill_level=TunableEnumEntry(description='\n If set, this will determine how to color the motive bar.\n ', tunable_type=MotiveFillColorLevel, default=MotiveFillColorLevel.NO_FILL, export_modes=ExportModes.All), data_description=TunableLocalizedString(description='\n Localized description of the current commodity state.\n ', export_modes=ExportModes.All), fill_color=TunableColor.TunableColorRGBA(description='\n Fill color for motive bar\n ', export_modes=(ExportModes.ClientBinary,)), background_color=TunableColor.TunableColorRGBA(description='\n Background color for motive bar\n ', export_modes=(ExportModes.ClientBinary,)), tooltip_icon_list=TunableList(description='\n A list of icons to show in the tooltip of this\n commodity state.\n ', tunable=TunableResourceKey(description='\n Icon that is displayed what types of objects help\n solve this motive.\n ', default='PNG:missing_image', resource_types=sims4.resources.CompoundTypes.IMAGE), export_modes=(ExportModes.ClientBinary,)), loot_list_on_enter=TunableList(description='\n List of loots that will be applied when commodity\n value enters this state if owner of the commodity is a sim.\n ', tunable=TunableReference(services.get_instance_manager(sims4.resources.Types.ACTION))), **kwargs)
class FestivalRunningTest(HasTunableSingletonFactory, AutoFactoryInit, event_testing.test_base.BaseTest): FACTORY_TUNABLES = {'drama_node': OptionalTunable(description='\n If enabled then we will check a specific type of festival drama\n node otherwise we will look at all of the festival drama nodes.\n ', tunable=TunableReference(description='\n Reference to the festival drama node that we want to be running.\n ', manager=services.get_instance_manager(sims4.resources.Types.DRAMA_NODE), class_restrictions=('FestivalDramaNode',)), enabled_by_default=True), 'check_if_on_festival_street': OptionalTunable(description="\n If enabled, test against if the player is on the festival's street.\n ", tunable=Tunable(description="\n If checked, this test will pass only if the player is on the\n festival's street. If unchecked, the test will pass only if the\n player is not on the festival street.\n ", tunable_type=bool, default=True)), 'valid_time_blocks': TunableTuple(description='\n Festival drama nodes have a tunable pre-festival duration that\n delay festival start to some point after the drama node has\n started. For example, if the festival drama node has a pre-festival\n duration of 2 hours and the drama node runs at 8am, the festival\n will not start until 10am.\n\n By default, this test passes if the festival drama node is running,\n regardless if the festival is in its pre-festival duration. This\n tuning changes that behavior.\n ', pre_festival=Tunable(description='\n If the festival is currently in its pre-festival duration,\n test can pass if this is checked and fails if unchecked.\n ', tunable_type=bool, default=True), running=Tunable(description='\n If the festival is running (it is past its pre-festival\n duration), test can pass if this is checked and fails if\n unchecked.\n ', tunable_type=bool, default=True)), 'negate': Tunable(description='\n If enabled this test will pass if no festivals of the tuned\n requirements are running.\n ', tunable_type=bool, default=False)} test_events = (TestEvent.FestivalStarted,) def get_expected_args(self): return {} @cached_test def __call__(self): drama_scheduler = services.drama_scheduler_service() for node in drama_scheduler.active_nodes_gen(): if self.drama_node is None: if node.drama_node_type != DramaNodeType.FESTIVAL: continue elif type(node) is not self.drama_node: continue if self.check_if_on_festival_street is not None and self.check_if_on_festival_street != node.is_on_festival_street(): continue if node.is_during_pre_festival(): if not self.valid_time_blocks.pre_festival: continue elif not self.valid_time_blocks.running: continue if self.negate: return TestResult(False, 'Drama nodes match the required conditions.') return TestResult.TRUE if self.negate: return TestResult.TRUE return TestResult(False, 'No drama nodes match the required conditions.')
class ObjectRewardsOperation(BaseLootOperation): __qualname__ = 'ObjectRewardsOperation' FACTORY_TUNABLES = { 'object_rewards': TunableVariant( description= '\n Object rewards when running the loot. Rewards objects will be created\n and sent to the tuned inventory.\n Spawnerdata reference will load the reward data from the interaction \n spawner tuning inside the spawner component of the participant selected\n Rewardsdata tuning will allow you to tune the object rewards directly \n ', spawnerdata_reference=SpawnerInteractionTuning.TunableFactory(), rewardsdata_tuning=ObjectRewardsTuning.TunableFactory()), 'notification': OptionalTunable( description= '\n If enabled, a notification will be displayed when this object reward\n is granted to a Sim.\n ', tunable=TunableUiDialogNotificationSnippet( description= '\n The notification to display when this object reward is granted\n to the Sim. There is one additional token provided: a string\n representing a bulleted list of all individual rewards granted.\n ' )), 'force_family_inventory': Tunable( description= '\n If Enabled, the rewards object(s) will be put in the family \n inventory no matter what. If not enabled, the object will try to\n be added to the sim inventory, if that is not possible it will be\n added to the family inventory as an automatic fallback.', tunable_type=bool, default=False), 'transfer_stored_sim_info_to_reward': OptionalTunable( description= "\n If enabled, the tuned participant will transfer its stored sim info\n into the rewards created. This is mostly used for the cow plant\n life essence, which will store the sim info of the sim from which\n the life essence was drained.\n \n Ex: For cow plant's milk life essence, we want to transfer the dead\n sim's sim info from the cow plant to the created essence drink.\n ", tunable=TunableEnumEntry( description= '\n The participant of this interaction which has a \n StoredSimInfoComponent. The stored sim info will be transferred\n to the created rewards and will then be removed from the source.\n ', tunable_type=ParticipantType, default=ParticipantType.Object)) } def __init__(self, object_rewards, notification, force_family_inventory, transfer_stored_sim_info_to_reward, subject=None, **kwargs): super().__init__(**kwargs) self._object_rewards = object_rewards self._notification = notification self._force_family_inventory = force_family_inventory self._transfer_stored_sim_info_to_reward = transfer_stored_sim_info_to_reward def _create_object_rewards(self, obj_weight_pair, obj_counter, resolver): obj_result = weighted_random_item(obj_weight_pair) for obj_reward in obj_result: created_obj = create_object( obj_reward, init=None, post_add=lambda *args: self._place_object(resolver=resolver, *args)) while created_obj is not None: obj_counter[obj_reward] += 1 def _apply_to_subject_and_target(self, subject, target, resolver): if subject.is_npc: return obj_counter = Counter() if self._object_rewards.spawn_type == ObjectRewardsTuning.SPAWNER_REWARD: participant = resolver.get_participant( self._object_rewards.spawner_participant) if participant is not None: weighted_data = participant.interaction_spawner_data() if weighted_data is not None: self._create_object_rewards(weighted_data, obj_counter, resolver) elif self._object_rewards.spawn_type == ObjectRewardsTuning.TUNABLE_REWARD: for _ in range(self._object_rewards.quantity): weight_pairs = [(data.weight, data.reward) for data in self._object_rewards.reward_objects ] self._create_object_rewards(weight_pairs, obj_counter, resolver) if obj_counter and self._notification is not None: obj_names = [ LocalizationHelperTuning.get_object_count(count, obj) for (obj, count) in obj_counter.items() ] dialog = self._notification(subject, resolver=resolver) dialog.show_dialog( additional_tokens=(LocalizationHelperTuning.get_bulleted_list( None, *obj_names), )) return True def _place_object(self, created_object, resolver=None): actor = resolver.get_participant( ParticipantType.Actor).get_sim_instance( allow_hidden_flags=ALL_HIDDEN_REASONS) created_object.update_ownership(actor, make_sim_owner=False) if self._transfer_stored_sim_info_to_reward is not None: stored_sim_source = resolver.get_participant( self._transfer_stored_sim_info_to_reward) sim_id = stored_sim_source.get_stored_sim_id() if sim_id is not None: created_object.add_dynamic_component( types.STORED_SIM_INFO_COMPONENT.instance_attr, sim_id=sim_id) stored_sim_source.remove_component( types.STORED_SIM_INFO_COMPONENT.instance_attr) created_object.update_object_tooltip() if self._force_family_inventory or actor.inventory_component.can_add( created_object): if actor.inventory_component.player_try_add_object(created_object): return build_buy.move_object_to_household_inventory(created_object)
class TimeUntilFestivalTest(HasTunableSingletonFactory, AutoFactoryInit, event_testing.test_base.BaseTest): FACTORY_TUNABLES = {'drama_node': OptionalTunable(description='\n If enabled then we will check a specific type of festival drama\n node otherwise we will look at any of the festival drama nodes.\n ', tunable=TunableReference(description='\n Reference to the festival drama node that we want to test.\n ', manager=services.get_instance_manager(sims4.resources.Types.DRAMA_NODE), class_restrictions=('FestivalDramaNode',)), enabled_by_default=True), 'max_time': Tunable(description='\n Maximum time in hours between when the test occurs to the start of\n the festival in order for the test to return true.\n ', tunable_type=float, default=18.0), 'negate': Tunable(description='\n If enabled this test will pass if the requested festival will not\n start within the specified time.\n ', tunable_type=bool, default=False)} def get_expected_args(self): return {} @cached_test def __call__(self): drama_scheduler = services.drama_scheduler_service() best_time = None for node in drama_scheduler.scheduled_nodes_gen(): if node.drama_node_type != DramaNodeType.FESTIVAL: continue if not self.drama_node is None: if self.drama_node is type(node): new_time = node.get_time_remaining() if not best_time is None: if new_time < best_time: best_time = new_time best_time = new_time new_time = node.get_time_remaining() if not best_time is None: if new_time < best_time: best_time = new_time best_time = new_time if best_time is None: if not self.negate: return TestResult(False, 'No scheduled Festivals of type {}.', self.drama_node, tooltip=self.tooltip) elif best_time.in_hours() < self.max_time: if self.negate: return TestResult(False, 'Next scheduled Festival is within specified time', tooltip=self.tooltip) elif not self.negate: return TestResult(False, "Next scheduled Festival isn't within specified time", tooltip=self.tooltip) return TestResult.TRUE
class LotteryDramaNode(BaseDramaNode): INSTANCE_TUNABLES = { 'payout': Tunable( description= "\n The payout of the lottery to the winning Sim's household.\n ", tunable_type=int, default=1000000), 'lottery_event': TunableEnumEntry( description= '\n The event that triggers the active household being added to the\n lottery.\n ', tunable_type=TestEvent, default=TestEvent.Invalid, invalid_enums=(TestEvent.Invalid, )), 'minimum_sims': TunableRange( description= '\n The minimum number of sims that we want to trigger a lottery\n for. If not enough households have signed up for the lottery we\n will select random non-played sims to fill up the lottery\n pool.\n ', tunable_type=int, default=100, minimum=1), 'end_time': TunableTimeOfWeek( description= '\n The time that this Drama Node is going to end.\n ' ), 'winning_sim_loot': LootActions.TunableReference( description= '\n Loot action applied to the Winning Sim if they are in the active\n household when the lottery completes.\n ' ), 'losing_sim_loot': LootActions.TunableReference( description= '\n Loot action applied to losing Sims if they are in the active\n household when the lottery completes.\n ' ), 'notification': TunableUiDialogNotificationSnippet( description= '\n The notification that we will display to explain the winner of the\n lottery.\n ' ) } @classproperty def simless(cls): return True @classproperty def persist_when_active(cls): return True def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._end_alarm_handle = None self._lottery_sims = set() def cleanup(self, from_service_stop=False): super().cleanup(from_service_stop=from_service_stop) if self._end_alarm_handle is not None: alarms.cancel_alarm(self._end_alarm_handle) self._lottery_sims.clear() services.get_event_manager().unregister_single_event( self, self.lottery_event) def handle_event(self, sim_info, event, resolver): if sim_info is None: return self._lottery_sims.add(sim_info.sim_id) def _check_lottery_sim_criteria(self, sim_info): if sim_info.lod == SimInfoLODLevel.MINIMUM: return False if sim_info.is_teen_or_younger: return False if sim_info.household.hidden: return False elif sim_info.household.is_player_household: return False return True def _end_lottery(self, _): try: if not self._lottery_sims: return lottery_candidates = [] active_household_candidates = [] sim_info_manager = services.sim_info_manager() for sim_id in self._lottery_sims: sim_info = sim_info_manager.get(sim_id) if sim_info is None: continue lottery_candidates.append(sim_info) if sim_info.is_selectable: active_household_candidates.append(sim_info) if len(lottery_candidates) < self.minimum_sims: sims_to_get = self.minimum_sims - len(lottery_candidates) additional_candidates = [ sim_info for sim_info in sim_info_manager.values() if self._check_lottery_sim_criteria(sim_info) ] if len(additional_candidates) < sims_to_get: lottery_candidates.extend(additional_candidates) else: lottery_candidates.extend( random.sample(additional_candidates, sims_to_get)) winning_sim_info = random.choice(lottery_candidates) winning_sim_info.household.funds.add( self.payout, Consts_pb2.FUNDS_HOLIDAY_LOTTERY) notification = self.notification(services.active_sim_info()) notification.show_dialog(additional_tokens=(winning_sim_info, )) if winning_sim_info.is_selectable: resolver = SingleSimResolver(winning_sim_info) self.winning_sim_loot.apply_to_resolver(resolver) else: for sim_info in active_household_candidates: resolver = SingleSimResolver(sim_info) self.losing_sim_loot.apply_to_resolver(resolver) finally: services.drama_scheduler_service().complete_node(self.uid) def _setup_lottery(self): time = create_date_and_time(days=self.end_time.day, hours=self.end_time.hour, minutes=self.end_time.minute) time_until_end = services.time_service().sim_now.time_to_week_time( time) self._end_alarm_handle = alarms.add_alarm(self, time_until_end, self._end_lottery, cross_zone=True) services.get_event_manager().register_single_event( self, self.lottery_event) def _run(self): self._setup_lottery() return DramaNodeRunOutcome.SUCCESS_NODE_INCOMPLETE def resume(self): self._setup_lottery() def _save_custom_data(self, writer): writer.write_uint64s(LOTTERY_CANIDATES_TOKEN, self._lottery_sims) def _load_custom_data(self, reader): self._lottery_sims = set( reader.read_uint64s(LOTTERY_CANIDATES_TOKEN, ())) return True
class AggregateMixerInteraction(MixerInteraction): INSTANCE_TUNABLES = { 'aggregated_affordances': TunableList( description= '\n A list of affordances composing this aggregate. A random one\n will be chosen from sub-action weights if multiple interactions\n pass at the same priority.\n ', tunable=TunableTuple( description= '\n An affordance and priority entry.\n ', priority=Tunable( description= '\n The relative priority of this affordance compared to\n other affordances in this aggregate.\n ', tunable_type=int, default=0), affordance=MixerInteraction.TunableReference( description= '\n The aggregated affordance.\n ', pack_safe=True)), tuning_group=GroupNames.GENERAL) } _allow_user_directed = True @classmethod def _aops_sorted_gen(cls, target, context, super_interaction=DEFAULT, **interaction_parameters): affordances = [] source_interaction = context.sim.posture.source_interaction if super_interaction == DEFAULT else super_interaction for aggregated_affordance in cls.aggregated_affordances: aop = AffordanceObjectPair(aggregated_affordance.affordance, target, source_interaction.affordance, source_interaction, **interaction_parameters) affordances.append((aggregated_affordance.priority, aop)) return sorted(affordances, key=operator.itemgetter(0), reverse=True) @classmethod def _test(cls, target, context, **interaction_parameters): result = super()._test(target, context, **interaction_parameters) if not result: return result cls._allow_user_directed = False context = context.clone_for_sim(sim=context.sim) for (_, aop) in cls._aops_sorted_gen(target, context, **interaction_parameters): result = aop.test(context) if result: if aop.affordance.allow_user_directed: cls._allow_user_directed = True return result return TestResult(False, 'No sub-affordances passed their tests.') @classmethod def consumes_object(cls): for aggregated_affordance in cls.aggregated_affordances: if aggregated_affordance.affordance.consumes_object(): return True return False @classproperty def allow_user_directed(cls): return cls._allow_user_directed def _do_perform_gen(self, timeline): context = self.context.clone_for_continuation(self) max_priority = None aops_valid = [] invalid_aops_with_result = [] for (priority, aop) in self._aops_sorted_gen( self.target, context, super_interaction=self.super_interaction, **self.interaction_parameters): if max_priority is not None: if priority < max_priority: break test_result = aop.test(context) if test_result: aops_valid.append(aop) max_priority = priority else: invalid_aops_with_result.append((aop, test_result)) if not aops_valid: logger.error( 'Failed to find valid mixer affordance in AggregateMixerInteraction: {}, did we not run its test immediately before executing it?\n{}', self, invalid_aops_with_result, owner='rmccord') return ExecuteResult.NONE yield interactions_by_weight = [] for aop in aops_valid: interaction_result = aop.interaction_factory(context) if not interaction_result: raise RuntimeError( 'Failed to generate interaction from aop {}. {} [rmccord]'. format(aop, interaction_result)) interaction = interaction_result.interaction if len(aops_valid) == 1: weight = 0 else: weight = interaction.affordance.calculate_autonomy_weight( context.sim) interactions_by_weight.append((weight, interaction)) if not interactions_by_weight: return ExecuteResult.NONE yield (_, interaction) = max(interactions_by_weight, key=operator.itemgetter(0)) return AffordanceObjectPair.execute_interaction(interaction) yield
( TunableWeatherForecastListReference, TunableWeatherForecastListSnippet ) = define_snippet( 'weather_forcast_list', TunableList(tunable=TunableTuple( description= '\n A tuple of forecast and weight.\n ', forecast=WeatherForecast.TunableReference( description= '\n The weather forecast.\n ', pack_safe=True), weight=Tunable( description= '\n Weight of this forecast being selected.\n ', tunable_type=int, default=1)))) ( TunableWeatherSeasonalForecastsReference, TunableWeatherSeasonalForecastsSnippet ) = define_snippet( 'weather_seasonal_forecasts', TunableMapping( key_type=TunableEnumEntry( description='\n The part of the season.\n ', tunable_type=SeasonSegment, default=SeasonSegment.MID), value_type=TunableWeatherForecastListReference( description= '\n Potential forecasts for this part of the season.\n ',
class RingDoorbellSuperInteraction(SuperInteraction): INSTANCE_TUNABLES = { '_nobody_home_failure_notification': UiDialogNotification.TunableFactory( description= '\n Notification that displays if no one was home when they tried\n to ring the doorbell.\n ' ), '_bad_relationship_failure_notification': UiDialogNotification.TunableFactory( description= "\n Notification that displays if there wasn't high enough\n relationship with any of the household members when they\n tried to ring the doorbell.\n " ), '_success_notification': UiDialogNotification.TunableFactory( description= '\n Notification that displays if the user succeeded in becoming\n greeted when they rang the doorbell.\n ' ), '_relationship_test': TunableRelationshipTest( description= '\n The Relationship test ran between the sim running the\n interaction and all of the npc family members to see if they\n are allowed in.\n ', locked_args={ 'subject': ParticipantType.Actor, 'target_sim': ParticipantType.TargetSim }), '_always_allow_greeting': Tunable( description= '\n If set to true, we will always allow the sim to be greeted.\n ', tunable_type=bool, default=False) } def _get_owner_sim_infos(self): owner_household_or_travel_group = services.household_manager().get( services.current_zone().lot.owner_household_id) if owner_household_or_travel_group is None: owner_household_or_travel_group = services.travel_group_manager( ).get_travel_group_by_zone_id(services.current_zone_id()) if owner_household_or_travel_group is None: return owner_sim_infos = tuple(owner_household_or_travel_group) return owner_sim_infos def _make_greeted(self): resolver = self.get_resolver() dialog = self._success_notification(self.sim, resolver) dialog.show_dialog() services.get_zone_situation_manager().make_waiting_player_greeted( self.sim) self._try_make_always_welcomed(self.sim) def _try_make_always_welcomed(self, sim): if any( sim.sim_info.has_trait(trait) for trait in VisitingTuning.ALWAYS_WELCOME_TRAITS): current_household = services.owning_household_of_active_lot() if current_household is None: logger.error( 'Current household is None when trying to run the ring doorbell interaction for visiting sim {}', sim) return current_household.add_always_welcome_sim(sim.sim_info.id) def _show_nobody_home_dialog(self): resolver = self.get_resolver() dialog = self._nobody_home_failure_notification(self.sim, resolver) dialog.show_dialog() def _try_to_be_invited_in(self): if self._always_allow_greeting: self._make_greeted() return owner_sim_infos = self._get_owner_sim_infos() if owner_sim_infos is None: self._show_nobody_home_dialog() return occupants = tuple(sim_info for sim_info in owner_sim_infos if sim_info.is_at_home) num_occupants = len(occupants) for sim_info in occupants: if sim_info.is_pet: num_occupants -= 1 else: sim = sim_info.get_sim_instance( allow_hidden_flags=ALL_HIDDEN_REASONS) if not sim is None: if sim.has_running_and_queued_interactions_with_liability( interactions.rabbit_hole.RABBIT_HOLE_LIABILTIY): num_occupants -= 1 num_occupants -= 1 if num_occupants == 0: self._show_nobody_home_dialog() return for occupant in occupants: relationship_resolver = DoubleSimResolver(self.sim.sim_info, occupant) if relationship_resolver(self._relationship_test): self._make_greeted() return dialog = self._bad_relationship_failure_notification( self.sim, resolver) dialog.show_dialog() def _post_perform(self): super()._post_perform() self.add_exit_function(self._try_to_be_invited_in)
class WeatherForecast(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager( Types.WEATHER_FORECAST)): INSTANCE_TUNABLES = { 'calendar_icon': TunableIcon( description= '\n The small icon for this forecast.\n ', export_modes=ExportModes.All), 'calendar_icon_large': TunableIcon( description= '\n The large icon for this forecast.\n ', export_modes=ExportModes.All), 'calendar_icon_mascot': TunableIcon( description= '\n Optional icon to use as the forecast mascot in the calendar.\n ', allow_none=True, export_modes=ExportModes.All), 'forecast_description': TunableLocalizedString( description= '\n The description for this forecast.\n ', export_modes=ExportModes.All), 'forecast_name': TunableLocalizedString( description= '\n The name for this forecast.\n ', export_modes=ExportModes.All), 'prescribed_weather_type': OptionalTunable( description= '\n The types of prescribed weather this forecast counts as\n ', tunable=TunableTuple( rain=Tunable( description= '\n If checked this forecast will be unavailable if rain is disabled\n ', tunable_type=bool, default=False), storm=Tunable( description= '\n If checked this forecast will be unavailable if storm is disabled\n ', tunable_type=bool, default=False), snow=Tunable( description= '\n If checked this forecast will be unavailable if snow is disabled\n ', tunable_type=bool, default=False), blizzard=Tunable( description= '\n If checked this forecast will be unavailable if blizzard is disabled\n ', tunable_type=bool, default=False))), 'weather_event_time_blocks': TunableMapping( description= '\n The weather events that make up this forecast. Key is hour of day\n that event would start, value is a list of potential events\n ', key_type=Tunable(tunable_type=int, default=0), value_type=TunableList( description= '\n List of the weather events that can occur in this time block\n ', tunable=TunableTuple( description= '\n A tuple of information for the weather event.\n ', weather_event=WeatherEvent.TunableReference( description= '\n The weather event.\n ', pack_safe=True), duration=TunableInterval( description= '\n Minimum and maximum time, in sim hours, this event can last.\n ', tunable_type=float, default_lower=1, default_upper=4), weight=Tunable( description= '\n Weight of this event being selected.\n ', tunable_type=int, default=1)))), 'weather_ui_override': TunableMapping( description= '\n If set, this overrides the weather type that is shown for the\n specified group.\n ', key_type=TunableEnumEntry( tunable_type=WeatherTypeGroup, default=WeatherTypeGroup.UNGROUPED, invalid_enums=(WeatherTypeGroup.UNGROUPED, )), value_type=TunableEnumEntry( tunable_type=WeatherType, default=WeatherType.UNDEFINED, invalid_enums=(WeatherType.UNDEFINED, )), tuning_group=GroupNames.SPECIAL_CASES) } @classmethod def get_weather_event(cls): weather_schedule = [] for (beginning_hour, event_list) in cls.weather_event_time_blocks.items(): weather_schedule.append((beginning_hour, event_list)) weather_schedule.sort(key=operator.itemgetter(0)) time_of_day = services.time_service().sim_now hour_of_day = time_of_day.hour() entry = weather_schedule[-1] weather_events = entry[1] for entry in weather_schedule: if entry[0] <= hour_of_day: weather_events = entry[1] else: break weighted_events = [(weather_event.weight, weather_event) for weather_event in weather_events] chosen_weather_event = random.weighted_random_item(weighted_events) return (chosen_weather_event.weather_event, chosen_weather_event.duration.random_float()) @classmethod def is_snowy(cls): if cls.prescribed_weather_type is None: return False return cls.prescribed_weather_type.snow or cls.prescribed_weather_type.blizzard @classmethod def is_rainy(cls): if cls.prescribed_weather_type is None: return False return cls.prescribed_weather_type.rain or cls.prescribed_weather_type.storm @classmethod def is_forecast_supported(cls, options, snow_safe, rain_safe): prescribed_weather_type = cls.prescribed_weather_type if prescribed_weather_type is None: return True if prescribed_weather_type.rain and ( not rain_safe or options[PrecipitationType.RAIN] == WeatherOption.WEATHER_DISABLED): return False if prescribed_weather_type.snow and ( not snow_safe or options[PrecipitationType.SNOW] == WeatherOption.WEATHER_DISABLED): return False if prescribed_weather_type.storm and (not rain_safe or options[PrecipitationType.RAIN] == WeatherOption.DISABLE_STORMS): return False elif prescribed_weather_type.blizzard and ( not snow_safe or options[PrecipitationType.SNOW] == WeatherOption.DISABLE_STORMS): return False return True
class InventoryStorage: UI_SORT_TYPES = TunableList( description= "\n A list of gameplay-based sort types used in the sim's inventory in the UI.\n ", tunable=TunableTuple( description= '\n Data that defines this sort for the inventory UI.\n ', sort_name=TunableLocalizedString( description= '\n The name displayed in the UI for this sort type.\n ' ), object_data=TunableVariant( description= '\n The object data that determines the sort order of\n this sort type.\n ', states=TunableList( description= '\n States whose values are used to sort on for this sort type. \n ', tunable=TunableReference( description= '\n A State to sort on.\n ', manager=services.get_instance_manager( sims4.resources.Types.OBJECT_STATE), class_restrictions='ObjectState')), default='states'), is_ascending=Tunable( description= '\n Whether a higher value from object_data will sort first.\n If a high value means that the object should sort lower \n (E.G. brokenness), this should be false.\n ', tunable_type=bool, default=True), debug_name=Tunable( description= '\n A unique name used to select this inventory sort type through \n the console command ui.inventory.set_sort_filter when the inventory\n ui is open.\n ', tunable_type=str, default='NONE'), export_class_name='InventoryUISortTypeTuple', export_modes=ExportModes.ClientBinary)) UI_FILTER_TYPES = TunableList( description= "\n A list of filter categories containing filter types used to filter the sim's\n inventory in the UI. The inventory can also be sorted by filter type; \n filters lower on this list will sort lower when sorted by filter type.\n ", tunable=TunableTuple( description= '\n A category of filters in the UI. Contains a name and a list of filters.\n ', filters=TunableList( description= '\n The filters used in this category. \n ', tunable=TunableTuple( description= '\n Data that defines a filter type in the inventory UI.\n ', tags=TunableTags( description= '\n Tags that should be considered part of this filter.\n ', binary_type=EnumBinaryExportType.EnumUint32), filter_name=TunableLocalizedString( description= '\n The name displayed in the UI for this filter type. \n ' ), debug_name=Tunable( description= '\n A unique name used to select this inventory filter type through \n the console command ui.inventory.set_sort_filter when the inventory\n ui is open.\n ', tunable_type=str, default='NONE'), export_class_name='InventoryUIFilterTypeTuple')), category_name=TunableLocalizedString( description= '\n The name displayed in the UI for this filter category.\n ' ), export_class_name='InventoryUIFilterCategoryTuple', export_modes=ExportModes.ClientBinary)) def __init__(self, inventory_type, item_location, max_size=None, allow_compaction=True, allow_ui=True, hidden_storage=False): self._objects = {} self._owners = WeakSet() self._inventory_type = inventory_type self._item_location = item_location self._max_size = max_size self._allow_compaction = allow_compaction self._allow_ui = allow_ui self._hidden_storage = hidden_storage self._stacks_with_options_counter = None def __len__(self): return len(self._objects) def __iter__(self): yield from iter(self._objects.values()) def __contains__(self, obj_id): return obj_id in self._objects def __getitem__(self, obj_id): if obj_id in self._objects: return self._objects[obj_id] def __repr__(self): return 'InventoryStorage<{},{}>'.format(self._inventory_type, self._get_inventory_id()) def register(self, owner): self._owners.add(owner) def unregister(self, owner): self._owners.discard(owner) def has_owners(self): if self._owners: return True return False def get_owners(self): return tuple(self._owners) @property def allow_ui(self): return self._allow_ui @allow_ui.setter def allow_ui(self, value): self._allow_ui = value def discard_object_id(self, obj_id): if obj_id in self._objects: del self._objects[obj_id] def discard_all_objects(self): for obj in self._objects.values(): self._distribute_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_REMOVE, obj) obj.inventoryitem_component.set_inventory_type(None, None) self._objects.clear() def can_insert(self, obj): if not obj.can_go_in_inventory_type(self._inventory_type): return False elif self._max_size is not None and sum( inventory_obj.stack_count() for inventory_obj in self) >= self._max_size: return False return True def insert(self, obj, inventory_object=None, compact=True): if not self.can_insert(obj): return False try: obj.on_before_added_to_inventory() except: logger.exception( 'Exception invoking on_before_added_to_inventory. obj: {}', obj) self._insert(obj, inventory_object) try: obj.on_added_to_inventory() except: logger.exception( 'Exception invoking on_added_to_inventory. obj: {}', obj) compacted_obj_id = None compacted_count = None if compact: (compacted_obj_id, compacted_count) = self._try_compact(obj) if compacted_obj_id is None: for owner in self._owners: try: owner.on_object_inserted(obj) except: logger.exception( 'Exception invoking on_object_inserted. obj: {}, owner: {}', obj, owner) self._distribute_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_ADD, obj) sent_stack_update = False if obj.inventoryitem_component.has_stack_option: if self._stacks_with_options_counter is None: self._stacks_with_options_counter = defaultdict(int) stack_id = obj.inventoryitem_component.get_stack_id() stack_objects = self._stacks_with_options_counter[stack_id] if stack_objects == 0: self._distribute_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_SET_STACK_OPTION, obj) sent_stack_update = True self._stacks_with_options_counter[stack_id] += 1 if not sent_stack_update: obj_owner = obj.inventoryitem_component.get_inventory().owner if obj_owner.is_sim and obj_owner.sim_info.favorites_tracker is not None and obj_owner.sim_info.favorites_tracker.is_favorite_stack( obj): self._distribute_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_SET_STACK_OPTION, obj) else: for owner in self._owners: try: owner.on_object_id_changed(obj, compacted_obj_id, compacted_count) except: logger.exception( 'Exception invoking on_object_id_changed. obj: {}, owner: {}', obj, owner) self._distribute_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_UPDATE, obj, obj_id=compacted_obj_id) return True def update_object_stack_by_id(self, obj_id, new_stack_id): if obj_id not in self._objects: return obj = self._objects[obj_id] self._distribute_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_REMOVE, obj) obj.set_stack_id(new_stack_id) self._distribute_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_ADD, obj) def remove(self, obj, count=1, move_to_object_manager=True): if obj.id not in self._objects: return False old_stack_count = obj.stack_count() split_obj = self._try_split(obj, count) try: obj.on_before_removed_from_inventory() except: logger.exception( 'Exception invoking on_before_removed_from_inventory. obj: {}', obj) self._remove(obj, move_to_object_manager=move_to_object_manager) try: obj.on_removed_from_inventory() except: logger.exception( 'Exception invoking on_removed_from_inventory. obj: {}', obj) if split_obj is None: for owner in self._owners: try: owner.on_object_removed(obj) except: logger.exception( 'Exception invoking on_object_removed. obj: {}, owner: {}', obj, owner) self._distribute_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_REMOVE, obj) if obj.inventoryitem_component.has_stack_option and self._stacks_with_options_counter is not None: stack_id = obj.inventoryitem_component.get_stack_id() self._stacks_with_options_counter[stack_id] -= 1 if stack_id in self._stacks_with_options_counter <= 0: if self._stacks_with_options_counter[stack_id] < 0: logger.error( 'Counter went negative for stack_id {} with scheme {}', stack_id, obj.inventoryitem_component.stack_scheme, owner='jdimailig') del self._stacks_with_options_counter[stack_id] else: for owner in self._owners: try: owner.on_object_id_changed(split_obj, obj.id, old_stack_count) except: logger.exception( 'Exception invoking on_object_id_changed. obj: {}, owner: {}', obj, owner) self._distribute_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_UPDATE, split_obj, obj_id=obj.id) return True def _insert(self, obj, inventory_object): self._objects[obj.id] = obj obj.inventoryitem_component.set_inventory_type(self._inventory_type, inventory_object) obj.item_location = self._item_location if self._inventory_type == InventoryType.SIM: obj.inventoryitem_component.is_hidden = self._hidden_storage object_manager = services.object_manager() if obj.id in object_manager: object_manager.move_to_inventory( obj, services.current_zone().inventory_manager) obj.set_parent(None) posture_graph_service = services.current_zone( ).posture_graph_service if posture_graph_service.is_object_pending_deletion(obj): posture_graph_service.finalize_object_deletion(obj) def _remove(self, obj, move_to_object_manager=False): if move_to_object_manager: services.current_zone().inventory_manager.move_to_world( obj, services.object_manager()) obj.item_location = ItemLocation.ON_LOT obj.inventoryitem_component.set_inventory_type( None, None, from_removal=not move_to_object_manager) del self._objects[obj.id] def _get_compact_data(self, obj): try: obj.inventoryitem_component.save_for_stack_compaction = True return obj.get_attribute_save_data() finally: obj.inventoryitem_component.save_for_stack_compaction = False obj.post_tooltip_save_data_stored() def _try_compact(self, obj): if not self._allow_compaction: return (None, None) if len(self._objects) < 2: return (None, None) if obj.has_component( components.types.OBJECT_CLAIM_COMPONENT ) and obj.object_claim_component.requires_claiming: return (None, None) similar = None def_id = obj.definition.id data = self._get_compact_data(obj) stack_id = obj.inventoryitem_component.get_stack_id() for other in self._objects.values(): if def_id != other.definition.id: continue if other is obj: continue if stack_id != other.inventoryitem_component.get_stack_id(): continue if not any(interaction.should_reset_based_on_pipeline_progress for interaction in other.interaction_refs): other_data = self._get_compact_data(other) if data == other_data: similar = other break if similar is None: return (None, None) similar_id = similar.id similar_count = similar.stack_count() self._remove(similar) similar.destroy(source=self, cause='InventoryStorage compaction') obj.update_stack_count(similar_count) return (similar_id, similar_count) def _try_split(self, obj, count): if count >= obj.stack_count(): return clone = obj.inventoryitem_component.get_clone_for_stack_split() self._insert(clone, obj.inventoryitem_component.last_inventory_owner) clone.update_stack_count(-count) obj.set_stack_count(count) clone.on_added_to_inventory() return clone def _get_inventory_id(self): if InventoryTypeTuning.is_shared_between_objects(self._inventory_type): return int(self._inventory_type) if self._owners: return next(iter(self._owners)).owner.id logger.error( "Non-shared storage that's missing an owner: InventoryStorage<{},{}>", self._inventory_type, 0) return 0 def _get_inventory_ui_type(self): if InventoryTypeTuning.is_shared_between_objects(self._inventory_type): return UI_pb2.InventoryItemUpdate.TYPE_SHARED return UI_pb2.InventoryItemUpdate.TYPE_OBJECT def _get_inventory_update_message(self, update_type, obj, obj_id=None, allow_while_zone_not_running=False): if not self._allow_ui: return if not services.current_zone( ).is_zone_running and not allow_while_zone_not_running: return if services.current_zone().is_zone_shutting_down: return msg = UI_pb2.InventoryItemUpdate() msg.type = update_type msg.inventory_id = self._get_inventory_id() msg.inventory_type = self._get_inventory_ui_type() msg.stack_id = obj.inventoryitem_component.get_stack_id() if obj_id is None: msg.object_id = obj.id else: msg.object_id = obj_id if update_type == UI_pb2.InventoryItemUpdate.TYPE_ADD: add_data = UI_pb2.InventoryItemData() add_data.definition_id = obj.definition.id msg.add_data = add_data if update_type == UI_pb2.InventoryItemUpdate.TYPE_ADD or update_type == UI_pb2.InventoryItemUpdate.TYPE_UPDATE: dynamic_data = UI_pb2.DynamicInventoryItemData() dynamic_data.value = obj.current_value dynamic_data.count = obj.stack_count() dynamic_data.new_object_id = obj.id dynamic_data.is_new = obj.new_in_inventory dynamic_data.sort_order = obj.get_stack_sort_order() icon_info = obj.get_icon_info_data() build_icon_info_msg(icon_info, None, dynamic_data.icon_info) recipe_name = obj.get_tooltip_field( TooltipFieldsComplete.recipe_name ) or obj.get_craftable_property(GameObjectProperty.RECIPE_NAME) if recipe_name is not None: dynamic_data.recipe_name = recipe_name if obj.custom_name is not None: dynamic_data.custom_name = obj.custom_name if InventoryStorage.UI_SORT_TYPES: sort_type = 0 for sort_type_data in InventoryStorage.UI_SORT_TYPES: value = None try: abs_value = None state_component = obj.state_component if state_component is None: continue for state in sort_type_data.object_data: if state_component.has_state(state): test_value = float( state_component.get_state(state).value) abs_test_value = abs(test_value) if value is None: value = test_value elif abs_value < abs_test_value: value = test_value abs_value = abs_test_value except TypeError: pass if value is not None: sort_data_item = UI_pb2.InventoryItemSortData() sort_data_item.type = sort_type sort_data_item.value = value dynamic_data.sort_data.append(sort_data_item) sort_type += 1 if update_type == UI_pb2.InventoryItemUpdate.TYPE_ADD: msg.add_data.dynamic_data = dynamic_data else: msg.update_data = dynamic_data if update_type == UI_pb2.InventoryItemUpdate.TYPE_SET_STACK_OPTION: dynamic_data = UI_pb2.DynamicInventoryItemData() if obj.inventoryitem_component.has_stack_option: obj.inventoryitem_component.populate_stack_icon_info_data( dynamic_data.icon_info) obj_owner = obj.inventoryitem_component.get_inventory().owner if obj_owner.is_sim: favorites_tracker = obj_owner.sim_info.favorites_tracker if favorites_tracker is not None: if favorites_tracker.is_favorite_stack(obj): dynamic_data.is_favorite = True msg.update_data = dynamic_data return msg def _distribute_inventory_update_message(self, update_type, obj, obj_id=None): msg = self._get_inventory_update_message(update_type, obj, obj_id=obj_id) if msg is not None: op = GenericProtocolBufferOp(Operation.INVENTORY_ITEM_UPDATE, msg) Distributor.instance().add_op_with_no_owner(op) def distribute_inventory_update_message(self, obj): if obj.id not in self._objects: return False msg = self._get_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_UPDATE, obj) if msg is not None: op = GenericProtocolBufferOp(Operation.INVENTORY_ITEM_UPDATE, msg) Distributor.instance().add_op_with_no_owner(op) def distribute_inventory_stack_update_message(self, obj): if obj.id not in self._objects: return msg = self._get_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_SET_STACK_OPTION, obj) if msg is not None: op = GenericProtocolBufferOp(Operation.INVENTORY_ITEM_UPDATE, msg) Distributor.instance().add_op_with_no_owner(op) def distribute_owned_inventory_update_message(self, obj, owner): if obj.id not in self._objects: return False msg = self._get_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_UPDATE, obj) if msg is not None: op = GenericProtocolBufferOp(Operation.INVENTORY_ITEM_UPDATE, msg) Distributor.instance().add_op(owner, op) def get_item_update_ops_gen(self): stack_options_set = set() for obj in self._objects.values(): message = self._get_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_ADD, obj, allow_while_zone_not_running=True) if message is None: continue yield (obj, GenericProtocolBufferOp(Operation.INVENTORY_ITEM_UPDATE, message)) if not obj.inventoryitem_component.has_stack_option: obj_owner = obj.inventoryitem_component.get_inventory().owner if obj_owner.is_sim: if obj_owner.sim_info.favorites_tracker is None: continue stack_id = obj.inventoryitem_component.get_stack_id() if stack_id in stack_options_set: continue option_msg = self._get_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_SET_STACK_OPTION, obj, allow_while_zone_not_running=True) if option_msg is not None: stack_options_set.add(stack_id) yield (obj, GenericProtocolBufferOp( Operation.INVENTORY_ITEM_UPDATE, option_msg)) else: stack_id = obj.inventoryitem_component.get_stack_id() if stack_id in stack_options_set: continue option_msg = self._get_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_SET_STACK_OPTION, obj, allow_while_zone_not_running=True) if option_msg is not None: stack_options_set.add(stack_id) yield (obj, GenericProtocolBufferOp( Operation.INVENTORY_ITEM_UPDATE, option_msg)) def open_ui_panel(self, obj): if not self._allow_ui: return False msg = UI_pb2.OpenInventory() msg.object_id = obj.id msg.inventory_id = self._get_inventory_id() msg.inventory_type = self._get_inventory_ui_type() op = GenericProtocolBufferOp(Operation.OPEN_INVENTORY, msg) Distributor.instance().add_op_with_no_owner(op) return True
class DisplayComponent(Component, HasTunableFactory, AutoFactoryInit, component_name=types.DISPLAY_COMPONENT): DISPLAY_STATE = TunableStateValueReference( description= '\n The state a display object will be set to when it is parented to a\n Display Parent.\n ' ) DEFAULT_STATE = TunableStateValueReference( description= '\n The default state a display object will be set to when it is unparented\n from a Display Parent.\n ' ) FACTORY_TUNABLES = { 'display_parent': CraftTaggedItemFactory( description= '\n If an object matches the tag(s), it will be considered a Display\n Parent for this display object. All display objects with a Display\n Component MUST have a Display Parent tuned, otherwise there is no\n need in the Display Component.\n ' ), 'use_display_state': Tunable( description= "\n If enabled, this object will change to the Display State when it is\n parented to a Display Parent. The Display State is tuned in the\n objects.components.display_component module tuning. NOTICE: If you\n are only tuning this and not tuning any Inventory State Triggers,\n it's recommended that you use the Slot Component in the Native\n Components section of the parent object.\n ", tunable_type=bool, default=True), 'inventory_state_triggers': TunableList( description= '\n Change states on the owning object based on tests applied to the\n inventory of the Display Parent. Tests will be done in order and\n will stop at the first success.\n ', tunable=TunableTuple(inventory_test=InventoryTest.TunableFactory(), set_state=TunableStateValueReference())) } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self.inventory_state_triggers: services.get_event_manager().register_tests( self, (self.inventory_state_triggers[0].inventory_test, )) @property def _is_on_display_parent(self): parent = self.owner.parent if parent is None: return False return self.display_parent(crafted_object=parent, skill=None) is not None def handle_event(self, sim_info, event, resolver): if sim_info is not None: return if not self._is_on_display_parent: return self._handle_inventory_changed() def _handle_inventory_changed(self): obj_resolver = SingleObjectResolver(self.owner) for trigger in self.inventory_state_triggers: if obj_resolver(trigger.inventory_test): if self.owner.has_state(trigger.set_state.state): self.owner.set_state(trigger.set_state.state, trigger.set_state) break def slotted_to_object(self, parent): if self._should_change_display_state(parent) and self.owner.has_state( self.DISPLAY_STATE.state): self.owner.set_state(self.DISPLAY_STATE.state, self.DISPLAY_STATE) self._handle_inventory_changed() def unslotted_from_object(self, parent): if self._should_change_display_state(parent) and self.owner.has_state( self.DEFAULT_STATE.state): self.owner.set_state(self.DEFAULT_STATE.state, self.DEFAULT_STATE) def _should_change_display_state(self, parent): if not self.use_display_state: return False return self.display_parent(crafted_object=parent, skill=None)
class FetchObjectSocialSuperInteraction(DistancePlacementMixin, SocialSuperInteraction): MAX_FETCH_RADIUS = 3 INSTANCE_TUNABLES = { 'fetch_constraint': TunableCircle( description= '\n The circle constraint for other Sims in the social group to route\n near the placement location.\n ', radius=MAX_FETCH_RADIUS, tuning_group=GroupNames.CONSTRAINTS), 'throw_xevent_id': Tunable( description= '\n An xevent id for when the carry target is thrown from an animation.\n ', tunable_type=int, default=0, tuning_group=GroupNames.ANIMATION) } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._plan_primitives = [] self._follow_path_elements = [] @flexmethod def _constraint_gen(cls, inst, sim, target, participant_type=ParticipantType.Actor, **kwargs): inst_or_cls = inst if inst is not None else cls yield from super(SuperInteraction, inst_or_cls)._constraint_gen( sim, target, participant_type=participant_type, **kwargs) if inst is not None and participant_type == ParticipantType.TargetSim: starting_location = inst._starting_location if inst._starting_location is not None else sim.location constraint = interactions.constraints.Circle( starting_location.transform.translation, inst.social_group.max_radius, starting_location.routing_surface) constraint = constraint.intersect( interactions.constraints.Facing( target_position=starting_location.transform.translation)) yield constraint placement_constraint = inst_or_cls._get_distance_placement_constraint( sim, target, participant_type=participant_type) yield placement_constraint def distribute_fetch_route(self, *args, **kwargs): for plan_primitive in self._plan_primitives: if FollowPath.should_follow_path(plan_primitive.sim, plan_primitive.path): follow_path_element = FollowPath(plan_primitive.sim, plan_primitive.path) self._follow_path_elements.append(follow_path_element) follow_path_element.distribute_path_asynchronously() def build_basic_content(self, *args, **kwargs): self.store_event_handler(self.distribute_fetch_route, handler_id=self.throw_xevent_id) sequence = super().build_basic_content(*args, **kwargs) def plan_fetch_paths(timeline): for target_sim in self.get_participants( ParticipantType.TargetSim | ParticipantType.Listeners): fetch_constraint = self.fetch_constraint.create_constraint( target_sim, target_position=self._distance_placement_transform. translation, routing_surface=self._routing_surface if self._routing_surface is not None else DEFAULT) facing = interactions.constraints.Facing( target_position=self._distance_placement_transform. translation, inner_radius=self.facing_radius) fetch_constraint = fetch_constraint.intersect(facing) if not fetch_constraint.valid: continue goals = [] handles = fetch_constraint.get_connectivity_handles(target_sim) for handle in handles: goals.extend(handle.get_goals()) if not goals: pass else: route = routing.Route( target_sim.routing_location, goals, routing_context=target_sim.routing_context) plan_primitive = PlanRoute(route, target_sim, interaction=self) result = yield from element_utils.run_child( timeline, plan_primitive) if result: if plan_primitive.path.nodes: if plan_primitive.path.nodes.plan_success: if plan_primitive.path.portal_obj is not None: logger.error( 'Need sub interaction to route {} due to portal on path' .format(target_sim)) else: self._plan_primitives.append( plan_primitive) yield from element_utils.run_child(timeline, sequence) return build_element(plan_fetch_paths)
class IngredientTuning: __qualname__ = 'IngredientTuning' INGREDIENT_QUALITY_MAPPING = TunableMapping(description='\n Mapping of all possible ingredient quality states to what possible\n states will the ingredients have.\n e.g. High quality ingredients need to be mapped to gardening high \n quality, fish high quality or any state that will indicate what \n high quality means on a different system.\n ', key_type=ObjectStateValue.TunableReference(description='\n The states that will define the ingredient quality.\n '), value_type=TunableTuple(description='\n Definition of the ingredient quality state. This will define\n the quality boost on the recipe and the possible states an \n ingredient can have to have this state.\n ', quality_boost=Tunable(description='\n Value that will be added to the quality commodity whenever\n this state is added.\n ', tunable_type=int, default=1), state_value_list=TunableList(description='\n List of ingredient states that will give this level of \n ingredient quality.\n ', tunable=ObjectStateValue.TunableReference(description='\n The states that will define the ingredient quality.\n ')))) INGREDIENT_TAG_DISPLAY_MAPPING = TunableMapping(description='\n Mapping of all object tags to their localized string that will display\n on the ingredient list.\n This will be used for displaying on the recipe\'s when an ingredient is \n tuned by tag instead of object definition.\n Example: Display objects of rag FISH as string "Any Fish"\n ', key_type=TunableEnumEntry(description='\n Tag corresponding at an ingredient type that can be used in a\n recipe.\n ', tunable_type=tag.Tag, default=tag.Tag.INVALID), value_type=TunableLocalizedStringFactory()) INGREDIENT_TAG = TunableEnumEntry(description='\n Tag to look for when iterating through objects to know if they are \n ingredients.\n All ingredients should be tuned with this tag.\n ', tunable_type=tag.Tag, default=tag.Tag.INVALID) @classmethod def get_quality_bonus(cls, ingredient): for quality_details in IngredientTuning.INGREDIENT_QUALITY_MAPPING.values(): for state_value in quality_details.state_value_list: while ingredient.state_value_active(state_value): return quality_details.quality_boost return 0 @classmethod def get_ingredient_quality_state(cls, quality_bonus): state_to_add = None bonus_selected = None for (quality_state_value, quality_details) in IngredientTuning.INGREDIENT_QUALITY_MAPPING.items(): while (bonus_selected is None or quality_details.quality_boost <= bonus_selected) and bonus_selected >= quality_bonus: bonus_selected = quality_details.quality_boost state_to_add = quality_state_value return state_to_add @classmethod def get_ingredient_string_for_tag(cls, tag): string_factory = IngredientTuning.INGREDIENT_TAG_DISPLAY_MAPPING.get(tag) if string_factory: return string_factory() return