class CareerAttendenceTest(event_testing.test_base.BaseTest): __qualname__ = 'CareerAttendenceTest' test_events = (event_testing.test_events.TestEvent.WorkdayComplete, ) USES_DATA_OBJECT = True USES_EVENT_DATA = True FACTORY_TUNABLES = { 'description': 'After a work day completes, did your sim work a desired of hours, earn a tuned amount (total over lifetime), at a specific or any career. Note: any career (leaving career untuned) means it checks against total of all of them.', 'career_to_test': TunableReference(manager=services.get_instance_manager( sims4.resources.Types.CAREER)), 'career_category': TunableEnumEntry( careers.career_tuning.CareerCategory, careers.career_tuning.CareerCategory.Invalid, description= 'Category the specified career is required to be in order to pass validation' ), 'simoleons_earned': TunableThreshold(description='Amount in Simoleons required to pass'), 'hours_worked': TunableThreshold(description='Amount in hours required to pass') } def __init__(self, career_to_test, career_category, simoleons_earned, hours_worked, **kwargs): super().__init__(**kwargs) self.career_to_test = career_to_test self.simoleons_earned = simoleons_earned self.hours_worked = hours_worked self.career_category = career_category def get_expected_args(self): return { 'career': event_testing.test_events.FROM_EVENT_DATA, 'data': event_testing.test_events.FROM_DATA_OBJECT, 'objective_guid64': event_testing.test_events.OBJECTIVE_GUID64 } @cached_test def __call__(self, career=None, data=None, objective_guid64=None): if career is None: return TestResult( False, 'Career provided is None, valid during zone load.') total_money_made = 0 total_time_worked = 0 if not isinstance(career, self.career_to_test): return TestResult(False, '{} does not match tuned value {}', career, self.career_to_test) career_data = data.get_career_data(career) total_money_made = career_data.get_money_earned() total_time_worked = career_data.get_hours_worked() relative_start_values = data.get_starting_values(objective_guid64) money = 0 time = 1 total_money_made -= relative_start_values[money] total_time_worked -= relative_start_values[time] if not (self.career_to_test is not None and relative_start_values is not None and self.simoleons_earned.compare(total_money_made)): return TestResultNumeric( False, 'CareerAttendenceTest: not the desired amount of Simoleons.', current_value=total_money_made, goal_value=self.simoleons_earned.value, is_money=True) if not self.hours_worked.compare(total_time_worked): return TestResultNumeric( False, 'CareerAttendenceTest: not the desired amount of time worked.', current_value=total_time_worked, goal_value=self.hours_worked.value, is_money=False) return TestResult.TRUE def save_relative_start_values(self, objective_guid64, data_object): if self.career_to_test is not None: return career_name = self.career_to_test.__name__ start_money = data_object.get_career_data_by_name( career_name).get_money_earned() start_time = data_object.get_career_data_by_name( career_name).get_hours_worked() data_object.set_starting_values(objective_guid64, [start_money, start_time])
class CarryableComponent( Component, HasTunableFactory, AutoFactoryInit, component_name=objects.components.types.CARRYABLE_COMPONENT): class _CarryableAllowedHands(HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = { 'biped_allowed_hands': TunableVariant(locked_args={ 'both': (Hand.LEFT, Hand.RIGHT), 'left_only': (Hand.LEFT, ), 'right_only': (Hand.RIGHT, ) }, default='both'), 'quadruped_allowed_hands': TunableVariant(locked_args={ 'both': (Hand.LEFT, Hand.RIGHT), 'mouth_only': (Hand.RIGHT, ), 'back_only': (Hand.LEFT, ) }, default='mouth_only') } def get_allowed_hands(self, sim): if sim is None: return self.biped_allowed_hands return sim.get_allowed_hands_type(self) class _CarryableTransitionConstraint(HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = { 'constraint_mobile': TunableList( description= '\n The constraint to use when the Sim is in a mobile posture.\n ', tunable=TunableGeometricConstraintVariant( disabled_constraints={ 'spawn_points', 'relative_circle', 'current_position' })), 'constraint_non_mobile': TunableList( description= '\n The constraint to use when the Sim is not in a mobile posture.\n ', tunable=TunableGeometricConstraintVariant( disabled_constraints={ 'spawn_points', 'relative_circle', 'current_position' })) } DEFAULT_GEOMETRIC_TRANSITION_CONSTRAINT = _CarryableTransitionConstraint.TunableFactory( description= '\n Unless specifically overridden, the constraint to use when transitioning\n into and out of a carry for any carryable object.\n ' ) DEFAULT_GEOMETRIC_TRANSITION_LARGE = TunableRange( description= '\n This is a large transition distance. This is used by:\n * TYAE humans picking up any pet\n ', tunable_type=float, default=0.7, minimum=0) DEFAULT_GEOMETRIC_TRANSITION_MEDIUM = TunableRange( description= '\n This is a medium transition distance. This is used by:\n * TYAE humans picking up P humans\n ', tunable_type=float, default=0.6, minimum=0) DEFAULT_GEOMETRIC_TRANSITION_SMALL = TunableRange( description= '\n This is a small transition distance. This is used by:\n * C humans picking up AE cats and AE small dogs\n ', tunable_type=float, default=0.503, minimum=0) DEFAULT_GEOMETRIC_TRANSITION_TINY = TunableRange( description= '\n This is a tiny transition distance. This is used by:\n * C humans picking up C cats and C dogs and small dogs\n ', tunable_type=float, default=0.419, minimum=0) DEFAULT_CARRY_AFFORDANCES = TunableList( description= '\n The list of default carry affordances.\n ', tunable=TunableReference(manager=services.get_instance_manager( sims4.resources.Types.INTERACTION))) PUT_IN_INVENTORY_AFFORDANCE = TunableReference( description= '\n The affordance used by carryable component to put objects in inventory.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION)) PUT_DOWN_HERE_AFFORDANCE = TunableReference( description= '\n The affordance used by carryable component to put down here via the\n PutDownLiability liability.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION)) PUT_DOWN_ANYWHERE_AFFORDANCE = TunableReference( description= '\n The affordance used by carryable component to put down objects anywhere\n via the PutDownLiability liability.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION)) FACTORY_TUNABLES = { 'put_down_tuning': TunableVariant( description= '\n Define how Sims prefer to put this object down.\n ', reference=TunablePutDownStrategySpeciesMapping( description= '\n Define specific costs for all the possible locations this object\n could be put down. Also, define, if necessary, custom\n interactions to put the object down.\n \n If this is not specified, and the default is used, the most\n appropriate put down strategy is used. This allows the object to\n be put down in most places (floor, inventory, slots). Also,\n species might specify their own custom behavior.\n ' ), locked_args={'use_default': None}, default='use_default'), 'state_based_put_down_tuning': TunableMapping( description= '\n A mapping from a state value to a putdownstrategy. If the owning\n object is in any of the states tuned here, it will use that state\'s\n associated putdownstrategy in place of the one putdownstrategy tuned\n in the "put_down_tuning" field. If the object is in multiple states\n listed in this mapping, the 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=TunablePutDownStrategySpeciesMapping( description= '\n Tuning for how to score where a Sim might want to set an\n object down.\n ' )), key_name='State', value_name='PutDownStrategy'), 'carry_affordances': OptionalTunable(tunable=TunableList(tunable=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': TunableProvidedAffordances( description= '\n Affordances that are generated when a Sim holding this object\n selects another object to interact with. The generated interactions\n target the selected object, and have their carry target set to the\n component\'s owner.\n \n By default, this is applied to Sims. e.g.: The "Drink" interaction\n provides "Make Toast" on other Sims.\n \n Optionally, you can specify a tag to have the interaction appear on\n other objects. e.g.: The "Hold Puppy" interaction might unlock "Put\n Down Here" on sofas.\n \n Target defaults to the object selected (Invalid). Carry Target is\n locked to this object being carried. If is_linked is\n checked, the affordance will be linked to the interaction\n carrying this object.\n ', class_restrictions=('SuperInteraction', ), locked_args={ 'allow_self': False, 'target': ParticipantType.Object, 'carry_target': ParticipantType.CarriedObject }), '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=TunableConstraintVariant( description= '\n A constraint that must be fulfilled in order to\n interact with this object.\n ' ))), 'allowed_hands_data': _CarryableAllowedHands.TunableFactory(), '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_when_routing': TunableUnholsterWhileRoutingBehaviorVariant(), '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), 'is_valid_posture_graph_object': Tunable( description= '\n If this is checked, this object is allowed to provide postures and\n expand in the posture graph, despite its ability to be carried.\n \n Normally, for the performance reasons, carryable objects are not\n posture providing objects. The preferred way of interacting with a\n carryable object is marking it as a carry requirement in the ASM.\n \n However, there are objects for which this is not possible. For\n example, Nesting Blocks are carryable, but toddlers interact with\n them while in the "Sit On Ground" posture, which must be provided by\n the object\'s parts.\n \n This field should be used with caution, since posture graph\n generation is one of our slowest systems. Do not enable it on\n objects such as food or drinks, since the world is bound to have\n many of them.\n ', tunable_type=bool, default=False), 'portal_key_mask_flags': TunableEnumFlags( description= "\n Any flag tuned here will be kept on the Sim routing context who's\n picking up this object. This will allow a Sim to pickup some\n type of objects and still be allowed to transition through\n some portals while carrying an object.\n ", enum_type=PortalFlags, allow_no_flags=True), 'reslot_plumbbob': OptionalTunable( description= '\n If tuned, the plumbbob will be repositioned when this item is carried.\n Reslot will always go away when sim stops carrying the object.\n ', tunable=TunableReslotPlumbbob()), 'defer_putdown': Tunable( description= '\n If true, the put down will be deferred to the end of the route. \n If false, put down will be done at the start of the route. This\n should be the default behavior. \n ', tunable_type=bool, default=False), 'put_down_height_tolerance': TunableRange( description= '\n Maximum height tolerance on the terrain we will use for the \n placement of this object when asking FGL to find a spot on the\n floor.\n Having a high value here will make it so an object can be placed\n in a terrain spot with a high slope that might result with\n clipping depending on the width of the object. The way this will\n work will be using the object footprint, if the edges are at a\n height higher than the height tolerance, then the location will\n not be valid.\n ', tunable_type=float, default=1.1, minimum=0) } def __init__(self, owner, **kwargs): super().__init__(owner, **kwargs) self._attempted_putdown = False self._attempted_alternative_putdown = False self._cached_put_down_strategy = None @property def attempted_putdown(self): return self._attempted_putdown @property def attempted_alternative_putdown(self): return self._attempted_alternative_putdown @property def ideal_slot_type_set(self): put_down_strategy = self.get_put_down_strategy() return put_down_strategy.ideal_slot_type_set @componentmethod def get_carry_object_posture(self): if self.carry_affordances is None: return CarryPostureStaticTuning.POSTURE_CARRY_OBJECT return self.carry_affordances[0].provided_posture_type @componentmethod_with_fallback(lambda *_, **__: ()) def get_allowed_hands(self, sim): return self.allowed_hands_data.get_allowed_hands(sim) @componentmethod_with_fallback(lambda *_, **__: 0) def get_portal_key_make_for_carry(self): return self.portal_key_mask_flags @componentmethod def should_unholster(self, *args, **kwargs): return self.unholster_when_routing.should_unholster(*args, **kwargs) @componentmethod def get_put_down_strategy(self, parent=DEFAULT): if self._cached_put_down_strategy is None: parent = self.owner.parent if parent is DEFAULT else parent species = parent.species if parent is not None else Species.HUMAN for (state_value, put_down_strategy ) in self.state_based_put_down_tuning.items(): if self.owner.state_value_active(state_value): self._cached_put_down_strategy = put_down_strategy.get( species) break else: if self.put_down_tuning is not None: self._cached_put_down_strategy = self.put_down_tuning.get( species) if self._cached_put_down_strategy is None: put_down_strategy = parent.get_default_put_down_strategy() self._cached_put_down_strategy = put_down_strategy return self._cached_put_down_strategy def _get_carry_transition_distance_for_sim(self, sim): carry_sim = self.owner.sim_info carrying_sim = sim.sim_info if carry_sim.is_toddler: return self.DEFAULT_GEOMETRIC_TRANSITION_MEDIUM if carrying_sim.is_teen_or_older: return self.DEFAULT_GEOMETRIC_TRANSITION_LARGE if carry_sim.is_teen_or_older: return self.DEFAULT_GEOMETRIC_TRANSITION_SMALL return self.DEFAULT_GEOMETRIC_TRANSITION_TINY def _get_adjusted_circle_constraint(self, sim, constraint): if not self.owner.is_sim: return constraint if not isinstance(constraint, TunedCircle): return constraint ideal_radius_override = self._get_carry_transition_distance_for_sim( sim) constraint = copy.copy(constraint) constraint.ideal_radius_width = constraint.ideal_radius_width / constraint.ideal_radius * ideal_radius_override constraint.radius = constraint.radius / constraint.ideal_radius * ideal_radius_override constraint.ideal_radius = ideal_radius_override return constraint @componentmethod def get_carry_transition_constraint(self, sim, position, routing_surface, cost=0, mobile=True): constraints = self.DEFAULT_GEOMETRIC_TRANSITION_CONSTRAINT constraints = constraints.constraint_mobile if mobile else constraints.constraint_non_mobile final_constraint = Anywhere() for constraint in constraints: if mobile: constraint = self._get_adjusted_circle_constraint( sim, constraint) final_constraint = final_constraint.intersect( constraint.create_constraint(None, None, target_position=position, routing_surface=routing_surface)) final_constraint = final_constraint.generate_constraint_with_cost(cost) final_constraint = final_constraint._copy(_multi_surface=True) return final_constraint @componentmethod def get_pick_up_constraint(self, sim): if self.constraint_pick_up is None: return final_constraint = Anywhere() for constraint in self.constraint_pick_up: constraint = self._get_adjusted_circle_constraint(sim, constraint) constraint = constraint.create_constraint(sim, target=self.owner) final_constraint = final_constraint.intersect(constraint) final_constraint = final_constraint._copy(_multi_surface=True) return final_constraint @componentmethod def get_provided_aops_gen(self, target, context, **kwargs): for provided_affordance_data in self.provided_affordances: if not provided_affordance_data.affordance.is_affordance_available( context=context): continue if not provided_affordance_data.object_filter.is_object_valid( target): continue if provided_affordance_data.affordance.is_social and not target.is_sim: affordance = provided_affordance_data.affordance interaction_target = self.owner preferred_objects = (target, ) else: affordance = CarryTargetInteraction.generate( provided_affordance_data.affordance, self.owner) interaction_target = target preferred_objects = () depended_on_si = None parent = self.owner.parent if parent is not None: if parent.is_sim: carry_posture = parent.posture_state.get_carry_posture( self.owner) if carry_posture is not None: if provided_affordance_data.is_linked: depended_on_si = carry_posture.source_interaction yield from affordance.potential_interactions( interaction_target, context, depended_on_si=depended_on_si, preferred_objects=preferred_objects, **kwargs) def component_super_affordances_gen(self, **kwargs): if self.carry_affordances is None: affordances = self.DEFAULT_CARRY_AFFORDANCES else: affordances = self.carry_affordances for affordance in affordances: yield affordance def component_interactable_gen(self): yield self def on_state_changed(self, state, old_value, new_value, from_init): if new_value in self.state_based_put_down_tuning or old_value in self.state_based_put_down_tuning: self._cached_put_down_strategy = None def component_reset(self, reset_reason): self.reset_put_down_count() @componentmethod def get_initial_put_down_position(self, carrying_sim=None): carrying_sim = carrying_sim or self.owner.parent if carrying_sim is None: return (self.owner.position, self.owner.routing_surface) additional_put_down_distance = carrying_sim.posture.additional_put_down_distance position = carrying_sim.position + carrying_sim.forward * ( carrying_sim.object_radius + additional_put_down_distance) sim_los_constraint = carrying_sim.lineofsight_component.constraint if not sims4.geometry.test_point_in_compound_polygon( position, sim_los_constraint.geometry.polygon): position = carrying_sim.position return (position, carrying_sim.routing_surface) @componentmethod def get_put_down_aop(self, interaction, context, alternative_multiplier=1, own_inventory_multiplier=1, object_inventory_multiplier=DEFAULT, in_slot_multiplier=DEFAULT, on_floor_multiplier=1, visibility_override=None, display_name_override=None, additional_post_run_autonomy_commodities=None, add_putdown_liability=False, **kwargs): sim = interaction.sim owner = self.owner if owner.transient: return self._get_destroy_aop(sim, **kwargs) put_down_strategy = self.get_put_down_strategy(parent=sim) if object_inventory_multiplier is DEFAULT: object_inventory_multiplier = sim.get_put_down_object_inventory_cost_override( ) if in_slot_multiplier is DEFAULT: in_slot_multiplier = sim.get_put_down_slot_cost_override() slot_types_and_costs = self.get_slot_types_and_costs( multiplier=in_slot_multiplier) (terrain_transform, terrain_routing_surface) = self._get_terrain_transform(interaction) objects = self._get_objects_with_inventory(interaction) objects = [obj for obj in objects if obj.can_access_for_putdown(sim)] if put_down_strategy.floor_cost is not None and on_floor_multiplier is not None: world_cost = put_down_strategy.floor_cost * on_floor_multiplier else: world_cost = None if put_down_strategy.inventory_cost is not None and own_inventory_multiplier is not None: sim_inventory_cost = put_down_strategy.inventory_cost * own_inventory_multiplier else: sim_inventory_cost = None if put_down_strategy.object_inventory_cost is not None and object_inventory_multiplier is not None: object_inventory_cost = put_down_strategy.object_inventory_cost * object_inventory_multiplier else: object_inventory_cost = None if not put_down_strategy.affordances: self._attempted_alternative_putdown = True if not self._attempted_alternative_putdown or self.owner.is_sim: self._attempted_alternative_putdown = True scored_aops = [] for scored_aop in self._gen_affordance_score_and_aops( interaction, slot_types_and_costs=slot_types_and_costs, world_cost=world_cost, sim_inventory_cost=sim_inventory_cost, object_inventory_cost=object_inventory_cost, terrain_transform=terrain_transform, terrain_routing_surface=terrain_routing_surface, objects_with_inventory=objects, visibility_override=visibility_override, display_name_override=display_name_override, additional_post_run_autonomy_commodities= additional_post_run_autonomy_commodities, multiplier=alternative_multiplier, add_putdown_liability=add_putdown_liability): if scored_aop.aop.test(context): scored_aops.append(scored_aop) if scored_aops: scored_aops.sort(key=operator.itemgetter(0)) return scored_aops[-1].aop affordance = CarryableComponent.PUT_DOWN_ANYWHERE_AFFORDANCE if add_putdown_liability: liabilities = ((PutDownLiability.LIABILITY_TOKEN, PutDownLiability(self.owner)), ) else: liabilities = () aop = AffordanceObjectPair( affordance, self.owner, affordance, None, slot_types_and_costs=slot_types_and_costs, world_cost=world_cost, sim_inventory_cost=sim_inventory_cost, object_inventory_cost=object_inventory_cost, terrain_transform=terrain_transform, terrain_routing_surface=terrain_routing_surface, objects_with_inventory=objects, visibility_override=visibility_override, display_name_override=display_name_override, additional_post_run_autonomy_commodities= additional_post_run_autonomy_commodities, liabilities=liabilities, **kwargs) self._attempted_putdown = True return aop def _gen_affordance_score_and_aops( self, interaction, slot_types_and_costs, world_cost, sim_inventory_cost, object_inventory_cost, terrain_transform, terrain_routing_surface, objects_with_inventory, visibility_override, display_name_override, additional_post_run_autonomy_commodities, multiplier=1, add_putdown_liability=False): put_down_strategy = self.get_put_down_strategy() for affordance in put_down_strategy.affordances: if add_putdown_liability: liabilities = ((PutDownLiability.LIABILITY_TOKEN, PutDownLiability(self.owner)), ) else: liabilities = () aop = AffordanceObjectPair( affordance, self.owner, affordance, None, slot_types_and_costs=slot_types_and_costs, world_cost=world_cost, sim_inventory_cost=sim_inventory_cost, object_inventory_cost=object_inventory_cost, terrain_transform=terrain_transform, terrain_routing_surface=terrain_routing_surface, objects_with_inventory=objects, visibility_override=visibility_override, display_name_override=display_name_override, additional_post_run_autonomy_commodities= additional_post_run_autonomy_commodities, liabilities=liabilities) yield ScoredAOP(multiplier, aop) def _get_cost_for_slot_type(self, slot_type): put_down_strategy = self.get_put_down_strategy() if slot_type in self.owner.ideal_slot_types: return put_down_strategy.preferred_slot_cost return put_down_strategy.normal_slot_cost def get_slot_types_and_costs(self, multiplier=1): slot_types_and_costs = [] for slot_type in self.owner.all_valid_slot_types: cost = self._get_cost_for_slot_type(slot_type) if cost is not None and multiplier is not None: cost *= multiplier else: cost = None slot_types_and_costs.append((slot_type, cost)) return slot_types_and_costs def _get_terrain_transform(self, interaction): if not self.owner.is_sim and self.owner.footprint_component is None: return (None, None) else: sim = interaction.sim put_down_position = interaction.interaction_parameters.get( 'put_down_position') put_down_routing_surface = interaction.interaction_parameters.get( 'put_down_routing_surface') if put_down_position is None: (starting_position, starting_routing_surface ) = self.get_initial_put_down_position(carrying_sim=sim) else: starting_position = put_down_position starting_routing_surface = put_down_routing_surface starting_location = placement.create_starting_location( position=starting_position, orientation=sim.orientation, routing_surface=starting_routing_surface) if self.owner.is_sim: search_flags = FGLSearchFlagsDefaultForSim | FGLSearchFlag.STAY_IN_CURRENT_BLOCK fgl_context_fn = functools.partial( placement.create_fgl_context_for_sim, search_flags=search_flags) else: search_flags = FGLSearchFlag.STAY_IN_CURRENT_BLOCK | FGLSearchFlag.SHOULD_TEST_ROUTING | FGLSearchFlag.CALCULATE_RESULT_TERRAIN_HEIGHTS | FGLSearchFlag.DONE_ON_MAX_RESULTS | FGLSearchFlag.SHOULD_TEST_BUILDBUY fgl_context_fn = functools.partial( placement.create_fgl_context_for_object, search_flags=search_flags) MAX_PUTDOWN_STEPS = 8 MAX_PUTDOWN_DISTANCE = 10 fgl_context = fgl_context_fn( starting_location, self.owner, max_steps=MAX_PUTDOWN_STEPS, max_distance=MAX_PUTDOWN_DISTANCE, height_tolerance=self.put_down_height_tolerance) (position, orientation) = placement.find_good_location(fgl_context) if position is not None: put_down_transform = sims4.math.Transform( position, orientation) return (put_down_transform, starting_routing_surface) return (None, None) def _get_objects_with_inventory(self, interaction): objects = [] inventory_item = self.owner.inventoryitem_component if inventory_item is not None: if CarryableComponent.PUT_IN_INVENTORY_AFFORDANCE is not None: for obj in inventory_item.valid_object_inventory_gen(): objects.append(obj) return objects def _get_destroy_aop(self, sim, **kwargs): affordance = CarryableComponent.PUT_DOWN_HERE_AFFORDANCE return AffordanceObjectPair(affordance, self.owner, affordance, None, put_down_transform=None, **kwargs) def reset_put_down_count(self): self._attempted_alternative_putdown = False self._attempted_putdown = False self._cached_put_down_strategy = None def on_object_carry(self, actor, *_, **__): if self.reslot_plumbbob is not None: reslot_plumbbob(actor, self.reslot_plumbbob) def on_object_uncarry(self, actor, *_, **__): if self.reslot_plumbbob is not None: unslot_plumbbob(actor)
class RoleState(HasDependentTunableReference, role.role_state_base.RoleStateBase, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager( sims4.resources.Types.ROLE_STATE)): INSTANCE_TUNABLES = { '_role_priority': TunableEnumEntry( RolePriority, RolePriority.NORMAL, description= '\n The priority of this role state. All the role states with the\n same priority will all be applied together. The highest group\n of priorities is considered the active ones.\n ' ), '_buffs': TunableList( buffs.tunable.TunableBuffReference(pack_safe=True), description= '\n Buffs that will be added to sim when role is active.\n ' ), '_off_lot_autonomy_buff': buffs.tunable.TunableBuffReference( description= 'A buff that\n prevents autonomy from considering some objects based on the\n location of the object (e.g. on lot, off lot, within a radius of the\n sim).\n \n In the buff set: Game Effect Modifiers->Autonomy Modifier->Off Lot\n Autonomy Rule.\n ', allow_none=True), 'tags': TunableSet( TunableEnumEntry(Tag, Tag.INVALID), description= '\n Tags for the role state for checking role states against a set\n of tags rather than against a list of role states.\n ' ), 'role_affordances': TunableList( description= "\n A list of affordances that are available on the Sim in this Role\n State.\n \n e.g: when a Maid is in the working Role State, he or she will have\n the 'Dismiss' and 'Fire' affordances available in the Pie Menu.\n ", tunable=TunableReference(manager=services.affordance_manager(), class_restrictions=('SuperInteraction', ), pack_safe=True)), 'role_target_affordances': TunableList( description= '\n A list of affordances that are available on other Sims when the\n actor Sim is in this Role State.\n \n e.g. a Sim in a specific Role State could have an "Invite to\n Situation" interaction available when bringing up other Sims\' Pie\n Menus.\n ', tunable=TunableReference(manager=services.affordance_manager(), class_restrictions=('SuperInteraction', ), pack_safe=True)), 'preroll_affordances': TunableList( description= '\n A list of affordances that are available for sims to consider when\n running pre-roll. Objects related to role can specify preroll\n autonomy, but there are some roles that may not have an object\n associated with it\n \n e.g. Romance guru in romance festival preroll to an attractor point.\n ', tunable=TunableReference(manager=services.affordance_manager(), class_restrictions=('SuperInteraction', ), pack_safe=True)), '_on_activate': TunableVariant( description= '\n Select the autonomy behavior when this role state becomes active on the sim.\n disabled: Take no action.\n autonomy_ping: We explicitly force an autonomy ping on the sim.\n push_affordance: Push the specific affordance on the sim.\n ', locked_args={'disabled': None}, autonomy_ping=DoAutonomyPingFromRole.TunableFactory(), parameterized_autonomy_ping=DoParameterizedAutonomyPingFromRole. TunableFactory(), push_affordance=PushAffordanceFromRole.TunableFactory(), default='disabled'), '_portal_disallowance_tags': TunableSet( description= '\n A set of tags that define what the portal disallowance tags of\n this role state are. Portals that include any of these\n disallowance tags are considered locked for sims that have this\n role state.\n ', tunable=TunableEnumWithFilter( description= '\n A single portal disallowance tag.\n ', tunable_type=tag.Tag, default=tag.Tag.INVALID, filter_prefixes=tag.PORTAL_DISALLOWANCE_PREFIX)), '_allow_npc_routing_on_active_lot': Tunable( description= '\n If True, then npc in this role will be allowed to route on the\n active lot.\n If False, then npc in this role will not be allowed to route on the\n active lot, unless they are already on the lot when the role\n state is activated.\n \n This flag is ignored for player sims and npcs who live on the\n active lot.\n \n e.g. ambient walkby sims should not be routing on the active lot\n because that is rude.\n ', tunable_type=bool, default=True), '_autonomy_state_override': OptionalTunable( description= '\n If tuned, will force role sims into a specific autonomy state.\n Please consult your GPE partner before using this.\n ', tunable=TunableEnumEntry(tunable_type=AutonomyState, default=AutonomyState.LIMITED_ONLY, invalid_enums=(AutonomyState.MEDIUM, )), tuning_filter=FilterTag.EXPERT_MODE), '_crafting_process_override': TunableEnumEntry( description= '\n The override option of who to assign ownership of objects made\n by Sims in this role state.\n ', tunable_type=RoleStateCraftingOwnershipOverride, default=RoleStateCraftingOwnershipOverride.NO_OVERRIDE), 'always_active': Tunable( description= "\n If set to True, this role will always be allowed to be active\n when set on a Sim, regardless of whether or not it is \n lower priority than the Sim's other currently active roles. \n Use for roles that are important but retuning priority for it \n and/or other roles isn't feasible.\n \n Consult a GPE before you set this to True.\n This is not to be used lightly and there may be other options\n like situation exclusivity that can be explored before you\n go down this route.\n \n e.g. Sim is possessed which runs at HIGH priority.\n Sim wants to go visit an NPC residential lot, which places\n Sim in NORMAL priority Role_UngreetedPlayerVisitingNPC, which\n sets portal disallowance and adds specific buffs.\n \n We actually want Role_UngreetedPlayerVisitingNPC to run\n even though the role priority is now HIGH, because \n otherwise a possessed Sim visiting an NPC would magically\n be able to route through homes because portal disallowance\n is removed.\n ", tunable_type=bool, default=False) } @classmethod def _verify_tuning_callback(cls): for buff_ref in cls.buffs: if buff_ref is None: logger.error( '{} has empty buff in buff list. Please fix tuning.', cls) elif buff_ref.buff_type._temporary_commodity_info is not None: logger.error( '{} has a buff {} that has a temporary commodity.', cls, buff_ref.buff_type) @classproperty def role_priority(cls): return cls._role_priority @classproperty def buffs(cls): return cls._buffs @classproperty def off_lot_autonomy_buff(cls): return cls._off_lot_autonomy_buff @classproperty def role_specific_affordances(cls): return cls.role_affordances @classproperty def allow_npc_routing_on_active_lot(cls): return cls._allow_npc_routing_on_active_lot @classproperty def autonomy_state_override(cls): return cls._autonomy_state_override @classproperty def on_activate(cls): return cls._on_activate @classproperty def portal_disallowance_tags(cls): return cls._portal_disallowance_tags @classproperty def has_full_permissions(cls): current_venue = services.get_current_venue() if current_venue and current_venue.allow_rolestate_routing_on_navmesh: return True return not cls._portal_disallowance_tags and cls._allow_npc_routing_on_active_lot def _get_target_for_push_affordance(self, situation_target, situation=None, role_affordance_target=None): if situation_target == SituationAffordanceTarget.NO_TARGET: return if situation_target == SituationAffordanceTarget.CRAFTED_OBJECT: return role_affordance_target if situation_target == SituationAffordanceTarget.TARGET_OBJECT and situation is not None: return situation.get_target_object() if situation_target == SituationAffordanceTarget.CREATED_OBJECT and situation is not None: return situation.get_created_object() logger.error( 'Unable to resolve target when trying to push affordance on role state {} activate. requested target type was {}', self, self._on_activate.target) @classproperty def active_household_crafting_override(cls): return cls._crafting_process_override == RoleStateCraftingOwnershipOverride.ACTIVE_HOUSEHOLD @classproperty def lot_owner_crafting_override(cls): return cls._crafting_process_override == RoleStateCraftingOwnershipOverride.LOT_OWNER
class Skill(HasTunableReference, statistics.continuous_statistic_tuning.TunedContinuousStatistic, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager( sims4.resources.Types.STATISTIC)): __qualname__ = 'Skill' SKILL_LEVEL_LIST = TunableMapping( key_type=TunableEnumEntry(SkillLevelType, SkillLevelType.MAJOR), value_type=TunableList( Tunable(int, 0), description= 'The level boundaries for skill type, specified as a delta from the previous value' ), export_modes=ExportModes.All) SKILL_EFFECTIVENESS_GAIN = TunableMapping( key_type=TunableEnumEntry(SkillEffectiveness, SkillEffectiveness.STANDARD), value_type=TunableCurve(), description='Skill gain points based on skill effectiveness.') DYNAMIC_SKILL_INTERVAL = TunableRange( description= '\n Interval used when dynamic loot is used in a\n PeriodicStatisticChangeElement.\n ', tunable_type=float, default=1, minimum=1) INSTANCE_TUNABLES = { 'stat_name': TunableLocalizedString( description= '\n Localized name of this Statistic\n ', export_modes=ExportModes.All), 'ad_data': TunableList( description= '\n A list of Vector2 points that define the desire curve for this\n commodity.\n ', tunable=TunableVector2( description= '\n Point on a Curve\n ', default=sims4.math.Vector2(0, 0))), 'weight': Tunable( description= "\n The weight of the Skill with regards to autonomy. It's ignored \n for the purposes of sorting stats, but it's applied when scoring \n the actual statistic operation for the SI.\n ", tunable_type=float, default=0.5), 'skill_level_type': TunableEnumEntry( description='\n Skill level list to use.\n ', tunable_type=SkillLevelType, default=SkillLevelType.MAJOR, export_modes=ExportModes.All), 'locked_description': TunableLocalizedString( description= "\n The skill description when it's locked.\n ", export_modes=ExportModes.All), 'skill_description': TunableLocalizedString( description= "\n The skill's normal description.\n ", export_modes=ExportModes.All), 'is_default': Tunable( description= '\n Whether Sim will default has this skill.\n ', tunable_type=bool, default=False), 'genders': TunableSet( description= '\n Skill allowed gender, empty set means not specified\n ', tunable=TunableEnumEntry(tunable_type=sim_info_types.Gender, default=None, export_modes=ExportModes.All)), 'ages': TunableSet( description= '\n Skill allowed ages, empty set means not specified\n ', tunable=TunableEnumEntry(tunable_type=sim_info_types.Age, default=None, export_modes=ExportModes.All)), 'entitlement': TunableEntitlement( description= '\n Entitlement required to use this skill.\n ' ), 'icon': TunableResourceKey( description= '\n Icon to be displayed for the Skill.\n ', default='PNG:missing_image', resource_types=sims4.resources.CompoundTypes.IMAGE, export_modes=ExportModes.All), 'tags': TunableList( description= '\n The associated categories of the skill\n ', tunable=TunableEnumEntry(tunable_type=tag.Tag, default=tag.Tag.INVALID)), 'priority': Tunable( description= '\n Skill priority. Higher priority skill will trump other skills when\n being displayed on the UI side. When a sim gains multiple skills at\n the same time only the highest priority one will display a progress\n bar over its head.\n ', tunable_type=int, default=1, export_modes=ExportModes.All), 'statistic_multipliers': TunableMapping( description= '\n Multipliers this skill applies to other statistics based on its\n value.\n ', key_type=TunableReference( description= '\n The statistic this multiplier will be applied to.\n ', manager=services.statistic_manager(), reload_dependent=True), value_type=TunableTuple( curve=TunableCurve( description= '\n Tunable curve where the X-axis defines the skill level, and\n the Y-axis defines the associated multiplier.\n ', x_axis_name='Skill Level', y_axis_name='Multiplier'), direction=TunableEnumEntry( description= "\n Direction where the multiplier should work on the\n statistic. For example, a tuned decrease for an object's\n brokenness rate will not also increase the time it takes to\n repair it.\n ", tunable_type=StatisticChangeDirection, default=StatisticChangeDirection.INCREASE), use_effective_skill=Tunable( description= '\n If checked, this modifier will look at the current\n effective skill value. If unchecked, this modifier will\n look at the actual skill value.\n ', tunable_type=bool, needs_tuning=True, default=True)), tuning_group=GroupNames.MULTIPLIERS), 'success_chance_multipliers': TunableList( description= '\n Multipliers this skill applies to the success chance of\n affordances.\n ', tunable=TunableSkillMultiplier(), tuning_group=GroupNames.MULTIPLIERS), 'monetary_payout_multipliers': TunableList( description= '\n Multipliers this skill applies to the monetary payout amount of\n affordances.\n ', tunable=TunableSkillMultiplier(), tuning_group=GroupNames.MULTIPLIERS), 'next_level_teaser': TunableList( description= '\n Tooltip which describes what the next level entails.\n ', tunable=TunableLocalizedString(), export_modes=(ExportModes.ClientBinary, )), 'level_data': TunableMapping( description= '\n Level-specific information, such as notifications to be displayed to\n level up.\n ', key_type=int, value_type=TunableTuple( level_up_notification=UiDialogNotification.TunableFactory( description= '\n The notification to display when the Sim obtains this level.\n The text will be provided two tokens: the Sim owning the\n skill and a number representing the 1-based skill level\n ', locked_args={ 'text_tokens': DEFAULT, 'icon': None, 'primary_icon_response': UiDialogResponse(text=None, ui_request=UiDialogResponse. UiDialogUiRequest.SHOW_SKILL_PANEL), 'secondary_icon': None }), level_up_screen_slam=OptionalTunable( description= '\n Screen slam to show when reaches this skill level.\n Localization Tokens: Sim - {0.SimFirstName}, Skill Name - \n {1.String}, Skill Number - {2.Number}\n ', tunable=ui.screen_slam.TunableScreenSlamSnippet(), tuning_group=GroupNames.UI))), 'mood_id': TunableReference( description= '\n When this mood is set and active sim matches mood, the UI will \n display a special effect on the skill bar to represent that this \n skill is getting a bonus because of the mood.\n ', manager=services.mood_manager(), export_modes=ExportModes.All), 'stat_asm_param': TunableStatAsmParam.TunableFactory(), 'tutorial': TunableReference( description= '\n Tutorial instance for this skill. This will be used to bring up the \n skill lesson from the first notification for Sim to know this skill.\n ', manager=services.get_instance_manager( sims4.resources.Types.TUTORIAL), class_restrictions=('Tutorial', )), 'skill_unlocks_on_max': TunableList( description= '\n A list of skills that become unlocked when this skill is maxed.\n ', tunable=TunableReference( description= '\n A skill to unlock.\n ', manager=services.get_instance_manager( sims4.resources.Types.STATISTIC), class_restrictions=('Skill', ))) } REMOVE_INSTANCE_TUNABLES = ('min_value_tuning', 'max_value_tuning', 'decay_rate', '_default_convergence_value') def __init__(self, tracker): super().__init__(tracker, self.initial_value) self._delta_enabled = True self._callback_handle = None if self.tracker.owner.is_simulating: self.on_initial_startup() self._max_level_update_sent = False def on_initial_startup(self): if self.tracker.owner.is_selectable: self.refresh_level_up_callback() def on_remove(self, on_destroy=False): super().on_remove(on_destroy=on_destroy) self._destory_callback_handle() def _apply_multipliers_to_continuous_statistics(self): for stat in self.statistic_multipliers: while stat.continuous: owner_stat = self.tracker.get_statistic(stat) if owner_stat is not None: owner_stat._recalculate_modified_decay_rate() @caches.cached def get_user_value(self): return super(Skill, self).get_user_value() def set_value(self, value, *args, from_load=False, interaction=None, **kwargs): old_value = self.get_value() super().set_value(value, *args, **kwargs) self.get_user_value.cache.clear() if not from_load: new_value = self.get_value() new_level = self.convert_to_user_value(value) if old_value == self.initial_value and old_value != new_value: sim_info = self._tracker._owner services.get_event_manager().process_event( test_events.TestEvent.SkillLevelChange, sim_info=sim_info, statistic=self.stat_type) old_level = self.convert_to_user_value(old_value) if old_level < new_level: self._apply_multipliers_to_continuous_statistics() def add_value(self, add_amount, interaction=None, **kwargs): old_value = self.get_value() if old_value == self.initial_value: telemhook = TELEMETRY_HOOK_SKILL_INTERACTION_FIRST_TIME else: telemhook = TELEMETRY_HOOK_SKILL_INTERACTION super().add_value(add_amount, interaction=interaction) self.get_user_value.cache.clear() if interaction is not None: self.on_skill_updated(telemhook, old_value, self.get_value(), interaction.affordance.__name__) def _update_value(self): old_value = self._value if gsi_handlers.sim_handlers_log.skill_change_archiver.enabled: last_update = self._last_update time_delta = super()._update_value() self.get_user_value.cache.clear() new_value = self._value if old_value == self.initial_value: telemhook = TELEMETRY_HOOK_SKILL_INTERACTION_FIRST_TIME self.on_skill_updated(telemhook, old_value, new_value, TELEMETRY_INTERACTION_NOT_AVAILABLE) sim_info = self._tracker._owner services.get_event_manager().process_event( test_events.TestEvent.SkillLevelChange, sim_info=sim_info, statistic=self.stat_type) old_level = self.convert_to_user_value(old_value) new_level = self.convert_to_user_value(new_value) if gsi_handlers.sim_handlers_log.skill_change_archiver.enabled and self.tracker.owner.is_sim: gsi_handlers.sim_handlers_log.archive_skill_change( self.tracker.owner, self, time_delta, old_value, new_value, new_level, last_update) if old_value < new_value and old_level < new_level: if self._tracker is not None: self._tracker.notify_watchers(self.stat_type, self._value, self._value) def on_skill_updated(self, telemhook, old_value, new_value, affordance_name): owner_sim = self._tracker._owner if owner_sim.is_selectable: with telemetry_helper.begin_hook(skill_telemetry_writer, telemhook, sim=owner_sim) as hook: hook.write_guid(TELEMETRY_FIELD_SKILL_ID, self.guid64) hook.write_string(TELEMETRY_FIELD_SKILL_AFFORDANCE, affordance_name) hook.write_bool(TELEMETRY_FIELD_SKILL_AFFORDANCE_SUCCESS, True) hook.write_int(TELEMETRY_FIELD_SKILL_AFFORDANCE_VALUE_ADD, new_value - old_value) if old_value == self.initial_value: skill_level = self.convert_to_user_value(old_value) self._show_level_notification(skill_level) def _destory_callback_handle(self): if self._callback_handle is not None: self.remove_callback(self._callback_handle) self._callback_handle = None def refresh_level_up_callback(self): self._destory_callback_handle() def _on_level_up_callback(stat_inst): new_level = stat_inst.get_user_value() old_level = new_level - 1 stat_inst.on_skill_level_up(old_level, new_level) stat_inst.refresh_level_up_callback() self._callback_handle = self.add_callback( Threshold(self._get_next_level_bound(), operator.ge), _on_level_up_callback) def on_skill_level_up(self, old_level, new_level): tracker = self.tracker sim_info = tracker._owner if self.reached_max_level: for skill in self.skill_unlocks_on_max: skill_instance = tracker.add_statistic(skill, force_add=True) skill_instance.set_value(skill.initial_value) with telemetry_helper.begin_hook(skill_telemetry_writer, TELEMETRY_HOOK_SKILL_LEVEL_UP, sim=sim_info) as hook: hook.write_guid(TELEMETRY_FIELD_SKILL_ID, self.guid64) hook.write_int(TELEMETRY_FIELD_SKILL_LEVEL, new_level) if sim_info.account is not None: services.social_service.post_skill_message(sim_info, self, old_level, new_level) self._show_level_notification(new_level) services.get_event_manager().process_event( test_events.TestEvent.SkillLevelChange, sim_info=sim_info, statistic=self.stat_type) def _show_level_notification(self, skill_level): sim_info = self._tracker._owner if not sim_info.is_npc: level_data = self.level_data.get(skill_level) if level_data is not None: tutorial_id = None if self.tutorial is not None and skill_level == 1: tutorial_id = self.tutorial.guid64 notification = level_data.level_up_notification( sim_info, resolver=SingleSimResolver(sim_info)) notification.show_dialog(icon_override=(self.icon, None), secondary_icon_override=(None, sim_info), additional_tokens=(skill_level, ), tutorial_id=tutorial_id) if level_data.level_up_screen_slam is not None: level_data.level_up_screen_slam.send_screen_slam_message( sim_info, sim_info, self.stat_name, skill_level) @classproperty def skill_type(cls): return cls @classproperty def remove_on_convergence(cls): return False @classmethod def can_add(cls, owner, force_add=False, **kwargs): if force_add: return True if cls.genders and owner.gender not in cls.genders: return False if cls.ages and owner.age not in cls.ages: return False if cls.entitlement is None: return True if owner.is_npc: return False return mtx.has_entitlement(cls.entitlement) @classmethod def get_level_list(cls): return cls.SKILL_LEVEL_LIST.get(cls.skill_level_type) @classmethod def get_max_skill_value(cls): level_list = cls.get_level_list() return sum(level_list) @classmethod def get_skill_value_for_level(cls, level): level_list = cls.get_level_list() if level > len(level_list): logger.error('Level {} out of bounds', level) return 0 return sum(level_list[:level]) @classmethod def get_skill_effectiveness_points_gain(cls, effectiveness_level, level): skill_gain_curve = cls.SKILL_EFFECTIVENESS_GAIN.get( effectiveness_level) if skill_gain_curve is not None: return skill_gain_curve.get(level) logger.error('{} does not exist in SKILL_EFFECTIVENESS_GAIN mapping', effectiveness_level) return 0 @classmethod def _tuning_loaded_callback(cls): super()._tuning_loaded_callback() level_list = cls.get_level_list() cls.max_level = len(level_list) cls.min_value_tuning = 0 cls.max_value_tuning = sum(level_list) cls._default_convergence_value = cls.min_value_tuning cls._build_utility_curve_from_tuning_data(cls.ad_data) for stat in cls.statistic_multipliers: multiplier = cls.statistic_multipliers[stat] curve = multiplier.curve direction = multiplier.direction use_effective_skill = multiplier.use_effective_skill stat.add_skill_based_statistic_multiplier(cls, curve, direction, use_effective_skill) for multiplier in cls.success_chance_multipliers: curve = multiplier.curve use_effective_skill = multiplier.use_effective_skill for affordance in multiplier.affordance_list: affordance.add_skill_multiplier( affordance.success_chance_multipliers, cls, curve, use_effective_skill) for multiplier in cls.monetary_payout_multipliers: curve = multiplier.curve use_effective_skill = multiplier.use_effective_skill for affordance in multiplier.affordance_list: affordance.add_skill_multiplier( affordance.monetary_payout_multipliers, cls, curve, use_effective_skill) @classmethod def _verify_tuning_callback(cls): success_multiplier_affordances = [] for multiplier in cls.success_chance_multipliers: success_multiplier_affordances.extend(multiplier.affordance_list) if len(success_multiplier_affordances) != len( set(success_multiplier_affordances)): logger.error( "The same affordance has been tuned more than once under {}'s success multipliers, and they will overwrite each other. Please fix in tuning.", cls, owner='tastle') monetary_payout_multiplier_affordances = [] for multiplier in cls.monetary_payout_multipliers: monetary_payout_multiplier_affordances.extend( multiplier.affordance_list) if len(monetary_payout_multiplier_affordances) != len( set(monetary_payout_multiplier_affordances)): logger.error( "The same affordance has been tuned more than once under {}'s monetary payout multipliers, and they will overwrite each other. Please fix in tuning.", cls, owner='tastle') @classmethod def convert_to_user_value(cls, value): if not cls.get_level_list(): return 0 current_value = value for (level, level_threshold) in enumerate(cls.get_level_list()): current_value -= level_threshold while current_value < 0: return level return level + 1 @classmethod def convert_from_user_value(cls, user_value): (level_min, _) = cls._get_level_bounds(user_value) return level_min @classmethod def _get_level_bounds(cls, level): level_list = cls.get_level_list() level_min = sum(level_list[:level]) if level < cls.max_level: level_max = sum(level_list[:level + 1]) else: level_max = sum(level_list) return (level_min, level_max) def _get_next_level_bound(self): level = self.convert_to_user_value(self._value) (_, level_max) = self._get_level_bounds(level) return level_max @property def reached_max_level(self): max_value = self.get_max_skill_value() if self.get_value() >= max_value: return True return False @property def should_send_update(self): if not self.reached_max_level: return True if not self._max_level_update_sent: self._max_level_update_sent = True return True return False @classproperty def is_skill(cls): return True @classproperty def autonomy_weight(cls): return cls.weight @classmethod def create_skill_update_msg(cls, sim_id, stat_value): if not cls.convert_to_user_value(stat_value) > 0: return skill_msg = Commodities_pb2.Skill_Update() skill_msg.skill_id = cls.guid64 skill_msg.curr_points = int(stat_value) skill_msg.sim_id = sim_id return skill_msg @property def is_initial_value(self): return self.initial_value == self.get_value() @classproperty def valid_for_stat_testing(cls): return True
class RabbitHole(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager( sims4.resources.Types.RABBIT_HOLE)): INSTANCE_TUNABLES = { 'affordance': TunableReference( description= ' \n The rabbit hole affordance. This affordance must have a tuned rabbit\n hole liability and must use a rabbit hole exit condition.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION)), 'away_action': OptionalTunable( description= '\n If tuned, an away action for the rabbit holed sim info to run. If\n not tuned, no away actions will be started.\n ', tunable=TunableReference(manager=services.get_instance_manager( sims4.resources.Types.AWAY_ACTION)), enabled_by_default=True), 'go_home_and_attend': OptionalTunable( description= '"\n If tuned, this affordance will run when a sim needs to go home to\n attend a rabbit hole. If not tuned, the sim will use the generic\n travel. This only needs to be tuned in cases where we need special\n travel behavior (like different constraints).\n ', tunable=TunableReference( manager=services.get_instance_manager( sims4.resources.Types.INTERACTION), class_restrictions=('GoHomeTravelInteraction', ))), 'loot_list': TunableList( description= "\n Loots to apply to rabbit holed sim after they leave the \n rabbit hole. Won't be applied if the rabbit hole is cancelled.\n ", tunable=TunableReference(manager=services.get_instance_manager( sims4.resources.Types.ACTION), class_restrictions=('LootActions', ), pack_safe=True)), 'exit_conditions': TunableList( description= '\n A list of exit conditions for this rabbit hole. When exit\n conditions are met then the rabbit hole ends.\n ', tunable=TunableTuple( conditions=TunableList( description= '\n A list of conditions that all must be satisfied for the\n group to be considered satisfied.\n ', tunable=TunableRabbitHoleCondition( description= '\n A condition that must be satisfied.\n ' )), tests=TunableTestSet( description= '\n A set of tests. If these tests do not pass, this condition\n will not be attached.\n ' ))), 'time_tracking_policy': TunableEnumEntry( description= "\n This option determines how a rabbit hole will keep track of \n duration:\n COUNT_ALL_TIME - This rabbit hole's duration will begin when this\n rabbit hole is first pushed. This should be used if the rabbit\n hole's duration is supposed to point to a specific time. For\n instance, if I know a sim has an audition between 1pm-2pm, I will\n push them into a rabbit hole at 1pm with a duration of 1 hour. Now\n imagine my sim is busy at class till 1:45pm. When they are done \n with class, they should go to the audition. At this point, there\n should be 15 minutes left in the audition and not 1 hour left. This\n is because we decided to COUNT_ALL_TIME for the audition rabbit \n hole.\n COUNT_ACTIVE_TIME - This rabbit hole's duration will begin when the\n sim enters it. Continuing from the above example, the audition\n rabbit hole would end at 2:45pm and not 2:00pm if it had been tuned\n to COUNT_ACTIVE_TIME since it only became active at 1:45pm.\n ", tunable_type=RabbitHoleTimingPolicy, default=RabbitHoleTimingPolicy.COUNT_ACTIVE_TIME), 'tested_affordances': TunableList( description= "\n A list of test sets to run to choose the affordance to do for this\n rabbit hole. If an affordance is found from this list, the sim will be\n instantiated into this zone if not already and pushed to do the found\n affordance, so tests should fail out if you do not want a sim to move\n zones.\n \n If no affordance is found from this list that pass the\n tests, normal rabbit hole affordance behavior will take over, running\n either 'affordance' if at home or 'go_home_and_attend' if not at home.\n \n These tests are run when Sim is being added to a rabbit hole and also\n on zone spin-up to check if we need to bring this Sim into the new zone to\n put them into the rabbit hole in the new zone.\n ", tunable=TunableTuple( tests=TunableTestSet( description= '\n A set of tests that if passed will make this the affordance that is\n run for the rabbit hole.\n ' ), affordance=TunableReference( description= '\n The rabbit hole affordance for this test set. This affordance must have a tuned rabbit\n hole liability and must use a rabbit hole exit condition. \n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION)))) } def __init__(self, sim_id, rabbit_hole_id=None, starting_phase=RabbitHolePhase.STARTING, picked_skill=None): self.rabbit_hole_id = rabbit_hole_id or id_generator.generate_object_id( ) self.sim_id = sim_id self.alarm_handle = None self.callbacks = CallableList() self.linked_rabbit_holes = [] self.picked_skill = picked_skill self.ignore_travel_cancel_callbacks = False self.current_phase = starting_phase self._selected_affordance = None self.time_remaining_on_load = None @property def sim(self): return services.sim_info_manager().get(self.sim_id) @property def target(self): pass @flexmethod def get_participant(cls, inst, participant_type=ParticipantType.Actor, **kwargs): inst_or_cl = inst if inst is not None else cls participants = inst_or_cl.get_participants( participant_type=participant_type, **kwargs) if not participants: return if len(participants) > 1: raise ValueError('Too many participants returned for {}!'.format( participant_type)) return next(iter(participants)) @flexmethod def get_participants(cls, inst, participant_type, *args, **kwargs): if inst: sim_info = inst.sim if participant_type is ParticipantType.Actor: return (sim_info, ) else: if participant_type is ParticipantType.Lot: (services.get_zone(sim_info.zone_id, allow_uninstantiated_zones=True), ) if participant_type is ParticipantType.PickedStatistic: return (inst.picked_skill, ) def is_valid_to_restore(self, sim_info): return True def save(self, rabbit_hole_data): if self.alarm_handle is not None: rabbit_hole_data.time_remaining = self.alarm_handle.get_remaining_time( ).in_ticks() picked_stat = self.get_participant(ParticipantType.PickedStatistic) if picked_stat is not None: rabbit_hole_data.picked_stat_id = picked_stat.guid64 rabbit_hole_data.phase = self.current_phase def load(self, rabbit_hole_data): if rabbit_hole_data.HasField('time_remaining'): self.time_remaining_on_load = date_and_time.TimeSpan( rabbit_hole_data.time_remaining) if rabbit_hole_data.HasField('picked_stat_id'): self.picked_skill = services.get_instance_manager( sims4.resources.Types.STATISTIC).get( rabbit_hole_data.picked_stat_id) if rabbit_hole_data.HasField('phase'): self.current_phase = RabbitHolePhase(rabbit_hole_data.phase) else: self.current_phase = RabbitHolePhase.ACTIVE def on_restore(self): self._selected_affordance = None self.ignore_travel_cancel_callbacks = False def on_activate(self): pass def on_remove(self, canceled=False): self.callbacks(canceled=canceled) def select_affordance(self): if self._selected_affordance is not None: return self._selected_affordance sim_info = services.sim_info_manager().get(self.sim_id) resolver = SingleSimResolver(sim_info) for tested_affordance_tuning in self.tested_affordances: if tested_affordance_tuning.tests.run_tests(resolver): self._selected_affordance = tested_affordance_tuning.affordance return tested_affordance_tuning.affordance if sim_info.is_at_home: self._selected_affordance = self.affordance return self.affordance def select_travel_affordance(self): return self.go_home_and_attend def set_expiration_alarm(self, callback): if self.time_tracking_policy is RabbitHoleTimingPolicy.NO_TIME_LIMIT: logger.error( "Expiration timer is trying to be set for a rabbit hole {} that doesn't support it.", self) return if self.alarm_handle is not None: time_remaining = self.alarm_handle.get_remaining_time() self.alarm_handle = alarms.add_alarm(self, time_remaining, callback, cross_zone=True) return if self.time_remaining_on_load is not None: self.alarm_handle = alarms.add_alarm(self, self.time_remaining_on_load, callback, cross_zone=True) return else: duration = self._get_duration() if duration is not None: self.alarm_handle = alarms.add_alarm(self, duration, callback, cross_zone=True) return def _get_duration(self): affordance = self.select_affordance() if affordance is not None: for conditional_action in affordance.basic_content.conditional_actions: for condition in conditional_action.conditions: if hasattr( condition._tuned_values, 'min_time') and hasattr( condition._tuned_values, 'max_time'): min_time = condition._tuned_values.min_time max_time = condition._tuned_values.max_time tuned_minutes = random.uniform(min_time, max_time) return date_and_time.create_time_span( minutes=tuned_minutes)
class GameRules(metaclass=TunedInstanceMetaclass, manager=services.get_instance_manager( sims4.resources.Types.GAME_RULESET)): __qualname__ = 'GameRules' INSTANCE_TUNABLES = { 'game_name': TunableLocalizedStringFactory( description='\n Name of the game.\n ', default=1860708663), 'teams_per_game': TunableInterval( description= '\n An interval specifying the number of teams allowed per game.\n \n Joining Sims are put on a new team if the maximum number of teams\n has not yet been met, otherwise they are put into the team with the\n fewest number of players.\n ', tunable_type=int, default_lower=2, default_upper=2, minimum=1), 'players_per_game': TunableInterval( description= '\n An interval specifying the number of players allowed per game.\n \n If the maximum number of players has not been met, Sims can\n continue to join a game. Joining Sims are put on a new team if the\n maximum number of teams as specified in the "teams_per_game"\n tunable has not yet been met, otherwise they are put into the team\n with the fewest number of players.\n ', tunable_type=int, default_lower=2, default_upper=2, minimum=1), 'players_per_turn': TunableRange( description= '\n An integer specifying number of players from the active team who\n take their turn at one time.\n ', tunable_type=int, default=1, minimum=1), 'initial_state': ObjectStateValue.TunableReference( description= "\n The game's starting object state.\n "), 'score_info': TunableTuple( description= "\n Tunables that affect the game's score.\n ", winning_score=Tunable( description= '\n An integer value specifying at what score the game will end.\n ', tunable_type=int, default=100), score_increase=TunableInterval( description= '\n An interval specifying the minimum and maximum score increases\n possible in one turn. A random value in this interval will be\n generated each time score loot is given.\n ', tunable_type=int, default_lower=35, default_upper=50, minimum=0), skill_level_bonus=Tunable( description= "\n A bonus number of points based on the Sim's skill level in the\n relevant_skill tunable that will be added to score_increase.\n \n ex: If this value is 2 and the Sim receiving score has a\n relevant skill level of 4, they will receive 8 (2 * 4) extra\n points.\n ", tunable_type=float, default=2), relevant_skill=Skill.TunableReference( description= "\n The skill relevant to this game. Each Sim's proficiency in\n this skill will effect the score increase they get.\n " ), use_effective_skill_level=Tunable( description= '\n If checked, we will use the effective skill level rather than\n the actual skill level of the relevant_skill tunable.\n ', tunable_type=bool, default=True), progress_stat=Statistic.TunableReference( description= '\n The statistic that advances the progress state of this game.\n ' )), 'clear_score_on_player_join': Tunable( description= '\n Tunable that, when checked, will clear the game score when a player joins.\n \n This essentially resets the game.\n ', tunable_type=bool, default=False), 'alternate_target_object': OptionalTunable( description= '\n Tunable that, when enabled, means the game should create an alternate object\n in the specified slot on setup that will be modified as the game goes on\n and destroyed when the game ends.\n ', tunable=TunableTuple( target_game_object=TunableReference( description= '\n The definition of the object that will be created/destroyed/altered\n by the game.\n ', manager=services.definition_manager()), parent_slot=TunableVariant( description= '\n The slot on the parent object where the target_game_object object should go. This\n may be either the exact name of a bone on the parent object or a\n slot type, in which case the first empty slot of the specified type\n in which the child object fits will be used.\n ', by_name=Tunable( description= '\n The exact name of a slot on the parent object in which the target\n game object should go. \n ', tunable_type=str, default='_ctnm_'), by_reference=TunableReference( description= '\n A particular slot type in which the target game object should go. The\n first empty slot of this type found on the parent will be used.\n ', manager=services.get_instance_manager( sims4.resources.Types.SLOT_TYPE))))) }
class OutfitMixin: __qualname__ = 'OutfitMixin' INSTANCE_TUNABLES = { 'outfit_category_map': TunableMapping( key_type=TunableEnumEntry( OutfitCategory, OutfitCategory.EVERYDAY, description='The outfit category to pull outfit indexes from'), value_type=TunableReference( services.get_instance_manager( sims4.resources.Types.PIE_MENU_CATEGORY), description= 'Pie menu category so we can display a submenu for each outfit category' )), 'current_outfit_tooltip': TunableLocalizedStringFactory( description= 'Greyed out tooltip that displays if the sim is currently wearing the selected outfit' ), 'outfit_and_index_interaction_name': TunableLocalizedStringFactory( description= 'A string that concatenates the outfit category localized string and the number of the outfit category index' ) } def __init__(self, aop, context, pie_menu_category=None, outfit_category=None, outfit_index=None, **kwargs): super().__init__(aop, context, **kwargs) self.pie_menu_category = pie_menu_category self.outfit_category = outfit_category self.outfit_index = outfit_index @flexmethod def get_pie_menu_category(cls, inst, pie_menu_category=None, **interaction_parameters): if inst is not None: return inst.pie_menu_category return pie_menu_category @staticmethod def _get_interaction_name(cls, outfit_category, outfit_index): localized_string = SimOutfits.OUTFIT_CATEGORY_TUNING.get( outfit_category).localized_category if localized_string is not None: return cls.outfit_and_index_interaction_name( localized_string(), outfit_index + 1) @flexmethod def _get_name(cls, inst, target=DEFAULT, context=DEFAULT, outfit_category=None, outfit_index=None, **interaction_parameters): if inst is not None: return cls._get_interaction_name(cls, inst.outfit_category, inst.outfit_index) return cls._get_interaction_name(cls, outfit_category, outfit_index) @classmethod def _shared_test(cls, sim, outfit_category, outfit_index): requested_outfit = (outfit_category, outfit_index) if sim.sim_info.get_current_outfit() == requested_outfit: return TestResult(False, 'Already in requested outfit', tooltip=cls.current_outfit_tooltip) return TestResult.TRUE @classmethod def _shared_potential_interactions(cls, sim, target, **kwargs): if sim is None: return outfit_category_map = cls.outfit_category_map for outfit_category in outfit_category_map: pie_menu_category = outfit_category_map[outfit_category] outfits = sim.sim_info.sim_outfits.outfits_in_category( outfit_category) index = 0 while outfits is not None: while True: for _ in outfits: yield AffordanceObjectPair( cls, target, cls, None, pie_menu_category=pie_menu_category, outfit_category=outfit_category, outfit_index=index, **kwargs) index += 1
class BaseFestivalState(HasTunableFactory, AutoFactoryInit): FACTORY_TUNABLES = { '_situations': TunableList( description= '\n The different Situations that should be running at this time.\n ', tunable=TunableTuple( description= '\n The Tunables for a single entry in this list. Each entry\n includes a minimum number of situations that we want to have\n running from this entry and a list of weighted Situations.\n We try and ensure a minimum number of the situations specified\n within the Situations list will exist.\n ', number_of_situations=TunableRange( description= '\n The number of situations that we want to have running from\n this entry. This is the Minimum number of situations that\n we try and maintain. If the number of situations exceeds\n this we will not destroy situations to reduce ourself to\n this value.\n ', tunable_type=int, default=1, minimum=1), object_tag_requirement=OptionalTunable( description= '\n If enabled then we will cap the number of situations\n created by this entry at either the minimum of objects\n created for this situation by tuned tag or the Number of\n Situations tunable.\n ', tunable=TunableEnumEntry( description= '\n A specific tag that an object on this lot must have for this\n situation to be allowed to start.\n ', tunable_type=Tag, default=Tag.INVALID, invalid_enums=(Tag.INVALID, ))), situations=TunableList( description= '\n A weighted list of situations that can be chosen for this\n state in the festival.\n ', tunable=TunableTuple( description= '\n A pair between a weight and a situation that can be\n chosen.\n ', weight=TunableRange( description= '\n Weight for each of the different situations that\n can be chosen.\n ', tunable_type=float, default=1, minimum=1), situation=TunableReference( description= '\n The situation that can be chosen. We will run any\n tests that GPEs have added to determine if this\n situation has been valid before actually selecting\n it.\n ', manager=services.get_instance_manager( sims4.resources.Types.SITUATION), class_restrictions=( BaseGenericFestivalSituation, )))))) } @classproperty def priority(cls): return OpenStreetDirectorPriority.FESTIVAL CHECK_SITUATIONS_ALARM_TIME = 10 CHECK_SITUATION_ALARM_KEY = 'check_situation_alarm' def __init__(self, owner, *args, **kwargs): super().__init__(*args, **kwargs) self._owner = owner self._check_situations_alarm_handle = None self._alarms = {} @classproperty def key(cls): raise NotImplementedError def _test_situation(self, situation): return situation.situation_meets_starting_requirements() def _create_required_number_of_situations(self): situation_manager = services.get_zone_situation_manager() running_situations = [ type(situation) for situation in self._owner.get_running_festival_situations() ] for situation_entry in self._situations: number_of_situations_running = 0 for situation_type_entry in situation_entry.situations: number_of_situations_running += running_situations.count( situation_type_entry.situation) if situation_entry.object_tag_requirement is None: required_situations = situation_entry.number_of_situations else: tagged_objects = 0 for obj in self._owner.get_all_layer_created_objects(): if build_buy.get_object_has_tag( obj.definition.id, situation_entry.object_tag_requirement): tagged_objects += 1 required_situations = min(tagged_objects, situation_entry.number_of_situations) if number_of_situations_running < required_situations: possible_situations = [ (situation_type_entry.weight, situation_type_entry.situation) for situation_type_entry in situation_entry.situations if self._test_situation(situation_type_entry.situation) ] if possible_situations: for _ in range(situation_entry.number_of_situations - number_of_situations_running): situation = weighted_random_item(possible_situations) guest_list = situation.get_predefined_guest_list() if guest_list is None: guest_list = SituationGuestList(invite_only=True) situation_id = situation_manager.create_situation( situation, guest_list=guest_list, spawn_sims_during_zone_spin_up=True, user_facing=False) self._owner._add_created_situation(situation_id) def _create_situations_callback(self, _): self._create_required_number_of_situations() def on_state_activated(self, reader=None, preroll_time_override=None): self.schedule_alarm(self.CHECK_SITUATION_ALARM_KEY, self.CHECK_SITUATIONS_ALARM_TIME, self._create_situations_callback, repeating=True, should_persist=False, reader=reader) def on_state_deactivated(self): for alarm_data in self._alarms.values(): alarms.cancel_alarm(alarm_data.alarm_handle) def on_layer_loaded(self, conditional_layer): pass def on_layer_objects_destroyed(self, conditional_layer): pass def save(self, writer): for (alarm_key, alarm_data) in self._alarms.items(): if alarm_data.should_persist: writer.write_float( alarm_key, alarm_data.alarm_handle.get_remaining_time().in_minutes()) def schedule_alarm(self, alarm_key, alarm_time, callback, repeating=False, should_persist=True, reader=None): if should_persist: if reader is not None: alarm_time = reader.read_float(alarm_key, alarm_time) alarm_handle = alarms.add_alarm(self, create_time_span(minutes=alarm_time), callback, repeating=repeating) self._alarms[alarm_key] = FestivalAlarmData(should_persist, alarm_handle) def _run_preroll(self): self._create_required_number_of_situations() def _get_fake_preroll_time(self): pass def _preroll_end_of_state(self): raise NotImplementedError def preroll(self, time_to_preroll): if time_to_preroll is None: return self._run_preroll() time_spent = self._get_fake_preroll_time() if time_spent is None: return TimeSpan(0) time_left = time_to_preroll - time_spent if time_left > TimeSpan.ZERO: self._preroll_end_of_state() return time_left
class CleanupObjectsFestivalState(BaseFestivalState): FACTORY_TUNABLES = { '_conditional_layers': TunableList( description= '\n A list of layers to be destroyed. Each one will load one after\n another.\n ', tunable=TunableReference( description= '\n The conditional layer that will be destroyed.\n ', manager=services.get_instance_manager( sims4.resources.Types.CONDITIONAL_LAYER)), unique_entries=True) } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._layers_to_destroy = [] def _get_next_state(self): raise NotImplementedError def _destroy_layers(self): self._layers_to_destroy = list(self._conditional_layers) if self._layers_to_destroy: for conditional_layer in tuple(self._layers_to_destroy): self._owner.remove_layer_objects(conditional_layer) else: self._next_state_or_destroy() def on_layer_objects_destroyed(self, conditional_layer): super().on_layer_objects_destroyed(conditional_layer) if self._owner._prerolling: return self._layers_to_destroy.remove(conditional_layer) if not self._layers_to_destroy: self._next_state_or_destroy() def _next_state_or_destroy(self): next_state = self._get_next_state() if next_state is not None: self._owner.change_state(next_state) else: self._owner._ready_for_destruction = True self._owner.self_destruct() def on_state_activated(self, reader=None, preroll_time_override=None): self._owner.run_lot_cleanup() super().on_state_activated(reader=reader, preroll_time_override=preroll_time_override) self._destroy_layers() def _run_preroll(self): for conditional_layer in self._layers_to_destroy: self._owner.remove_layer_objects(conditional_layer) super()._run_preroll() def _get_fake_preroll_time(self): return TimeSpan.ZERO def _preroll_end_of_state(self): next_state = self._get_next_state() if next_state is not None: self._owner.change_state(next_state) else: self._owner.self_destruct()
class StatisticTransferOp(StatisticOperation): __qualname__ = 'StatisticTransferOp' FACTORY_TUNABLES = { 'statistic_donor': TunableEnumEntry( description= '\n The owner of the statistic we are transferring the value from.\n ', tunable_type=ParticipantType, default=ParticipantType.TargetSim), 'transferred_stat': TunableReference( description= '\n The statistic whose value to transfer.\n ', manager=services.get_instance_manager( sims4.resources.Types.STATISTIC)), 'transfer_type': TunableEnumEntry( description= '\n Type of statistic transfer to use.\n ', tunable_type=TransferType, default=TransferType.ADDITIVE) } def __init__(self, statistic_donor=None, transferred_stat=None, transfer_type=None, **kwargs): super().__init__(**kwargs) self._statistic_donor = statistic_donor self._transferred_stat = transferred_stat self._transfer_type = transfer_type self._donors = None def __repr__(self): if self.stat is not None: return '<{}: {} transfer>'.format(self.__class__.__name__, self.stat.__name__) return '<{}: Stat is None in StatisticTransferOp>'.format( self.__class__.__name__) def get_value(self, obj=None, interaction=None, sims=None): return self.stat.get_value() def _apply_to_subject_and_target(self, subject, target, resolver): self._donors = resolver.get_participants(self._statistic_donor) super()._apply_to_subject_and_target(subject, target, resolver) def _apply(self, tracker, interaction=None): donors = self._donors if self._donors is not None else interaction.get_participants( self._statistic_donor) for donor in donors: transfer_value = donor.statistic_tracker.get_value( self._transferred_stat) if self._transfer_type == TransferType.ADDITIVE: tracker.add_value(self.stat, transfer_value, interaction=interaction) elif self._transfer_type == TransferType.SUBTRACTIVE: tracker.add_value(self.stat, -transfer_value, interaction=interaction) else: while self._transfer_type == TransferType.REPLACEMENT: tracker.set_value(self.stat, transfer_value, interaction=interaction)
class RelationshipOperation(StatisticOperation, BaseTargetedLootOperation): __qualname__ = 'RelationshipOperation' @staticmethod def _verify_tunable_callback(instance_class, tunable_name, source, relationship_operation=None, **tuned_values): if relationship_operation.target_participant_type is None or relationship_operation.target_participant_type == ParticipantType.Invalid: logger.error( 'Relationship Operation: {} has no Target Participant Type tuned.', instance_class) FACTORY_TUNABLES = { 'track': TunableReference( description='\n The track to be manipulated.', manager=services.get_instance_manager( sims4.resources.Types.STATISTIC), class_restrictions='RelationshipTrack'), 'track_range': TunableInterval( description= '\n The relationship track must > lower_bound and <= upper_bound for\n the operation to apply.', tunable_type=float, default_lower=-101, default_upper=100), 'locked_args': { 'advertise': False }, 'verify_tunable_callback': _verify_tunable_callback } DEFAULT_PARTICIPANT_ARGUMENTS = { 'subject_participant_type_options': { 'description': '\n The owner Sim for this relationship change. Relationship is\n applied to all Sims in this list, to all Sims in the Target\n Participant Type list\n ', 'use_flags_enum': True }, 'target_participant_type_options': { 'description': "\n The target Sim for this relationship change. Any\n relationship that would be given to 'self' is discarded.\n ", 'use_flags_enum': True } } def __init__(self, track_range=None, track=DEFAULT, **kwargs): super().__init__(**kwargs) self._track_range = track_range self._track = track if self._track is None: self._track = DEFAULT self._loot_type = LootType.RELATIONSHIP def get_stat(self, interaction, source=None, target=None): if source is None: actors = interaction.get_participants(self.subject) source = next(iter(actors)) if target is None: targets = interaction.get_participants( self.target_participant_type) for potential_target in targets: while potential_target is not source: target = potential_target break if target is None: return if isinstance(target, int): target_sim_id = target else: target_sim_id = target.sim_id return source.sim_info.relationship_tracker.get_relationship_track( target_sim_id, self._track, True) def _get_interval(self, aop): return StatisticOperation.STATIC_CHANGE_INTERVAL def _apply_to_subject_and_target(self, subject, target, resolver): source_sim_info = self._get_sim_info_from_participant(subject) if not source_sim_info: return target_sim_info = self._get_sim_info_from_participant(target) if not target_sim_info: return self._apply_to_sim_info(source_sim_info, target_sim_info.sim_id, resolver.interaction) self._apply_to_sim_info(target_sim_info, source_sim_info.sim_id, resolver.interaction) def _get_sim_info_from_participant(self, participant): if isinstance(participant, int): sim_info_manager = services.sim_info_manager() if sim_info_manager is None: return sim_info = sim_info_manager.get(participant) else: sim_info = getattr(participant, 'sim_info', participant) if sim_info is None: logger.error( 'Could not get Sim Info from {0} in StatisticAddRelationship loot op.', participant) return sim_info def _apply_to_sim_info(self, source_sim_info, target_sim_id, interaction): if self._track is DEFAULT: self._track = RelationshipGlobalTuning.REL_INSPECTOR_TRACK rel_stat = source_sim_info.relationship_tracker.get_relationship_track( target_sim_id, self._track, True) if rel_stat is not None: self._maybe_apply_op(rel_stat.tracker, source_sim_info, interaction=interaction) def _maybe_apply_op(self, tracker, target_sim, **kwargs): value = tracker.get_value(self._track) if self._track_range.lower_bound < value <= self._track_range.upper_bound: self._apply(tracker, target_sim, **kwargs)
class StatisticOperation(BaseLootOperation): __qualname__ = 'StatisticOperation' STATIC_CHANGE_INTERVAL = 1 DISPLAY_TEXT = TunableLocalizedStringFactory( description= '\n A string displaying the amount that this stat operation awards. It will\n be provided two tokens: the statistic name and the value change.\n ' ) DEFAULT_PARTICIPANT_ARGUMENTS = { 'subject_participant_type_options': { 'description': '\n The owner of the stat that we are operating on.\n ', 'use_flags_enum': True } } FACTORY_TUNABLES = { 'stat': TunableReference( description='\n The statistic we are operating on.', manager=services.get_instance_manager( sims4.resources.Types.STATISTIC)), 'advertise': Tunable( description= '\n This statistic operation should advertise to autonomy. This only\n advertises if the statistic operation is used as part of Periodic\n Statistic Change.\n ', tunable_type=bool, needs_tuning=True, default=True) } def __init__(self, stat=None, **kwargs): super().__init__(**kwargs) self._stat = stat self._ad_multiplier = 1 self._loot_type = LootType.GENERIC if self._stat is not None and issubclass(self._stat, Skill): self._loot_type = LootType.SKILL def __repr__(self): return '<{} {} {}>'.format( type(self).__name__, self.stat, self.subject) @property def stat(self): return self._stat @property def loot_type(self): return self._loot_type @property def ad_multiplier(self): return self._ad_multiplier def modify_ad_multiplier(self, multiplier): pass def _apply_to_subject_and_target(self, subject, target, resolver): stat = self.get_stat(None) if not subject.is_locked(stat): tracker = subject.get_tracker(stat) self._apply(tracker, interaction=resolver.interaction) def _apply(self, tracker, interaction=None): raise NotImplementedError def get_value(self, obj=None, interaction=None, sims=None): raise NotImplementedError def _attempt_to_get_real_stat_value(self, obj, interaction): if obj is None and interaction is not None: obj = interaction.get_participant(ParticipantType.Actor) if obj is not None: stat_value = obj.get_stat_value(self.stat) if stat_value is not None: return stat_value return self.stat.default_value def _get_interval(self, aop): return aop.super_affordance.approximate_duration def get_fulfillment_rate(self, interaction): if not self._advertise: return 0 value = self.get_value(interaction=interaction) if interaction.target is not None: value *= interaction.target.get_stat_multiplier( self.stat, self.subject) interval = self._get_interval(interaction) if interval <= 0: logger.error( 'Tuning error: affordance interval should be greater than 0 (defaulting to 1)' ) interval = 1 score = value / interval return score def _get_display_text(self): if self.stat.stat_name is not None: value = self.get_value() if value: return self.DISPLAY_TEXT(*self._get_display_text_tokens()) def _get_display_text_tokens(self): return (self.stat.stat_name, self.get_value())
class BuffForAmountOfTimeTest(event_testing.test_base.BaseTest): __qualname__ = 'BuffForAmountOfTimeTest' test_events = (TestEvent.BuffEndedEvent, TestEvent.BuffUpdateEvent) USES_DATA_OBJECT = True USES_EVENT_DATA = True class BuffTestType(enum.Int): __qualname__ = 'BuffForAmountOfTimeTest.BuffTestType' ANY_SINGLE_BUFF = 0 SUM_OF_BUFFS = 1 FACTORY_TUNABLES = { 'description': 'Test for the total amount of time that this buff has been on sims on this account.', 'buff_to_check': TunableList( TunableReference(services.get_instance_manager( sims4.resources.Types.BUFF), description='Buff checked for this test.')), 'length_of_time': TunableSimMinute( 1, description= 'The total length of time that should be checked against.'), 'buff_test_type': TunableEnumEntry( description= '\n The type determines how to handle multiple buffs in the list. "Any\n single buff" will test for the time threshold in each listed buff\n and return true if one meets it. "Sum of buffs" will add the time\n stored for each buff and test against the total. Note that using\n SUM OF BUFFS will accumulate time for all buffs in the list and\n does not separate out overlaps. So if two buffs in the list are on\n the Sim, time will accumulate twice as much during that period.\n ', tunable_type=BuffTestType, default=BuffTestType.ANY_SINGLE_BUFF) } def __init__(self, buff_to_check, length_of_time, buff_test_type, **kwargs): super().__init__(**kwargs) self.buffs_to_check = buff_to_check self.length_of_time = interval_in_sim_minutes(length_of_time) self.buff_test_type = buff_test_type def get_expected_args(self): return { 'buff': event_testing.test_events.FROM_EVENT_DATA, 'data_object': event_testing.test_events.FROM_DATA_OBJECT, 'objective_guid64': event_testing.test_events.OBJECTIVE_GUID64 } @cached_test def __call__(self, buff=None, data_object=None, objective_guid64=None): if buff is None: return TestResult( False, 'Buff provided is None, valid during zone load.') if buff not in self.buffs_to_check: return TestResult( False, 'Buff provided is not among the buffs you are looking for.') buff_uptime = TimeSpan(0) if self.buff_test_type == self.BuffTestType.SUM_OF_BUFFS: for buff_tuning in self.buffs_to_check: buff_uptime += data_object.get_total_buff_uptime(buff_tuning) else: buff_uptime = data_object.get_total_buff_uptime(buff) relative_start_value = data_object.get_starting_values( objective_guid64) if relative_start_value is not None: ticks = 0 buff_uptime -= TimeSpan(relative_start_value[ticks]) if buff_uptime >= self.length_of_time: return TestResult.TRUE run_time = self.length_of_time.in_hours() - (self.length_of_time - buff_uptime).in_hours() return TestResultNumeric( False, 'BuffForAmountOfTimeTest: Buff has not existed long enough.', current_value=run_time, goal_value=self.length_of_time.in_hours(), is_money=False) def save_relative_start_values(self, objective_guid64, data_object): buff_uptime = TimeSpan(0) for buff_tuning in self.buffs_to_check: buff_uptime += data_object.get_total_buff_uptime(buff_tuning) data_object.set_starting_values(objective_guid64, [buff_uptime.in_ticks()]) def tuning_is_valid(self): return len(self.buffs_to_check) > 0 def goal_value(self): return self.length_of_time.in_hours()
class TotalRelationshipBitTest(event_testing.test_base.BaseTest): __qualname__ = 'TotalRelationshipBitTest' test_events = (TestEvent.AddRelationshipBit, ) USES_DATA_OBJECT = True FACTORY_TUNABLES = { 'description': 'Gate availability by a relationship status.', 'use_current_relationships': Tunable( bool, False, description= 'Use the current number of relationships held at this bit rather than the total number ever had.' ), 'relationship_bits': TunableSet( TunableReference( services.relationship_bit_manager(), description='The relationship bit that will be checked.', class_restrictions='RelationshipBit')), 'num_relations': TunableThreshold( description= 'Number of Sims with specified relationships required to pass.') } def __init__(self, use_current_relationships, relationship_bits, num_relations, **kwargs): super().__init__(**kwargs) self.use_current_relationships = use_current_relationships self.relationship_bits = relationship_bits self.num_relations = num_relations def get_expected_args(self): return { 'data_object': event_testing.test_events.FROM_DATA_OBJECT, 'objective_guid64': event_testing.test_events.OBJECTIVE_GUID64 } @cached_test def __call__(self, data_object=None, objective_guid64=None): current_relationships = 0 for relationship_bit in self.relationship_bits: if self.use_current_relationships: current_relationships += data_object.get_current_total_relationships( relationship_bit) else: current_relationships += data_object.get_total_relationships( relationship_bit) relative_start_value = data_object.get_starting_values( objective_guid64) if relative_start_value is not None: relations = 0 current_relationships -= relative_start_value[relations] if not self.num_relations.compare(current_relationships): return TestResultNumeric( False, 'TotalRelationshipBitTest: Not enough relationships.', current_value=current_relationships, goal_value=self.num_relations.value, is_money=False) return TestResult.TRUE def save_relative_start_values(self, objective_guid64, data_object): current_relationships = 0 for relationship_bit in self.relationship_bits: if self.use_current_relationships: current_relationships += data_object.get_current_total_relationships( relationship_bit) else: current_relationships += data_object.get_total_relationships( relationship_bit) data_object.set_starting_values(objective_guid64, [current_relationships]) def tuning_is_valid(self): if self.relationship_bits: return True return False def goal_value(self): return self.num_relations.value
class PregnancyTracker(SimInfoTracker): PREGNANCY_COMMODITY_MAP = TunableMapping( description= '\n The commodity to award if conception is successful.\n ', key_type=TunableEnumEntry( description= '\n Species these commodities are intended for.\n ', tunable_type=Species, default=Species.HUMAN, invalid_enums=(Species.INVALID, )), value_type=TunableReference( description= '\n The commodity reference controlling pregnancy.\n ', pack_safe=True, manager=services.get_instance_manager( sims4.resources.Types.STATISTIC))) PREGNANCY_TRAIT = TunableReference( description= '\n The trait that all pregnant Sims have during pregnancy.\n ', manager=services.trait_manager()) PREGNANCY_ORIGIN_TRAIT_MAPPING = TunableMapping( description= '\n A mapping from PregnancyOrigin to a set of traits to be added at the\n start of the pregnancy, and removed at the end of the pregnancy.\n ', key_type=PregnancyOrigin, value_type=TunableTuple( description= '\n A tuple of the traits that should be added/removed with a pregnancy\n that has this origin, and the content pack they are associated with.\n ', traits=TunableSet( description= '\n The traits to be added/removed.\n ', tunable=Trait.TunablePackSafeReference()), pack=TunableEnumEntry( description= '\n The content pack associated with this set of traits. If the pack\n is uninstalled, the pregnancy will be auto-completed.\n ', tunable_type=Pack, default=Pack.BASE_GAME))) PREGNANCY_RATE = TunableRange( description='\n The rate per Sim minute of pregnancy.\n ', tunable_type=float, default=0.001, minimum=EPSILON) MULTIPLE_OFFSPRING_CHANCES = TunableList( description= '\n A list defining the probabilities of multiple births.\n ', tunable=TunableTuple( size=Tunable( description= '\n The number of offspring born.\n ', tunable_type=int, default=1), weight=Tunable( description= '\n The weight, relative to other outcomes.\n ', tunable_type=float, default=1), npc_dialog=UiDialogOk.TunableFactory( description= '\n A dialog displayed when a NPC Sim gives birth to an offspring\n that was conceived by a currently player-controlled Sim. The\n dialog is specifically used when this number of offspring is\n generated.\n \n Three tokens are passed in: the two parent Sims and the\n offspring\n ', locked_args={'text_tokens': None}), modifiers=TunableMultiplier.TunableFactory( description= '\n A tunable list of test sets and associated multipliers to apply\n to the total chance of this number of potential offspring.\n ' ), screen_slam_one_parent=OptionalTunable( description= '\n Screen slam to show when only one parent is available.\n Localization Tokens: Sim A - {0.SimFirstName}\n ', tunable=TunableScreenSlamSnippet()), screen_slam_two_parents=OptionalTunable( description= '\n Screen slam to show when both parents are available.\n Localization Tokens: Sim A - {0.SimFirstName}, Sim B -\n {1.SimFirstName}\n ', tunable=TunableScreenSlamSnippet()))) MONOZYGOTIC_OFFSPRING_CHANCE = TunablePercent( description= '\n The chance that each subsequent offspring of a multiple birth has the\n same genetics as the first offspring.\n ', default=50) GENDER_CHANCE_STAT = TunableReference( description= '\n A commodity that determines the chance that an offspring is female. The\n minimum value guarantees the offspring is male, whereas the maximum\n value guarantees it is female.\n ', manager=services.statistic_manager()) BIRTHPARENT_BIT = RelationshipBit.TunableReference( description= '\n The bit that is added on the relationship from the Sim to any of its\n offspring.\n ' ) AT_BIRTH_TESTS = TunableGlobalTestSet( description= '\n Tests to run between the pregnant sim and their partner, at the time of\n birth. If any test fails, the the partner sim will not be set as the\n other parent. This is intended to prevent modifications to the partner\n sim during the time between impregnation and birth that would make the\n partner sim an invalid parent (age too young, relationship incestuous, etc).\n ' ) PREGNANCY_ORIGIN_MODIFIERS = TunableMapping( description= '\n Define any modifiers that, given the origination of the pregnancy,\n affect certain aspects of the generated offspring.\n ', key_type=TunableEnumEntry( description= '\n The origin of the pregnancy.\n ', tunable_type=PregnancyOrigin, default=PregnancyOrigin.DEFAULT, pack_safe=True), value_type=TunableTuple( description= '\n The aspects of the pregnancy modified specifically for the specified\n origin.\n ', default_relationships=TunableTuple( description= '\n Override default relationships for the parents.\n ', father_override=OptionalTunable( description= '\n If set, override default relationships for the father.\n ', tunable=TunableEnumEntry( description= '\n The default relationships for the father.\n ', tunable_type=DefaultGenealogyLink, default=DefaultGenealogyLink.FamilyMember)), mother_override=OptionalTunable( description= '\n If set, override default relationships for the mother.\n ', tunable=TunableEnumEntry( description= '\n The default relationships for the mother.\n ', tunable_type=DefaultGenealogyLink, default=DefaultGenealogyLink.FamilyMember))), trait_entries=TunableList( description= '\n Sets of traits that might be randomly applied to each generated\n offspring. Each group is individually randomized.\n ', tunable=TunableTuple( description= '\n A set of random traits. Specify a chance that a trait from\n the group is selected, and then specify a set of traits.\n Only one trait from this group may be selected. If the\n chance is less than 100%, no traits could be selected.\n ', chance=TunablePercent( description= '\n The chance that a trait from this set is selected.\n ', default=100), traits=TunableList( description= '\n The set of traits that might be applied to each\n generated offspring. Specify a weight for each trait\n compared to other traits in the same set.\n ', tunable=TunableTuple( description= '\n A weighted trait that might be applied to the\n generated offspring. The weight is relative to other\n entries within the same set.\n ', weight=Tunable( description= '\n The relative weight of this trait compared to\n other traits within the same set.\n ', tunable_type=float, default=1), trait=Trait.TunableReference( description= '\n A trait that might be applied to the generated\n offspring.\n ', pack_safe=True))))))) def __init__(self, sim_info): self._sim_info = sim_info self._clear_pregnancy_data() self._completion_callback_listener = None self._completion_alarm_handle = None @property def account(self): return self._sim_info.account @property def is_pregnant(self): if self._seed: return True return False @property def offspring_count(self): return max(len(self._offspring_data), 1) @property def offspring_count_override(self): return self._offspring_count_override @offspring_count_override.setter def offspring_count_override(self, value): self._offspring_count_override = value def _get_parent(self, sim_id): sim_info_manager = services.sim_info_manager() if sim_id in sim_info_manager: return sim_info_manager.get(sim_id) def get_parents(self): if self._parent_ids: parent_a = self._get_parent(self._parent_ids[0]) parent_b = self._get_parent(self._parent_ids[1]) or parent_a return (parent_a, parent_b) return (None, None) def get_partner(self): (owner, partner) = self.get_parents() if partner is not owner: return partner def start_pregnancy(self, parent_a, parent_b, pregnancy_origin=PregnancyOrigin.DEFAULT): if self.is_pregnant: return if not parent_a.incest_prevention_test(parent_b): return self._seed = random.randint(1, MAX_UINT32) self._parent_ids = (parent_a.id, parent_b.id) self._offspring_data = [] self._origin = pregnancy_origin self.enable_pregnancy() def enable_pregnancy(self): if self.is_pregnant: if not self._is_enabled: pregnancy_commodity_type = self.PREGNANCY_COMMODITY_MAP.get( self._sim_info.species) tracker = self._sim_info.get_tracker(pregnancy_commodity_type) pregnancy_commodity = tracker.get_statistic( pregnancy_commodity_type, add=True) pregnancy_commodity.add_statistic_modifier(self.PREGNANCY_RATE) threshold = sims4.math.Threshold(pregnancy_commodity.max_value, operator.ge) self._completion_callback_listener = tracker.create_and_add_listener( pregnancy_commodity.stat_type, threshold, self._on_pregnancy_complete) if threshold.compare(pregnancy_commodity.get_value()): self._on_pregnancy_complete() tracker = self._sim_info.get_tracker(self.GENDER_CHANCE_STAT) tracker.add_statistic(self.GENDER_CHANCE_STAT) self._sim_info.add_trait(self.PREGNANCY_TRAIT) traits_pack_tuple = self.PREGNANCY_ORIGIN_TRAIT_MAPPING.get( self._origin) if traits_pack_tuple is not None: for trait in traits_pack_tuple.traits: self._sim_info.add_trait(trait) self._is_enabled = True def _on_pregnancy_complete(self, *_, **__): if not self.is_pregnant: return if self._sim_info.is_npc: current_zone = services.current_zone() if not current_zone.is_zone_running or self._sim_info.is_instanced( allow_hidden_flags=ALL_HIDDEN_REASONS): if self._completion_alarm_handle is None: self._completion_alarm_handle = alarms.add_alarm( self, clock.interval_in_sim_minutes(1), self._on_pregnancy_complete, repeating=True, cross_zone=True) else: self._create_and_name_offspring() self._show_npc_dialog() self.clear_pregnancy() def complete_pregnancy(self): services.get_event_manager().process_event( TestEvent.OffspringCreated, sim_info=self._sim_info, offspring_created=self.offspring_count) for tuning_data in self.MULTIPLE_OFFSPRING_CHANCES: if tuning_data.size == self.offspring_count: (parent_a, parent_b) = self.get_parents() if parent_a is parent_b: screen_slam = tuning_data.screen_slam_one_parent else: screen_slam = tuning_data.screen_slam_two_parents if screen_slam is not None: screen_slam.send_screen_slam_message( self._sim_info, parent_a, parent_b) break def _clear_pregnancy_data(self): self._seed = 0 self._parent_ids = [] self._offspring_data = [] self._offspring_count_override = None self._origin = PregnancyOrigin.DEFAULT self._is_enabled = False def clear_pregnancy_visuals(self): if self._sim_info.pregnancy_progress: self._sim_info.pregnancy_progress = 0 def clear_pregnancy(self): pregnancy_commodity_type = self.PREGNANCY_COMMODITY_MAP.get( self._sim_info.species) tracker = self._sim_info.get_tracker(pregnancy_commodity_type) if tracker is not None: stat = tracker.get_statistic(pregnancy_commodity_type, add=True) if stat is not None: stat.set_value(stat.min_value) stat.remove_statistic_modifier(self.PREGNANCY_RATE) if self._completion_callback_listener is not None: tracker.remove_listener(self._completion_callback_listener) self._completion_callback_listener = None tracker = self._sim_info.get_tracker(self.GENDER_CHANCE_STAT) if tracker is not None: tracker.remove_statistic(self.GENDER_CHANCE_STAT) if self._sim_info.has_trait(self.PREGNANCY_TRAIT): self._sim_info.remove_trait(self.PREGNANCY_TRAIT) traits_pack_tuple = self.PREGNANCY_ORIGIN_TRAIT_MAPPING.get( self._origin) if traits_pack_tuple is not None: for trait in traits_pack_tuple.traits: if self._sim_info.has_trait(trait): self._sim_info.remove_trait(trait) if self._completion_alarm_handle is not None: alarms.cancel_alarm(self._completion_alarm_handle) self._completion_alarm_handle = None self.clear_pregnancy_visuals() self._clear_pregnancy_data() def _create_and_name_offspring(self, on_create=None): self.create_offspring_data() for offspring_data in self.get_offspring_data_gen(): offspring_data.first_name = self._get_random_first_name( offspring_data) sim_info = self.create_sim_info(offspring_data) if on_create is not None: on_create(sim_info) def validate_partner(self): impregnator = self.get_partner() if impregnator is None: return resolver = DoubleSimResolver(self._sim_info, impregnator) if not self.AT_BIRTH_TESTS.run_tests(resolver): self._parent_ids = (self._sim_info.id, self._sim_info.id) def create_sim_info(self, offspring_data): self.validate_partner() (parent_a, parent_b) = self.get_parents() sim_creator = SimCreator(age=offspring_data.age, gender=offspring_data.gender, species=offspring_data.species, first_name=offspring_data.first_name, last_name=offspring_data.last_name) household = self._sim_info.household zone_id = household.home_zone_id (sim_info_list, _) = SimSpawner.create_sim_infos( (sim_creator, ), household=household, account=self.account, zone_id=zone_id, generate_deterministic_sim=True, creation_source='pregnancy') sim_info = sim_info_list[0] sim_info.world_id = services.get_persistence_service( ).get_world_id_from_zone(zone_id) for trait in tuple(sim_info.trait_tracker.personality_traits): sim_info.remove_trait(trait) for trait in offspring_data.traits: sim_info.add_trait(trait) sim_info.apply_genetics(parent_a, parent_b, seed=offspring_data.genetics) sim_info.resend_extended_species() sim_info.resend_physical_attributes() default_track_overrides = {} mother = parent_a if parent_a.gender == Gender.FEMALE else parent_b father = parent_a if parent_a.gender == Gender.MALE else parent_b if self._origin in self.PREGNANCY_ORIGIN_MODIFIERS: father_override = self.PREGNANCY_ORIGIN_MODIFIERS[ self._origin].default_relationships.father_override if father_override is not None: default_track_overrides[father] = father_override mother_override = self.PREGNANCY_ORIGIN_MODIFIERS[ self._origin].default_relationships.mother_override if mother_override is not None: default_track_overrides[mother] = mother_override self.initialize_sim_info( sim_info, parent_a, parent_b, default_track_overrides=default_track_overrides) self._sim_info.relationship_tracker.add_relationship_bit( sim_info.id, self.BIRTHPARENT_BIT) return sim_info @staticmethod def initialize_sim_info(sim_info, parent_a, parent_b, default_track_overrides=None): sim_info.add_parent_relations(parent_a, parent_b) if sim_info.household is not parent_a.household: parent_a.household.add_sim_info_to_household(sim_info) sim_info.set_default_relationships( reciprocal=True, default_track_overrides=default_track_overrides) services.sim_info_manager().set_default_genealogy( sim_infos=(sim_info, )) parent_generation = max( parent_a.generation, parent_b.generation if parent_b is not None else 0) sim_info.generation = parent_generation + 1 if sim_info.is_played_sim else parent_generation services.get_event_manager().process_event(TestEvent.GenerationCreated, sim_info=sim_info) client = services.client_manager().get_client_by_household_id( sim_info.household_id) if client is not None: client.add_selectable_sim_info(sim_info) parent_b_sim_id = parent_b.sim_id if parent_b is not None else 0 RelgraphService.relgraph_add_child(parent_a.sim_id, parent_b_sim_id, sim_info.sim_id) @classmethod def select_traits_for_offspring(cls, offspring_data, parent_a, parent_b, num_traits, origin=PregnancyOrigin.DEFAULT, random=random): traits = [] personality_trait_slots = num_traits def _add_trait_if_possible(selected_trait): nonlocal personality_trait_slots if selected_trait in traits: return False if any(t.is_conflicting(selected_trait) for t in traits): return False if selected_trait.is_personality_trait: if not personality_trait_slots: return False personality_trait_slots -= 1 traits.append(selected_trait) return True if origin in cls.PREGNANCY_ORIGIN_MODIFIERS: trait_entries = cls.PREGNANCY_ORIGIN_MODIFIERS[ origin].trait_entries for trait_entry in trait_entries: if random.random() >= trait_entry.chance: continue selected_trait = pop_weighted( [(t.weight, t.trait) for t in trait_entry.traits if t.trait.is_valid_trait(offspring_data)], random=random) if selected_trait is not None: _add_trait_if_possible(selected_trait) if parent_a is not None: if parent_b is not None: for inherited_trait_entries in parent_a.trait_tracker.get_inherited_traits( parent_b): selected_trait = pop_weighted( list(inherited_trait_entries), random=random) if selected_trait is not None: _add_trait_if_possible(selected_trait) if not personality_trait_slots: return traits personality_traits = get_possible_traits(offspring_data) random.shuffle(personality_traits) while True: current_trait = personality_traits.pop() if _add_trait_if_possible(current_trait): break if not personality_traits: return traits if not personality_trait_slots: return traits traits_a = set(parent_a.trait_tracker.personality_traits) traits_b = set(parent_b.trait_tracker.personality_traits) shared_parent_traits = list( traits_a.intersection(traits_b) - set(traits)) random.shuffle(shared_parent_traits) while personality_trait_slots: while shared_parent_traits: current_trait = shared_parent_traits.pop() if current_trait in personality_traits: personality_traits.remove(current_trait) did_add_trait = _add_trait_if_possible(current_trait) if did_add_trait: if not personality_trait_slots: return traits remaining_parent_traits = list( traits_a.symmetric_difference(traits_b) - set(traits)) random.shuffle(remaining_parent_traits) while personality_trait_slots: while remaining_parent_traits: current_trait = remaining_parent_traits.pop() if current_trait in personality_traits: personality_traits.remove(current_trait) did_add_trait = _add_trait_if_possible(current_trait) if did_add_trait: if not personality_trait_slots: return traits while personality_trait_slots: while personality_traits: current_trait = personality_traits.pop() _add_trait_if_possible(current_trait) return traits def create_offspring_data(self): r = random.Random() r.seed(self._seed) if self._offspring_count_override is not None: offspring_count = self._offspring_count_override else: offspring_count = pop_weighted([ (p.weight * p.modifiers.get_multiplier(SingleSimResolver(self._sim_info)), p.size) for p in self.MULTIPLE_OFFSPRING_CHANCES ], random=r) offspring_count = min(self._sim_info.household.free_slot_count + 1, offspring_count) species = self._sim_info.species age = self._sim_info.get_birth_age() aging_data = AgingTuning.get_aging_data(species) num_personality_traits = aging_data.get_personality_trait_count(age) self._offspring_data = [] for offspring_index in range(offspring_count): if offspring_index and r.random( ) < self.MONOZYGOTIC_OFFSPRING_CHANCE: gender = self._offspring_data[offspring_index - 1].gender genetics = self._offspring_data[offspring_index - 1].genetics else: gender_chance_stat = self._sim_info.get_statistic( self.GENDER_CHANCE_STAT) if gender_chance_stat is None: gender_chance = 0.5 else: gender_chance = (gender_chance_stat.get_value() - gender_chance_stat.min_value) / ( gender_chance_stat.max_value - gender_chance_stat.min_value) gender = Gender.FEMALE if r.random( ) < gender_chance else Gender.MALE genetics = r.randint(1, MAX_UINT32) last_name = SimSpawner.get_last_name(self._sim_info.last_name, gender, species) offspring_data = PregnancyOffspringData(age, gender, species, genetics, last_name=last_name) (parent_a, parent_b) = self.get_parents() offspring_data.traits = self.select_traits_for_offspring( offspring_data, parent_a, parent_b, num_personality_traits, origin=self._origin) self._offspring_data.append(offspring_data) def get_offspring_data_gen(self): for offspring_data in self._offspring_data: yield offspring_data def _get_random_first_name(self, offspring_data): tries_left = 10 def is_valid(first_name): nonlocal tries_left if not first_name: return False tries_left -= 1 if tries_left and any(sim.first_name == first_name for sim in self._sim_info.household): return False elif any(sim.first_name == first_name for sim in self._offspring_data): return False return True first_name = None while not is_valid(first_name): first_name = SimSpawner.get_random_first_name( offspring_data.gender, offspring_data.species) return first_name def assign_random_first_names_to_offspring_data(self): for offspring_data in self.get_offspring_data_gen(): offspring_data.first_name = self._get_random_first_name( offspring_data) def _show_npc_dialog(self): for tuning_data in self.MULTIPLE_OFFSPRING_CHANCES: if tuning_data.size == self.offspring_count: npc_dialog = tuning_data.npc_dialog if npc_dialog is not None: for parent in self.get_parents(): if parent is None: logger.error( 'Pregnancy for {} has a None parent for IDs {}. Please file a DT with a save attached.', self._sim_info, ','.join( str(parent_id) for parent_id in self._parent_ids)) return parent_instance = parent.get_sim_instance() if parent_instance is not None: if parent_instance.client is not None: additional_tokens = list( itertools.chain(self.get_parents(), self._offspring_data)) dialog = npc_dialog( parent_instance, DoubleSimResolver(additional_tokens[0], additional_tokens[1])) dialog.show_dialog( additional_tokens=additional_tokens) return def save(self): data = SimObjectAttributes_pb2.PersistablePregnancyTracker() data.seed = self._seed data.origin = self._origin data.parent_ids.extend(self._parent_ids) return data def load(self, data): self._seed = int(data.seed) try: self._origin = PregnancyOrigin(data.origin) except KeyError: self._origin = PregnancyOrigin.DEFAULT self._parent_ids.clear() self._parent_ids.extend(data.parent_ids) def refresh_pregnancy_data(self, on_create=None): if not self.is_pregnant: self.clear_pregnancy() return traits_pack_tuple = self.PREGNANCY_ORIGIN_TRAIT_MAPPING.get( self._origin) if traits_pack_tuple is not None and not is_available_pack( traits_pack_tuple.pack): self._create_and_name_offspring(on_create=on_create) self.clear_pregnancy() self.enable_pregnancy() def on_lod_update(self, old_lod, new_lod): if new_lod == SimInfoLODLevel.MINIMUM: self.clear_pregnancy()
class DebugSetupLotInteraction(TerrainImmediateSuperInteraction): INSTANCE_TUNABLES = { 'setup_lot_destroy_old_objects': Tunable(bool, False, description= 'Destroy objects previously created by this interaction.'), 'setup_lot_objects': TunableList( TunableTuple( definition=TunableReference(definition_manager()), position=TunableVector2(Vector2.ZERO()), angle=TunableRange(int, 0, -360, 360), children=TunableList( TunableTuple( definition=TunableReference( definition_manager(), description= 'The child object to create. It will appear in the first available slot in which it fits, subject to additional restrictions specified in the other values of this tuning.' ), part_index=OptionalTunable( Tunable( int, 0, description= 'If specified, restrict slot selection to the given part index.' )), bone_name=OptionalTunable( Tunable( str, '_ctnm_chr_', description= 'If specified, restrict slot selection to one with this exact bone name.' )), slot_type=OptionalTunable( TunableReference( manager=services.get_instance_manager( Types.SLOT_TYPE), description= 'If specified, restrict slot selection to ones that support this type of slot.' )), init_state_values=TunableList( description= '\n List of states the children object will be set to.\n ', tunable=TunableStateValueReference()))), init_state_values=TunableList( description= '\n List of states the created object will be pushed to.\n ', tunable=TunableStateValueReference()))) } _zone_to_cls_to_created_objects = WeakKeyDictionary() @classproperty def destroy_old_objects(cls): return cls.setup_lot_destroy_old_objects @classproperty def created_objects(cls): created_objects = cls._zone_to_cls_to_created_objects.setdefault( services.current_zone(), {}) return setdefault_callable(created_objects, cls, WeakSet) def _run_interaction_gen(self, timeline): with supress_posture_graph_build(): if self.destroy_old_objects: while self.created_objects: obj = self.created_objects.pop() obj.destroy( source=self, cause='Destroying old objects in setup debug lot.') position = self.context.pick.location self.spawn_objects(position) return True yield def _create_object(self, definition_id, position=Vector3.ZERO(), orientation=Quaternion.IDENTITY(), level=0, owner_id=0): obj = objects.system.create_object(definition_id) if obj is not None: transform = Transform(position, orientation) location = Location(transform, self.context.pick.routing_surface) obj.location = location obj.set_household_owner_id(owner_id) self.created_objects.add(obj) return obj def spawn_objects(self, position): root = sims4.math.Vector3(position.x, position.y, position.z) zone = services.current_zone() lot = zone.lot owner_id = lot.owner_household_id if not self.contained_in_lot(lot, root): closest_point = self.find_nearest_point_on_lot(lot, root) if closest_point is None: return False radius = (self.top_right_pos - self.bottom_left_pos).magnitude_2d() / 2 root = closest_point + sims4.math.vector_normalize( sims4.math.vector_flatten(lot.center) - closest_point) * (radius + 1) if not self.contained_in_lot(lot, root): sims4.log.warn( 'Placement', "Placed the lot objects but the entire bounding box isn't inside the lot. This is ok. If you need them to be inside the lot run the interaction again at a diffrent location." ) def _generate_vector(offset_x, offset_z): ground_obj = services.terrain_service.terrain_object() ret_vector = sims4.math.Vector3(root.x + offset_x, root.y, root.z + offset_z) ret_vector.y = ground_obj.get_height_at(ret_vector.x, ret_vector.z) return ret_vector def _generate_quat(rot): return sims4.math.Quaternion.from_axis_angle( rot, sims4.math.Vector3(0, 1, 0)) for info in self.setup_lot_objects: new_pos = _generate_vector(info.position.x, info.position.y) new_rot = _generate_quat(sims4.math.PI / 180 * info.angle) new_obj = self._create_object(info.definition, new_pos, new_rot, owner_id=owner_id) if new_obj is None: sims4.log.error('SetupLot', 'Unable to create object: {}', info) else: for state_value in info.init_state_values: new_obj.set_state(state_value.state, state_value) for child_info in info.children: slot_owner = new_obj if child_info.part_index is not None: for obj_part in new_obj.parts: if obj_part.subroot_index == child_info.part_index: slot_owner = obj_part break bone_name_hash = None if child_info.bone_name is not None: bone_name_hash = hash32(child_info.bone_name) slot_type = None if child_info.slot_type is not None: slot_type = child_info.slot_type for runtime_slot in slot_owner.get_runtime_slots_gen( slot_types={slot_type}, bone_name_hash=bone_name_hash): if runtime_slot.is_valid_for_placement( definition=child_info.definition): break else: sims4.log.error( 'SetupLot', 'Unable to find slot for child object: {}', child_info) child = self._create_object(child_info.definition, owner_id=owner_id) if child is None: sims4.log.error('SetupLot', 'Unable to create child object: {}', child_info) else: runtime_slot.add_child(child) for state_value in child_info.init_state_values: child.set_state(state_value.state, state_value) def contained_in_lot(self, lot, root): self.find_corner_points(root) return True def find_corner_points(self, root): max_x = 0 min_x = 0 max_z = 0 min_z = 0 for info in self.setup_lot_objects: if info.position.x > max_x: max_x = info.position.x if info.position.x < min_x: min_x = info.position.x if info.position.y > max_z: max_z = info.position.y if info.position.y < min_z: min_z = info.position.y self.top_right_pos = sims4.math.Vector3(root.x + max_x, root.y, root.z + max_z) self.bottom_right_pos = sims4.math.Vector3(root.x + max_x, root.y, root.z + min_z) self.top_left_pos = sims4.math.Vector3(root.x + min_x, root.y, root.z + max_z) self.bottom_left_pos = sims4.math.Vector3(root.x + min_x, root.y, root.z + min_z) def find_nearest_point_on_lot(self, lot, root): lot_corners = lot.corners segments = [(lot_corners[0], lot_corners[1]), (lot_corners[1], lot_corners[2]), (lot_corners[2], lot_corners[3]), (lot_corners[3], lot_corners[1])] dist = 0 closest_point = None for segment in segments: new_point = sims4.math.get_closest_point_2D(segment, root) new_distance = (new_point - root).magnitude() if dist == 0: dist = new_distance closest_point = new_point elif new_distance < dist: dist = new_distance closest_point = new_point return closest_point
class GameComponent(Component, component_name=types.GAME_COMPONENT): __qualname__ = 'GameComponent' _PLAYERS = 0 _SCORE = 1 _NEXT_PLAYER = 2 FACTORY_TUNABLES = { 'description': 'Manage information about the games that the attached object can play.', 'games': TunableList(TunableReference(manager=services.get_instance_manager( sims4.resources.Types.GAME_RULESET)), description='The games that the attached object can play.') } def __init__(self, owner, games, **kwargs): super().__init__(owner) self.owner = owner self.games = games self._teams = [] self.active_sims = [] self.active_team = None self.winning_team = None self.current_game = None self.current_target = None self.has_started = False self.requires_setup = False self.target_object = None @property def number_of_players(self): return sum(len(team[self._PLAYERS]) for team in self._teams) @property def number_of_teams(self): return len(self._teams) def get_team_name(self, team_number): return 'Team #' + str(team_number + 1) def is_joinable(self, sim=None): if self.current_game is None or self.number_of_players < self.current_game.players_per_game.upper_bound: if sim is None: return True for team in self._teams: while sim in team[self._PLAYERS]: return False return True return False @property def game_state_dirty(self): if self.target_object is None: return False if self.current_game.initial_state is not None and not self.target_object.state_component.state_value_active( self.current_game.initial_state): return True return False @property def game_has_ended(self): if self.current_game is not None and self.winning_team is None: return False return True @property def progress_stat(self): max_score = max(team[self._SCORE] for team in self._teams) progress = max_score / self.current_game.score_info.winning_score progress *= self.current_game.score_info.progress_stat.max_value_tuning return progress def get_game_target(self, actor_sim=None): if self.number_of_teams <= 1 or self.active_team is None: return if actor_sim is None: actor_team = self.active_team else: for (actor_team, team) in enumerate(self._teams): while actor_sim in team[self._PLAYERS]: break return random_team = random.randrange(self.number_of_teams - 1) if random_team >= actor_team: random_team += 1 random_sim = random.choice(self._teams[random_team][self._PLAYERS]) return random_sim def _build_active_sims(self): del self.active_sims[:] (self.active_sims, next_player) = self._generate_active_sims() self._teams[self.active_team][self._NEXT_PLAYER] = next_player def _generate_active_sims(self): temporary_active_sims = [] team = self._teams[self.active_team][self._PLAYERS] next_player = self._teams[self.active_team][self._NEXT_PLAYER] next_player %= len(team) i = 0 while i < self.current_game.players_per_turn: temporary_active_sims.append(team[next_player]) i += 1 next_player += 1 next_player %= len(team) return (temporary_active_sims, next_player) def _rebalance_teams(self): excess_index = None starvation_index = None min_value = int(self.number_of_players / self.number_of_teams) i = 0 for team in self._teams: team_length = len(team[self._PLAYERS]) if excess_index is None and team_length > min_value: excess_index = i elif team_length < min_value: starvation_index = i if excess_index is not None and starvation_index is not None: self._teams[starvation_index][self._PLAYERS].append( self._teams[excess_index][self._PLAYERS].pop()) break i += 1 if starvation_index is not None and excess_index is None: logger.error( 'Unable to re-balance teams. No excess index index found.', owner='tastle') if gsi_handlers.game_component_handlers.game_log_archiver.enabled: gsi_handlers.game_component_handlers.archive_game_log_entry( self.target_object, 'Rebalanced teams.') def clear_scores(self): for team in self._teams: team[self._SCORE] = 0 self.winning_team = None if gsi_handlers.game_component_handlers.game_log_archiver.enabled: gsi_handlers.game_component_handlers.archive_game_log_entry( self.target_object, 'Cleared all scores.') def add_team(self, sims): if self.current_game is None: logger.error('Cannot add a team when no game is running.', owner='tastle') return if self.number_of_teams >= self.current_game.teams_per_game.upper_bound: logger.error( 'Cannot add a team to a game that already has the maximum number of allowed teams.', owner='tastle') return self._teams.append([sims, 0, 0]) if gsi_handlers.game_component_handlers.game_log_archiver.enabled: team_name = self.get_team_name(len(self._teams) - 1) team_str = 'Added team: ' + team_name gsi_handlers.game_component_handlers.archive_game_log_entry( self.target_object, team_str) def add_player(self, sim): if self.current_game is None: logger.error('Cannot add a player when no game is running.', owner='tastle') return if self.number_of_players >= self.current_game.players_per_game.upper_bound: logger.error( 'Cannot add any players to a game that already has the maximum number of allowed players.', owner='tastle') return if gsi_handlers.game_component_handlers.game_log_archiver.enabled: player_str = 'Added player: ' + str(sim) gsi_handlers.game_component_handlers.archive_game_log_entry( self.target_object, player_str) if self.game_state_dirty and not self.has_started: self.requires_setup = True if self.number_of_teams < self.current_game.teams_per_game.upper_bound: self.add_team([sim]) return previous_number_of_players = len(self._teams[0][self._PLAYERS]) for team in reversed(self._teams): while len(team[self._PLAYERS]) <= previous_number_of_players: team[self._PLAYERS].append(sim) return self._teams[0][self._PLAYERS].append(sim) if self.current_game.clear_score_on_player_join: self.clear_scores() def remove_player(self, sim): for team in self._teams: if sim not in team[self._PLAYERS]: pass if gsi_handlers.game_component_handlers.game_log_archiver.enabled: player_str = 'Removed player: ' + str(sim) gsi_handlers.game_component_handlers.archive_game_log_entry( self.target_object, player_str) team[self._PLAYERS].remove(sim) if self.winning_team is None: self._rebalance_teams() if not team[self._PLAYERS]: self._teams.remove(team) if not self.has_started or self.current_game is not None and self.number_of_players < self.current_game.players_per_game.lower_bound: self.has_started = False self.active_team = None del self.active_sims[:] if self.game_state_dirty: self.requires_setup = True if not self.number_of_teams: self.end_game() elif self.active_team is not None and self.active_team <= self.number_of_teams: self.active_team = random.randrange(self.number_of_teams) self._build_active_sims() break def is_sim_turn(self, sim): if self.active_team is not None and self.can_play( ) and sim in self.active_sims: return True return False def can_play(self): if self.current_game is None: return False team_len = self.number_of_teams player_len = self.number_of_players teams_per_game = self.current_game.teams_per_game if not teams_per_game.lower_bound <= team_len <= teams_per_game.upper_bound: return False players_per_game = self.current_game.players_per_game if not players_per_game.lower_bound <= player_len <= players_per_game.upper_bound: return False return True def take_turn(self, sim=None): if gsi_handlers.game_component_handlers.game_log_archiver.enabled and self.active_team is not None: team_name = self.get_team_name(self.active_team) turn_str = str( sim ) + ' (' + team_name + ') ' + 'just finished taking their turn' gsi_handlers.game_component_handlers.archive_game_log_entry( self.target_object, turn_str) if not self.can_play(): return False if sim and sim in self.active_sims: self.active_sims.remove(sim) if self.active_sims: return False if self.active_team is None: self.clear_scores() self.active_team = random.randrange(self.number_of_teams) self.has_started = True self._build_active_sims() return True def set_current_game(self, game): if self.current_game is not None: self.end_game() self.current_game = game if self.current_game.alternate_target_object is None: self.target_object = self.owner if gsi_handlers.game_component_handlers.game_log_archiver.enabled: game_str = 'Setting current game to ' + str(self.current_game) gsi_handlers.game_component_handlers.archive_game_log_entry( self.target_object, game_str) target_str = 'Target Object is ' + str(self.target_object) gsi_handlers.game_component_handlers.archive_game_log_entry( self.target_object, target_str) def increase_score(self, sim): if self.target_object is None: return for (team_number, team) in enumerate(self._teams): if sim not in team[self._PLAYERS]: pass if self.active_team is not None and team_number != self.active_team: return score_info = self.current_game.score_info score_increase = sims4.random.uniform( score_info.score_increase.lower_bound, score_info.score_increase.upper_bound) relevant_skill = score_info.relevant_skill if relevant_skill is not None: if score_info.use_effective_skill_level: skill_level = sim.get_effective_skill_level(relevant_skill) else: skill = sim.get_stat_instance(relevant_skill) skill_level = skill if skill is not None else 0 score_increase += score_info.skill_level_bonus * skill_level team[self._SCORE] += score_increase if gsi_handlers.game_component_handlers.game_log_archiver.enabled: team_name = self.get_team_name(team_number) increase_str = str(sim) + ' scored ' + str( score_increase) + ' points for ' + team_name gsi_handlers.game_component_handlers.archive_game_log_entry( self.target_object, increase_str) score_str = 'Score for ' + team_name + ' is now ' + str( team[self._SCORE]) + ' / ' + str(score_info.winning_score) gsi_handlers.game_component_handlers.archive_game_log_entry( self.target_object, score_str) if team[self._SCORE] >= score_info.winning_score: self.winning_team = team[self._PLAYERS] if gsi_handlers.game_component_handlers.game_log_archiver.enabled: team_name = self.get_team_name(team_number) win_str = team_name + ' has won the game' gsi_handlers.game_component_handlers.archive_game_log_entry( self.target_object, win_str) if score_info.progress_stat is not None: self.target_object.statistic_tracker.set_value( score_info.progress_stat, self.progress_stat) logger.error( 'The given Sim {} is not a member of any team, so we cannot increase its score.', sim, owner='tastle') def end_game(self): if gsi_handlers.game_component_handlers.game_log_archiver.enabled: game_over_str = 'Game ' + str(self.current_game) + ' has ended' gsi_handlers.game_component_handlers.archive_game_log_entry( self.target_object, game_over_str) if self.target_object is not None and self.target_object is not self.owner: self.target_object.fade_and_destroy() self.target_object = None self.current_game = None self.active_team = None self.winning_team = None self.has_started = False del self._teams[:] del self.active_sims[:] def setup_game(self): self.requires_setup = False if self.target_object is not None: return if gsi_handlers.game_component_handlers.game_log_archiver.enabled: setup_str = 'Game ' + str(self.current_game) + ' has been set up' gsi_handlers.game_component_handlers.archive_game_log_entry( self.target_object, setup_str) self.clear_scores() slot_hash = None alternate_target_object = self.current_game.alternate_target_object parent_slot = alternate_target_object.parent_slot if isinstance(parent_slot, str): slot_hash = sims4.hash_util.hash32(parent_slot) for child in get_child_objects(self.owner): while child.definition is alternate_target_object.target_game_object: slot = child.parent_slot if slot_hash is not None: if slot_hash == slot.slot_name_hash: self.target_object = child return if parent_slot in slot.slot_types: self.target_object = child return elif parent_slot in slot.slot_types: self.target_object = child return created_object = create_object( alternate_target_object.target_game_object) self.target_object = created_object self.owner.slot_object(parent_slot=parent_slot, slotting_object=created_object)
class DebugCreateSimWithGenderAndAgeInteraction( TerrainImmediateSuperInteraction): INSTANCE_TUNABLES = { 'gender': TunableEnumEntry( description= '\n The gender of the Sim to be created.\n ', tunable_type=Gender, default=Gender.MALE), 'age': TunableEnumEntry( description= '\n The age of the Sim to be created.\n ', tunable_type=Age, default=Age.ADULT), 'species': TunableEnumEntry( description= '\n The species of the Sim to be created.\n ', tunable_type=Species, default=Species.HUMAN, invalid_enums=(Species.INVALID, )), 'breed_picker': OptionalTunable( description= '\n Breed picker to use if using a non-human species.\n \n If disabled, breed will be random.\n ', tunable=TunableReference( manager=services.get_instance_manager( sims4.resources.Types.INTERACTION), class_restrictions=('BreedPickerSuperInteraction', ), allow_none=True)) } def _run_interaction_gen(self, timeline): if self.species == Species.HUMAN or self.breed_picker is None: position = self.context.pick.location routing_surface = self.context.pick.routing_surface actor_sim_info = self.sim.sim_info household = actor_sim_info.household if self.age == Age.BABY else None sim_creator = sims.sim_spawner.SimCreator(age=self.age, gender=self.gender, species=self.species) (sim_info_list, _) = sims.sim_spawner.SimSpawner.create_sim_infos( (sim_creator, ), household=household, account=actor_sim_info.account, zone_id=actor_sim_info.zone_id, creation_source='cheat: DebugCreateSimInteraction', is_debug=True) sim_info = sim_info_list[0] if sim_info.age == Age.BABY: PregnancyTracker.initialize_sim_info(sim_info, actor_sim_info, None) create_and_place_baby(sim_info, position=position, routing_surface=routing_surface) else: sims.sim_spawner.SimSpawner.spawn_sim(sim_info, sim_position=position, is_debug=True) else: self.sim.push_super_affordance(self.breed_picker, self.target, self.context, picked_object=self.target, age=self.age, gender=self.gender, species=self.species) return True yield
class WalksStyleBehavior(HasTunableSingletonFactory, AutoFactoryInit): SWIMMING_WALKSTYLES = TunableList( description= '\n The exhaustive list of walkstyles allowed while Sims are swimming. If a\n Sim has a request for a walkstyle that is not supported, the first\n element is used as a replacement.\n ', tunable=TunableWalkstyle(pack_safe=True)) WALKSTYLE_COST = TunableMapping( description= '\n Associate a specific walkstyle to a statistic cost before the walkstyle\n can be activated.\n ', key_type=TunableWalkstyle( description= '\n The walkstyle that should have a specified cost when triggered.\n ', pack_safe=True), value_type=TunableTuple( description= '\n Cost data of the specified walkstyle.\n ', walkstyle_cost_statistic=TunableReference( description= '\n The statistic we are operating on when the walkstyle is\n triggered.\n ', manager=services.get_instance_manager( sims4.resources.Types.STATISTIC), pack_safe=True), cost=TunableRange( description= '\n When the walkstyle is triggered during a route, this is the\n cost that will be substracted from the specified statistic. \n ', tunable_type=int, default=1, minimum=0))) WALKSTYLES_OVERRIDE_TELEPORT = TunableList( description= '\n Any walkstyles found here will be able to override the teleport styles\n if they are specified.\n ', tunable=TunableWalkstyle(pack_safe=True)) FACTORY_TUNABLES = { 'carry_walkstyle_behavior': OptionalTunable( description= '\n Define the walkstyle the Sim plays whenever they are being carried\n by another Sim.\n \n If this is set to "no behavior", the Sim will not react to the\n parent\'s walkstyle at all. They will play no walkstyle, and rely on\n posture idles to animate.\n \n If this is set, Sims have the ability to modify their walkstyle\n whenever their parent is routing.\n ', tunable=TunableTuple( description= '\n Specify how this Sim behaves when being carried by another Sim.\n ', default_carry_walkstyle=TunableWalkstyle( description= '\n Unless an override is specified, this is the walkstyle\n applied to the Sim whenever they are being carried.\n ' ), carry_walkstyle_overrides=TunableMapping( description= '\n Define carry walkstyle overrides. For instance, we might\n want to specify a different carry walkstyle if the parent is\n frantically running, for example.\n ', key_type=TunableWalkstyle( description= '\n The walkstyle that this carry walkstyle override applies to.\n ', pack_safe=True), value_type=TunableWalkstyle( description= '\n The carry walkstyle override.\n ', pack_safe=True))), enabled_name='Apply_Carry_Walkstyle', disabled_name='No_Behavior'), 'combo_walkstyle_replacements': TunableList( description= '\n The prioritized list of the combo walkstyle replacement rules. We\n use this list to decide if a Sim should use a combo walk style based\n on the the highest priority walkstyle request, and other walkstyles\n that might affect the replacement based on the key combo rules.\n ', tunable=TunableTuple( description= '\n The n->1 mapping of walkstyle replacement. \n ', key_combo_list=TunableList( description= '\n The list of the walkstyles used as key combos. If the\n current highest priority walkstyle exists in this list, and\n the Sim has every other walkstyle in the key list, then we\n replace this with the result walkstyle tuned in the tuple.\n ', tunable=TunableWalkstyle(pack_safe=True)), result=TunableWalkstyle( description= '\n The mapped combo walkstyle.\n ', pack_safe=True))), 'default_walkstyle': TunableWalkstyle( description= '\n The underlying walkstyle for this Sim. This is most likely going to\n be overridden by the CAS walkstyle, emotional walkstyles, buff\n walkstyles, etc...\n ' ), 'run_allowed_flags': TunableEnumFlags( description= "\n Define where the Sim is allowed to run. Certain buffs might suppress\n a Sim's ability to run.\n ", enum_type=WalkStyleRunAllowedFlags, default=WalkStyleRunAllowedFlags.RUN_ALLOWED_OUTDOORS, allow_no_flags=True), 'run_disallowed_walkstyles': TunableList( description= "\n A set of walkstyles that would never allow a Sim to run, i.e., if\n the Sim's requested walkstyle is in this set, they will not run,\n even to cover great distances.\n ", tunable=TunableWalkstyle(pack_safe=True)), 'run_required_total_distance': TunableRange( description= '\n For an entire route, the minimum distance required for Sim to run.\n ', tunable_type=float, minimum=0, default=20), 'run_required_segment_distance': TunableRange( description= '\n For a specific route segment, the minimum distance required for the\n Sim to run.\n ', tunable_type=float, minimum=0, default=10), 'run_walkstyle': TunableWalkstyle( description= '\n The walkstyle to use when this Sim is supposed to be running.\n ' ), 'wading_walkstyle': OptionalTunable( description= '\n If enabled, the routing agent will play a different walkstyle when\n walking through water.\n ', tunable=TunableWalkstyle( description= '\n The walkstyle to use when wading through water.\n ' )), 'wading_walkstyle_buff': OptionalTunable( description= '\n A buff which, if tuned, will be on the sim if the sim is currently\n in wading level water.\n ', tunable=TunableReference(manager=services.get_instance_manager( sims4.resources.Types.BUFF))), 'short_walkstyle': TunableWalkstyle( description= '\n The walkstyle to use when Sims are routing over a distance shorter\n than the one defined in "Short Walkstyle Distance" or any of the\n overrides.\n \n This value is used if no override is tuned in "Short Walkstyle Map".\n ' ), 'short_walkstyle_distance': TunableRange( description= "\n Any route whose distance is less than this value will request the\n short version of the Sim's current walkstyle.\n ", tunable_type=float, minimum=0, default=7), 'short_walkstyle_distance_override_map': TunableMapping( description= "\n If a Sim's current walkstyle is any of the ones specified in here,\n use the associated value to determine if the short version of the\n walkstyle is to be requested.\n ", key_type=TunableWalkstyle( description= '\n The walkstyle that this distance override applies to.\n ', pack_safe=True), value_type=TunableRange( description= "\n Any route whose distance is less than this value will request\n the short version of the Sim's current walkstyle, provided the\n Sim's current walkstyle is the associated walkstyle.\n ", tunable_type=float, minimum=0, default=7)), 'short_walkstyle_map': TunableMapping( description= '\n Associate a specific short version of a walkstyle to walkstyles.\n ', key_type=TunableWalkstyle( description= '\n The walkstyle that this short walkstyle mapping applies to.\n ', pack_safe=True), value_type=TunableWalkstyle( description= '\n The short version of the associated walkstyle.\n ', pack_safe=True)) } def _get_walkstyle_overrides(self, actor): if actor.is_sim: return tuple(buff.walkstyle_behavior_override for buff in actor.get_active_buff_types() if buff.walkstyle_behavior_override is not None) return () def _apply_run_walkstyle_to_path(self, actor, path, walkstyle_overrides, time_offset=None): run_allowed_flags = self.run_allowed_flags for walkstyle_override in walkstyle_overrides: run_allowed_flags |= walkstyle_override.additional_run_flags for walkstyle_override in walkstyle_overrides: run_allowed_flags &= ~walkstyle_override.removed_run_flags if not run_allowed_flags: return run_required_total_distance = sims4.math.safe_max( (override for override in walkstyle_overrides if override.run_required_total_distance is not None), key=operator.attrgetter('walkstyle_behavior_priority'), default=self).run_required_total_distance if path.length() < run_required_total_distance: return run_required_segment_distance = sims4.math.safe_max( (override for override in walkstyle_overrides if override.run_required_segment_distance is not None), key=operator.attrgetter('walkstyle_behavior_priority'), default=self).run_required_segment_distance path_nodes = list(path.nodes) all_path_node_data = [] for (start_node, end_node) in zip(path_nodes, path_nodes[1:]): switch_routing_surface = start_node.routing_surface_id != end_node.routing_surface_id is_outside = start_node.portal_id == 0 and get_block_id_for_node( start_node) == 0 route_key = (switch_routing_surface, is_outside) all_path_node_data.append((route_key, start_node, end_node)) run_walkstyle = self.get_run_walkstyle(actor) for ((_, is_outside), path_node_data) in itertools.groupby(all_path_node_data, key=operator.itemgetter(0)): if is_outside and not run_allowed_flags & WalkStyleRunAllowedFlags.RUN_ALLOWED_OUTDOORS: continue if not is_outside and not run_allowed_flags & WalkStyleRunAllowedFlags.RUN_ALLOWED_INDOORS: continue path_node_data = list(path_node_data) segment_length = sum( (sims4.math.Vector3(*start_node.position) - sims4.math.Vector3(*end_node.position)).magnitude_2d() for (_, start_node, end_node) in path_node_data) if segment_length < run_required_segment_distance: continue for (_, path_node, _) in path_node_data: if not time_offset is None: if path_node.time >= time_offset: path_node.walkstyle = run_walkstyle path_node.walkstyle = run_walkstyle def check_for_wading(self, sim, *_, **__): routing_component = sim.routing_component if not routing_component.last_route_has_wading_nodes and not routing_component.wading_buff_handle: return wading_interval = OceanTuning.get_actor_wading_interval(sim) if wading_interval is None: return water_height = get_water_depth_at_location(sim.location) if water_height in wading_interval: if routing_component.wading_buff_handle is None: routing_component.wading_buff_handle = sim.add_buff( self.wading_walkstyle_buff) elif routing_component.wading_buff_handle is not None: sim.remove_buff(routing_component.wading_buff_handle) routing_component.wading_buff_handle = None def _apply_wading_walkstyle_to_path(self, actor, path, default_walkstyle, time_offset=None): if actor.is_sim and actor.sim_info.is_ghost: return False wading_interval = OceanTuning.get_actor_wading_interval(actor) if wading_interval is None: return False wading_walkstyle = self.get_wading_walkstyle(actor) if wading_walkstyle is None: return False def get_node_water_height(path_node): return get_water_depth(path_node.position[0], path_node.position[2], path_node.routing_surface_id.secondary_id) path_nodes = list(path.nodes) start_wading = get_node_water_height(path_nodes[0]) in wading_interval end_wading = get_node_water_height(path_nodes[-1]) in wading_interval if not start_wading and not end_wading: return False path_contains_wading = False for (start_node, end_node) in zip(path_nodes, path_nodes[1:]): if time_offset is not None and end_node.time < time_offset: continue if start_node.routing_surface_id.type == SurfaceType.SURFACETYPE_POOL: continue if start_node.portal_object_id != 0: continue start_wading = get_node_water_height(start_node) in wading_interval end_wading = get_node_water_height(end_node) in wading_interval if not start_wading and not end_wading: continue is_wading = start_wading if is_wading: start_node.walkstyle = wading_walkstyle path_contains_wading = True nodes_to_add = [] for (transform, routing_surface, time) in path.get_location_data_along_segment_gen( start_node.index, end_node.index, time_step=0.3): should_wade = get_water_depth( transform.translation[0], transform.translation[2]) in wading_interval if is_wading and not should_wade: is_wading = False nodes_to_add.append( (Location(transform.translation, transform.orientation, routing_surface), time, 0, default_walkstyle, 0, 0, end_node.index)) elif not is_wading: if should_wade: is_wading = True nodes_to_add.append( (Location(transform.translation, transform.orientation, routing_surface), time, 0, wading_walkstyle, 0, 0, end_node.index)) path_contains_wading = True running_index_offset = 0 for (loc, time, node_type, walkstyle, portal_obj_id, portal_id, index) in nodes_to_add: node_index = index + running_index_offset path.nodes.add_node(loc, time, node_type, walkstyle, portal_obj_id, portal_id, node_index) node = path.nodes[node_index] node.is_procedural = False running_index_offset += 1 return path_contains_wading def apply_walkstyle_to_path(self, actor, path, time_offset=None): gsi_archiver = None can_archive = gsi_handlers.walkstyle_handlers.archiver.enabled and actor.is_sim if can_archive: gsi_archiver = WalkstyleGSIArchiver(actor) walkstyle = self.get_walkstyle_for_path(actor, path, gsi_archiver) if can_archive: gsi_archiver.gsi_archive_entry() path_nodes = list(path.nodes) for path_node in path_nodes: if not time_offset is None: if path_node.time >= time_offset: path_node.walkstyle = walkstyle path_node.walkstyle = walkstyle walkstyle_overrides = self._get_walkstyle_overrides(actor) if walkstyle not in self.run_disallowed_walkstyles: self._apply_run_walkstyle_to_path(actor, path, walkstyle_overrides, time_offset=time_offset) if actor.is_sim: actor.routing_component.last_route_has_wading_nodes = self._apply_wading_walkstyle_to_path( actor, path, walkstyle, time_offset=time_offset) return walkstyle def get_combo_replacement(self, highest_priority_walkstyle, walkstyle_list): for combo_tuple in self.combo_walkstyle_replacements: key_combo_list = combo_tuple.key_combo_list if highest_priority_walkstyle in key_combo_list: if all(ws in walkstyle_list for ws in key_combo_list): return combo_tuple def get_combo_replaced_walkstyle(self, highest_priority_walkstyle, walkstyle_list): combo_tuple = self.get_combo_replacement(highest_priority_walkstyle, walkstyle_list) if combo_tuple is not None: return combo_tuple.result def get_default_walkstyle(self, actor, gsi_archiver=None): walkstyle = actor.get_cost_valid_walkstyle( WalksStyleBehavior.WALKSTYLE_COST) walkstyle_list = actor.get_walkstyle_list() replaced_walkstyle = self.get_combo_replaced_walkstyle( walkstyle, walkstyle_list) if gsi_archiver is not None: gsi_archiver.default_walkstyle = walkstyle gsi_archiver.combo_replacement_walkstyle_found = replaced_walkstyle if replaced_walkstyle is not None: walkstyle = replaced_walkstyle return walkstyle def get_short_walkstyle(self, walkstyle, actor): short_walkstyle = self._get_property_override(actor, 'short_walkstyle') return self.short_walkstyle_map.get(walkstyle, short_walkstyle) def get_run_walkstyle(self, actor): run_walkstyle = self._get_property_override(actor, 'run_walkstyle') return run_walkstyle def get_wading_walkstyle(self, actor): wading_walkstyle = self._get_property_override(actor, 'wading_walkstyle') return wading_walkstyle def supports_wading_walkstyle_buff(self, actor): return self.wading_walkstyle_buff is not None and OceanTuning.get_actor_wading_interval( actor) def _get_property_override(self, actor, property_name): overrides = self._get_walkstyle_overrides(actor) override = sims4.math.safe_max( (override for override in overrides if getattr(override, property_name) is not None), key=operator.attrgetter('walkstyle_behavior_priority'), default=self) property_value = getattr(override, property_name) return property_value def _apply_walkstyle_cost(self, actor, walkstyle): walkstyle_cost = WalksStyleBehavior.WALKSTYLE_COST.get(walkstyle, None) if walkstyle_cost is not None: stat_instance = actor.get_stat_instance( walkstyle_cost.walkstyle_cost_statistic) if stat_instance is None: logger.error( 'Statistic {}, not found on Sim {} for walkstyle cost', walkstyle_cost.walkstyle_cost_statistic, actor, owner='camilogarcia') return stat_instance.add_value(-walkstyle_cost.cost) def get_walkstyle_for_path(self, actor, path, gsi_archiver=None): walkstyle = self.get_default_walkstyle(actor, gsi_archiver) if gsi_archiver is not None: gsi_archiver.walkstyle_requests = actor.routing_component.get_walkstyle_requests( ) short_walk_distance = self.short_walkstyle_distance_override_map.get( walkstyle, self.short_walkstyle_distance) if path.length() < short_walk_distance: walkstyle = self.get_short_walkstyle(walkstyle, actor) if gsi_archiver is not None: gsi_archiver.default_walkstyle_replaced_by_short_walkstyle = walkstyle if actor.is_sim: if actor.in_pool and walkstyle not in self.SWIMMING_WALKSTYLES: walkstyle = self.SWIMMING_WALKSTYLES[0] if gsi_archiver is not None: gsi_archiver.default_walkstyle_replaced_by_swimming_walkstyle = walkstyle return walkstyle else: posture = actor.posture if posture.mobile and posture.compatible_walkstyles and walkstyle not in posture.compatible_walkstyles: walkstyle = posture.compatible_walkstyles[0] if gsi_archiver is not None: gsi_archiver.default_walkstyle_replaced_by_posture_walkstyle = walkstyle return walkstyle return walkstyle
class AffordanceReferenceScoringModifier(BaseGameEffectModifier): FACTORY_TUNABLES = { 'content_score_bonus': Tunable( description= '\n When determine content score for affordances and afforance matches\n tuned here, content score is increased by this amount.\n ', tunable_type=int, default=0), 'success_modifier': TunablePercent( description= '\n Amount to adjust percent success chance. For example, tuning 10%\n will increase success chance by 10% over the base success chance.\n Additive with other buffs.\n ', default=0, minimum=-100), 'affordances': TunableList( description= '\n A list of affordances that will be compared against.\n ', tunable=TunableReference(manager=services.affordance_manager())), 'affordance_lists': TunableList( description= '\n A list of affordance snippets that will be compared against.\n ', tunable=snippets.TunableAffordanceListReference()), 'interaction_category_tags': TunableSet( description= '\n This attribute is used to test for affordances that contain any of the tags in this set.\n ', tunable=TunableEnumEntry( description= '\n These tag values are used for testing interactions.\n ', tunable_type=Tag, default=Tag.INVALID)), 'interaction_category_blacklist_tags': TunableSet( description= '\n Any interaction with a tag in this set will NOT be modiified.\n Affects display name on a per interaction basis.\n ', tunable=TunableEnumEntry( description= '\n These tag values are used for testing interactions.\n ', tunable_type=Tag, default=Tag.INVALID)), 'pie_menu_parent_name': OptionalTunable( description= '\n If enabled, we will insert the name into this parent string\n in the pie menu. Only affected by test and blacklist tags\n (for performance reasons)\n ', tunable=TunableLocalizedStringFactory( description= '\n A string to wrap the normal interaction name. Token 0 is actor,\n Token 1 is the normal name.\n ' )), 'new_pie_menu_icon': TunableIconAllPacks( description= "\n Icon to put on interactions that pass test (interaction resolver)\n and don't match blacklist tags.\n ", allow_none=True), 'basic_extras': TunableBasicExtras( description= '\n Basic extras to add to interactions that match. \n ' ), 'test': event_testing.tests.TunableTestSet( description= '\n The test to run to see if the display_name should be\n overridden. Ors of Ands.\n ' ) } def __init__(self, content_score_bonus=0, success_modifier=0, affordances=(), affordance_lists=(), interaction_category_tags=set(), interaction_category_blacklist_tags=set(), pie_menu_parent_name=None, new_pie_menu_icon=None, basic_extras=(), test=None): super().__init__(GameEffectType.AFFORDANCE_MODIFIER) self._score_bonus = content_score_bonus self._success_modifier = success_modifier self._affordances = affordances self._affordance_lists = affordance_lists self._interaction_category_tags = interaction_category_tags self._interaction_category_blacklist_tags = interaction_category_blacklist_tags self._pie_menu_parent_name = pie_menu_parent_name self._new_pie_menu_icon = new_pie_menu_icon self._basic_extras = basic_extras self._test = test def is_type(self, affordance, resolver): if affordance is not None: if affordance.interaction_category_tags & self._interaction_category_blacklist_tags: return False if affordance in self._affordances: return True for affordances in self._affordance_lists: if affordance in affordances: return True if affordance.interaction_category_tags & self._interaction_category_tags: return True elif self._test: result = False try: result = self._test.run_tests(resolver) except: pass if result: return True if self._test: result = False try: result = self._test.run_tests(resolver) except: pass if result: return True return False def get_score_for_type(self, affordance, resolver): if self.is_type(affordance, resolver): return self._score_bonus return 0 def get_success_for_type(self, affordance, resolver): if self.is_type(affordance, resolver): return self._success_modifier return 0 def get_new_pie_menu_icon_and_parent_name_for_type(self, affordance, resolver): if self.is_type(affordance, resolver): return (self._new_pie_menu_icon, self._pie_menu_parent_name, self._interaction_category_blacklist_tags) return (None, None, None) def get_basic_extras_for_type(self, affordance, resolver): if self.is_type(affordance, resolver): return self._basic_extras return [] def debug_affordances_gen(self): for affordance in self._affordances: yield affordance.__name__ for affordnace_snippet in self._affordance_lists: yield affordnace_snippet.__name__
class RemoveTraitLootOp(BaseLootOperation): __qualname__ = 'RemoveTraitLootOp' FACTORY_TUNABLES = {'description': '\n This loot will remove the specified trait\n ', 'trait': TunableReference(description='\n The trait to be removed.\n ', manager=services.get_instance_manager(sims4.resources.Types.TRAIT))} def __init__(self, trait, **kwargs): super().__init__(**kwargs) self._trait = trait def _apply_to_subject_and_target(self, subject, target, resolver): subject.trait_tracker.remove_trait(self._trait)
class WhimsTracker(SimInfoTracker): MAX_GOALS = 2 EMOTIONAL_WHIM_PRIORITY = 1 class WhimAwardTypes(enum.Int): MONEY = 0 BUFF = 1 OBJECT = 2 TRAIT = 3 CASPART = 4 SATISFACTION_STORE_ITEMS = TunableMapping( description= '\n A list of Sim based Tunable Rewards offered from the Satisfaction Store.\n ', key_type=TunableReference( description='\n The reward to offer.\n ', manager=services.get_instance_manager( sims4.resources.Types.REWARD), pack_safe=True), value_type=TunableTuple( description= '\n A collection of data about this reward.\n ', cost=Tunable(tunable_type=int, default=100), award_type=TunableEnumEntry(WhimAwardTypes, WhimAwardTypes.MONEY))) WHIM_THRASHING_CHANCE = TunablePercent( description= '\n The tunable percent chance that the activation of a whimset will try\n and cancel a whim of a lower whimset priority as long as that whim is\n not locked, and not on the anti thrashing cooldown.\n ', default=50) WHIM_ANTI_THRASHING_TIME = TunableSimMinute( description= '\n The amount of time in sim minutes that a whim will not be overwritten\n by another whimset becoming active. This is essentially a period of\n time after a whim becomes active that it is considered locked.\n ', default=5) @classproperty def max_whims(cls): return WhimsTracker.MAX_GOALS + 1 @classproperty def emotional_whim_index(cls): return WhimsTracker.MAX_GOALS def __init__(self, sim_info): self._sim_info = sim_info self._goal_id_generator = uid.UniqueIdGenerator(1) self._active_whimsets_data = {} self._active_whims = [_ActiveWhimData() for _ in range(self.max_whims)] self._hidden = False self._cooldown_alarms = {} self._whim_goal_proto = None self._completed_goals = {} self._test_results_map = {} self._goals_dirty = True self._score_multipliers = [] def start_whims_tracker(self): self._offer_whims() def activate_whimset_from_objective_completion(self, whimset): self._activate_whimset(whimset) self._try_and_thrash_whims(whimset.activated_priority) def validate_goals(self): sim = self._sim_info.get_sim_instance() if sim is None: return for whim_data in self._active_whims: whim = whim_data.whim if whim is None: continue required_sim_info = whim.get_required_target_sim_info() if not whim.can_be_given_as_goal( sim, None, inherited_target_sim_info=required_sim_info): self._remove_whim(whim, TelemetryWhimEvents.NO_LONGER_AVAILABLE) self._offer_whims() def whims_and_parents_gen(self): for whim_data in self._active_whims: if whim_data.whim is None: continue yield (whim_data.whim, whim_data.whimset) def get_active_whimsets(self): whim_sets = set(self._active_whimsets_data.keys()) if self._sim_info.primary_aspiration is not None and self._sim_info.primary_aspiration.whim_set is not None: whim_sets.add(self._sim_info.primary_aspiration.whim_set) current_venue = services.get_current_venue() if current_venue.whim_set is not None: whim_sets.add(current_venue.whim_set) for trait in self._sim_info.trait_tracker: if trait.whim_set is not None: whim_sets.add(trait.whim_set) season_service = services.season_service() if season_service is not None: season_content = season_service.season_content if season_content.whim_set is not None: whim_sets.add(season_content.whim_set) object_manager = services.object_manager() whim_sets.update(object_manager.active_whim_sets) zone_director = services.venue_service().get_zone_director() open_street_director = zone_director.open_street_director if open_street_director is not None and open_street_director.whim_set: whim_sets.add(open_street_director.whim_set) return whim_sets def get_active_whim_data(self): return tuple(self._active_whims) def get_whimset_target(self, whimset): whimset_data = self._active_whimsets_data.get(whimset) if whimset_data is None: return return whimset_data.target def get_emotional_whimset(self): return self._sim_mood().whim_set def refresh_emotion_whim(self): emotional_whim = self._active_whims[self.emotional_whim_index].whim if emotional_whim is not None: self._remove_whim(emotional_whim, TelemetryWhimEvents.NO_LONGER_AVAILABLE) self._offer_whims() def get_priority(self, whimset): return whimset.get_priority(self._sim_info) def clean_up(self): for whim_data in self._active_whims: whim = whim_data.whim if whim is not None: whim.destroy() if whim_data.anti_thrashing_alarm_handle is not None: alarms.cancel_alarm(whim_data.anti_thrashing_alarm_handle) self._active_whims = [_ActiveWhimData() for _ in range(self.max_whims)] for alarm_handle in self._cooldown_alarms.values(): alarms.cancel_alarm(alarm_handle) self._cooldown_alarms.clear() self._test_results_map.clear() self._active_whimsets_data.clear() def refresh_whim(self, whim_type): whim = self._get_whim_by_whim_type(whim_type) if whim is None: logger.error( 'Trying to refresh whim type {} when there are no active whims of that type.', whim_type) return self._remove_whim(whim, TelemetryWhimEvents.CANCELED) self._offer_whims(prohibited_whims={whim_type}) def toggle_whim_lock(self, whim_type): whim = self._get_whim_by_whim_type(whim_type) if whim is None: logger.error( 'Trying to toggle the locked status of whim type {} when there are no active whims of that type.', whim_type) return whim.toggle_locked_status() self._goals_dirty = True self._send_goals_update() def hide_whims(self): if self._hidden: logger.error('Trying to hide whims when they are already hidden.') return self._hidden = True self._goals_dirty = True self._send_goals_update() def show_whims(self, reset=False): if not self._hidden: logger.error("Trying to show whims when they aren't hidden.") return self._hidden = False self._goals_dirty = True if reset: self.refresh_whims() self._send_goals_update() def refresh_whims(self): prohibited_whims = set() for whim_data in self._active_whims: whim = whim_data.whim if whim is not None: if whim.locked: continue prohibited_whims.add(type(whim)) self._remove_whim(whim, TelemetryWhimEvents.CANCELED) self._offer_whims(prohibited_whims=prohibited_whims) def add_score_multiplier(self, multiplier): self._score_multipliers.append(multiplier) self._goals_dirty = True self._send_goals_update() def get_score_multiplier(self): return reduce(operator.mul, self._score_multipliers, 1) def get_score_for_whim(self, score): return int(score * self.get_score_multiplier()) def remove_score_multiplier(self, multiplier): if multiplier in self._score_multipliers: self._score_multipliers.remove(multiplier) self._goals_dirty = True self._send_goals_update() def on_zone_unload(self): if not game_services.service_manager.is_traveling: return self._whim_goal_proto = GameplaySaveData_pb2.WhimsetTrackerData() self.save_whims_info_to_proto(self._whim_goal_proto, copy_existing=False) self.clean_up() def purchase_whim_award(self, reward_guid64): reward_instance = services.get_instance_manager( sims4.resources.Types.REWARD).get(reward_guid64) award = reward_instance cost = self.SATISFACTION_STORE_ITEMS[reward_instance].cost if self._sim_info.get_whim_bucks() < cost: logger.debug( 'Attempting to purchase a whim award with insufficient funds: Cost: {}, Funds: {}', cost, self._sim_info.get_whim_bucks()) return self._sim_info.add_whim_bucks(-cost, SetWhimBucks.PURCHASED_REWARD, source=reward_guid64) award.give_reward(self._sim_info) def send_satisfaction_reward_list(self): msg = Sims_pb2.SatisfactionRewards() for (reward, data) in self.SATISFACTION_STORE_ITEMS.items(): reward_msg = Sims_pb2.SatisfactionReward() reward_msg.reward_id = reward.guid64 reward_msg.cost = data.cost reward_msg.affordable = True if data.cost <= self._sim_info.get_whim_bucks( ) else False reward_msg.available = reward.is_valid(self._sim_info) reward_msg.type = data.award_type unavailable_tooltip = reward.get_unavailable_tooltip( self._sim_info) if unavailable_tooltip is not None: reward_msg.unavailable_tooltip = unavailable_tooltip msg.rewards.append(reward_msg) msg.sim_id = self._sim_info.id distributor = Distributor.instance() distributor.add_op_with_no_owner( GenericProtocolBufferOp(Operation.SIM_SATISFACTION_REWARDS, msg)) def cache_whim_goal_proto(self, whim_tracker_proto, skip_load=False): if skip_load: return if self._sim_info.is_npc: return if self._sim_info.whim_tracker is None: return self._whim_goal_proto = GameplaySaveData_pb2.WhimsetTrackerData() self._whim_goal_proto.CopyFrom(whim_tracker_proto) def load_whims_info_from_proto(self): if self._sim_info.is_npc: return if self._whim_goal_proto is None: return for whim_data in self._active_whims: whim = whim_data.whim if whim is not None: self._remove_whim(whim, None) if len(self._whim_goal_proto.active_whims) > self.max_whims: logger.error( 'More whims saved than the max number of goals allowed') aspiration_manager = services.get_instance_manager( sims4.resources.Types.ASPIRATION) sim_info_manager = services.sim_info_manager() for active_whim_msg in self._whim_goal_proto.active_whims: if not active_whim_msg.HasField('index'): continue whimset = aspiration_manager.get(active_whim_msg.whimset_guid) if whimset is None: logger.info( 'Trying to load unavailable ASPIRATION resource: {}', active_whim_msg.whimset_guid) else: goal_seed = GoalSeedling.deserialize_from_proto( active_whim_msg.goal_data) if goal_seed is None: continue target_sim_info = None if goal_seed.target_id: target_sim_info = sim_info_manager.get(goal_seed.target_id) if target_sim_info is None: continue else: secondary_sim_info = None if goal_seed.secondary_target_id: secondary_sim_info = sim_info_manager.get( goal_seed.secondary_target_id) if secondary_sim_info is None: continue else: whim_index = active_whim_msg.index goal = goal_seed.goal_type( sim_info=self._sim_info, goal_id=self._goal_id_generator(), inherited_target_sim_info=target_sim_info, secondary_sim_info=secondary_sim_info, count=goal_seed.count, reader=goal_seed.reader, locked=goal_seed.locked) goal.setup() goal.register_for_on_goal_completed_callback( self._on_goal_completed) whim_data = self._active_whims[whim_index] whim_data.whim = goal whim_data.whimset = whimset self._create_anti_thrashing_cooldown(whim_data) self._goals_dirty = True logger.info('Whim {} loaded.', goal_seed.goal_type) self._whim_goal_proto = None self._send_goals_update() def save_whims_info_to_proto(self, whim_tracker_proto, copy_existing=True): if self._sim_info.is_npc: return if copy_existing and self._whim_goal_proto is not None: whim_tracker_proto.CopyFrom(self._whim_goal_proto) return for (index, active_whim_data) in enumerate(self._active_whims): active_whim = active_whim_data.whim if active_whim is None: continue with ProtocolBufferRollback( whim_tracker_proto.active_whims) as active_whim_msg: active_whim_msg.whimset_guid = active_whim_data.whimset.guid64 active_whim_msg.index = index goal_seed = active_whim.create_seedling() goal_seed.finalize_creation_for_save() goal_seed.serialize_to_proto(active_whim_msg.goal_data) def debug_activate_whimset(self, whimset, chained): if not whimset.update_on_load: return self._activate_whimset(whimset) self._try_and_thrash_whims(whimset.activated_priority) def debug_activate_whim(self, whim): whim_data = self._active_whims[0] if whim_data.whim is not None: self._remove_whim(whim_data.whim, TelemetryWhimEvents.CANCELED) goal = whim(sim_info=self._sim_info, goal_id=self._goal_id_generator()) goal.setup() goal.register_for_on_goal_completed_callback(self._on_goal_completed) goal.show_goal_awarded_notification() whim_data.whim = goal whim_data.whimset = next(iter(self._active_whimsets_data.keys())) self._create_anti_thrashing_cooldown(whim_data) self._goals_dirty = True self._send_goals_update() def debug_offer_whim_from_whimset(self, whimset): if whimset.update_on_load: self._activate_whimset(whimset) whim_data = self._active_whims[0] if whim_data.whim is not None: self._remove_whim(whim_data.whim, TelemetryWhimEvents.CANCELED) goal = self._create_whim(whimset, set()) goal.setup() goal.register_for_on_goal_completed_callback(self._on_goal_completed) goal.show_goal_awarded_notification() whim_data.whim = goal whim_data.whimset = whimset self._create_anti_thrashing_cooldown(whim_data) self._goals_dirty = True self._send_goals_update() @property def _whims_needed(self): return self.max_whims - sum(1 for whim_info in self._active_whims if whim_info.whim is not None) @property def _sim_mood(self): return self._sim_info.get_mood() def _get_currently_active_whim_types(self): return { type(whim_data.whim) for whim_data in self._active_whims if whim_data.whim is not None } def _get_currently_used_whimsets(self): return { whim_data.whimset for whim_data in self._active_whims if whim_data.whimset is not None } def _get_whimsets_on_cooldown(self): return set(self._cooldown_alarms.keys()) def _get_whim_data(self, whim): for whim_data in self._active_whims: if whim is whim_data.whim: return whim_data def _get_whim_by_whim_type(self, whim_type): for whim_data in self._active_whims: if isinstance(whim_data.whim, whim_type): return whim_data.whim def _get_target_for_whimset(self, whimset): if whimset.force_target is None: whimset_data = self._active_whimsets_data.get(whimset) if whimset_data is not None: return whimset_data.target return else: return whimset.force_target(self._sim_info) def _deactivate_whimset(self, whimset): if whimset not in self._active_whimsets_data: return logger.info('Deactivating Whimset {}', whimset) if whimset.cooldown_timer > 0: def _cooldown_ended(_): if whimset in self._cooldown_alarms: del self._cooldown_alarms[whimset] self._cooldown_alarms[whimset] = alarms.add_alarm( self, create_time_span(minutes=whimset.cooldown_timer), _cooldown_ended) if whimset.timeout_retest is not None: resolver = event_testing.resolver.SingleSimResolver(self._sim_info) if resolver(whimset.timeout_retest.objective_test): self._activate_whimset(whimset) return del self._active_whimsets_data[whimset] if self._sim_info.aspiration_tracker is not None: self._sim_info.aspiration_tracker.reset_milestone(whimset) self._sim_info.remove_statistic(whimset.priority_commodity) def _activate_whimset(self, whimset, target=None, chained=False): if chained: new_priority = whimset.chained_priority else: new_priority = whimset.activated_priority if new_priority == 0: return self._sim_info.set_stat_value(whimset.priority_commodity, new_priority, add=True) whimset_data = self._active_whimsets_data.get(whimset) if whimset_data is None: stat = self._sim_info.get_stat_instance(whimset.priority_commodity) threshold = Threshold(whimset.priority_commodity.convergence_value, operator.le) def remove_active_whimset(_): self._deactivate_whimset(whimset) callback_data = stat.create_and_add_callback_listener( threshold, remove_active_whimset) self._active_whimsets_data[whimset] = _ActiveWhimsetData( target, callback_data) stat.decay_enabled = True logger.info('Setting whimset {} to active at priority {}.', whimset, new_priority) else: logger.info( 'Setting whimset {} which is already active to new priority value {}.', whimset, new_priority) def _remove_whim(self, whim, telemetry_event): whim.decommision() whim_data = self._get_whim_data(whim) whim_data.whim = None whim_data.whimset = None if whim_data.anti_thrashing_alarm_handle is not None: alarms.cancel_alarm(whim_data.anti_thrashing_alarm_handle) whim_data.anti_thrashing_alarm_handle = None if telemetry_event is not None: with telemetry_helper.begin_hook(writer, TELEMETRY_HOOK_WHIM_EVENT, sim_info=self._sim_info) as hook: hook.write_int(TELEMETRY_WHIM_EVENT_TYPE, telemetry_event) hook.write_guid(TELEMETRY_WHIM_GUID, whim.guid64) logger.info('Whim {} removed from whims tracker.', whim) self._goals_dirty = True def _on_goal_completed(self, whim, whim_completed): if not whim_completed: self._goals_dirty = True self._send_goals_update() return whim_data = self._get_whim_data(whim) parent_whimset = whim_data.whimset whim_type = type(whim) self._completed_goals[whim_type] = (whim, parent_whimset) inherited_target_sim_info = whim.get_actual_target_sim_info() self._remove_whim(whim, TelemetryWhimEvents.COMPLETED) services.get_event_manager().process_event( test_events.TestEvent.WhimCompleted, sim_info=self._sim_info, whim_completed=whim) should_deactivate_parent_whimset = parent_whimset.deactivate_on_completion highest_chained_priority = 0 for set_to_chain in parent_whimset.connected_whim_sets: if set_to_chain is parent_whimset: should_deactivate_parent_whimset = False if set_to_chain.chained_priority > highest_chained_priority: highest_chained_priority = set_to_chain.chained_priority self._activate_whimset(set_to_chain, target=inherited_target_sim_info, chained=True) connected_whimsets = parent_whimset.connected_whims.get(whim) if connected_whimsets is not None: for set_to_chain in connected_whimsets: if set_to_chain is parent_whimset: should_deactivate_parent_whimset = False if set_to_chain.chained_priority > highest_chained_priority: highest_chained_priority = set_to_chain.chained_priority self._activate_whimset(set_to_chain, target=inherited_target_sim_info, chained=True) if should_deactivate_parent_whimset: self._deactivate_whimset(parent_whimset) op = distributor.ops.SetWhimComplete(whim_type.guid64) Distributor.instance().add_op(self._sim_info, op) score = self.get_score_for_whim(whim.score) if score > 0: self._sim_info.add_whim_bucks(score, SetWhimBucks.WHIM, source=whim.guid64) logger.info('Goal completed: {}, from Whim Set: {}', whim, parent_whimset) thrashed = False if highest_chained_priority > 0: thrashed = self._try_and_thrash_whims( highest_chained_priority, extra_prohibited_whims={whim_type}) if not thrashed: self._offer_whims(prohibited_whims={whim_type}) def _create_whim(self, whimset, prohibited_whims): potential_target = self._get_target_for_whimset(whimset) if potential_target is None and whimset.force_target is not None: return if whimset.secondary_target is not None: secondary_target = whimset.secondary_target(self._sim_info) if secondary_target is None: return else: secondary_target = None sim = self._sim_info.get_sim_instance( allow_hidden_flags=ALL_HIDDEN_REASONS_EXCEPT_UNINITIALIZED) disallowed_whims = self._get_currently_active_whim_types( ) | prohibited_whims weighted_whims = [(possible_whim.weight, possible_whim.goal) for possible_whim in whimset.whims if possible_whim.goal not in disallowed_whims] while weighted_whims: selected_whim = sims4.random.pop_weighted(weighted_whims) old_whim_instance_and_whimset = self._completed_goals.get( selected_whim) if old_whim_instance_and_whimset is not None and old_whim_instance_and_whimset[ 0].is_on_cooldown(): continue pretest = selected_whim.can_be_given_as_goal( sim, None, inherited_target_sim_info=potential_target) if pretest: whim = selected_whim( sim_info=self._sim_info, goal_id=self._goal_id_generator(), inherited_target_sim_info=potential_target, secondary_sim_info=secondary_target) return whim def _create_anti_thrashing_cooldown(self, whim_data): def end_cooldown(_): whim_data.anti_thrashing_alarm_handle = None whim_data.anti_thrashing_alarm_handle = alarms.add_alarm( self, create_time_span(minutes=WhimsTracker.WHIM_ANTI_THRASHING_TIME), end_cooldown) def _offer_whims(self, prohibited_whimsets=EMPTY_SET, prohibited_whims=EMPTY_SET): if self._whims_needed == 0: return if self._sim_info.is_npc: return if not self._sim_info.is_instanced( allow_hidden_flags=ALL_HIDDEN_REASONS_EXCEPT_UNINITIALIZED): return if services.current_zone().is_zone_shutting_down: return whimsets_on_cooldown = self._get_whimsets_on_cooldown() for (index, whim_data) in enumerate(self._active_whims): if whim_data.whim is not None: continue if index == self.emotional_whim_index: emotional_whimset = self.get_emotional_whimset() if emotional_whimset is None: logger.info('No emotional whimset found for mood {}.', self._sim_mood) else: possible_whimsets = {emotional_whimset} possible_whimsets -= self._get_currently_used_whimsets() possible_whimsets -= prohibited_whimsets possible_whimsets -= whimsets_on_cooldown prioritized_whimsets = [(self.get_priority(whimset), whimset) for whimset in possible_whimsets] while prioritized_whimsets: whimset = sims4.random.pop_weighted( prioritized_whimsets) if whimset is None: break goal = self._create_whim(whimset, prohibited_whims) if goal is None: continue goal.setup() goal.register_for_on_goal_completed_callback( self._on_goal_completed) goal.show_goal_awarded_notification() whim_data.whim = goal whim_data.whimset = whimset self._create_anti_thrashing_cooldown(whim_data) with telemetry_helper.begin_hook( writer, TELEMETRY_HOOK_WHIM_EVENT, sim_info=self._sim_info) as hook: hook.write_int(TELEMETRY_WHIM_EVENT_TYPE, TelemetryWhimEvents.ADDED) hook.write_guid(TELEMETRY_WHIM_GUID, goal.guid64) self._goals_dirty = True break else: possible_whimsets = self.get_active_whimsets() possible_whimsets -= self._get_currently_used_whimsets() possible_whimsets -= prohibited_whimsets possible_whimsets -= whimsets_on_cooldown prioritized_whimsets = [(self.get_priority(whimset), whimset) for whimset in possible_whimsets] while prioritized_whimsets: whimset = sims4.random.pop_weighted(prioritized_whimsets) if whimset is None: break goal = self._create_whim(whimset, prohibited_whims) if goal is None: continue goal.setup() goal.register_for_on_goal_completed_callback( self._on_goal_completed) goal.show_goal_awarded_notification() whim_data.whim = goal whim_data.whimset = whimset self._create_anti_thrashing_cooldown(whim_data) with telemetry_helper.begin_hook( writer, TELEMETRY_HOOK_WHIM_EVENT, sim_info=self._sim_info) as hook: hook.write_int(TELEMETRY_WHIM_EVENT_TYPE, TelemetryWhimEvents.ADDED) hook.write_guid(TELEMETRY_WHIM_GUID, goal.guid64) self._goals_dirty = True break self._send_goals_update() def _try_and_thrash_whims(self, priority, extra_prohibited_whims=EMPTY_SET): whims_thrashed = set() for (index, whim_data) in enumerate(self._active_whims): if index == self.emotional_whim_index: continue if whim_data.whim is None: continue if not whim_data.anti_thrashing_alarm_handle is not None: if whim_data.whim.locked: continue if self.get_priority(whim_data.whimset) >= priority: continue if not sims4.random.random_chance( WhimsTracker.WHIM_THRASHING_CHANCE * 100): continue whims_thrashed.add(type(whim_data.whim)) self._remove_whim(whim_data.whim, TelemetryWhimEvents.CANCELED) if not whims_thrashed: return False prohibited_whims = whims_thrashed | extra_prohibited_whims self._offer_whims(prohibited_whims=prohibited_whims) return True def _send_goals_update(self): if not self._goals_dirty: return logger.debug('Sending whims update for {}. Current active whims: {}', self._sim_info, self._active_whims, owner='jjacobson') current_whims = [] for (index, whim_data) in enumerate(self._active_whims): whim = whim_data.whim if whim is None or self._hidden: whim_goal = DistributorOps_pb2.WhimGoal() current_whims.append(whim_goal) else: goal_target_id = 0 goal_whimset = whim_data.whimset goal_target = whim.get_required_target_sim_info() goal_target_id = goal_target.id if goal_target is not None else 0 whim_goal = DistributorOps_pb2.WhimGoal() whim_goal.whim_guid64 = whim.guid64 whim_name = whim.get_display_name() if whim_name is not None: whim_goal.whim_name = whim_name whim_goal.whim_score = self.get_score_for_whim(whim.score) whim_goal.whim_noncancel = whim.noncancelable whim_display_icon = whim.display_icon if whim_display_icon is not None: whim_goal.whim_icon_key.type = whim_display_icon.type whim_goal.whim_icon_key.group = whim_display_icon.group whim_goal.whim_icon_key.instance = whim_display_icon.instance whim_goal.whim_goal_count = whim.max_iterations whim_goal.whim_current_count = whim.completed_iterations whim_goal.whim_target_sim = goal_target_id whim_tooltip = whim.get_display_tooltip() if whim_tooltip is not None: whim_goal.whim_tooltip = whim_tooltip if index == self.emotional_whim_index: whim_goal.whim_mood_guid64 = self._sim_mood().guid64 else: whim_goal.whim_mood_guid64 = 0 whim_goal.whim_tooltip_reason = goal_whimset.whim_reason( *whim.get_localization_tokens()) whim_goal.whim_locked = whim.locked current_whims.append(whim_goal) if self._goals_dirty: self._sim_info.current_whims = current_whims self._goals_dirty = False @classproperty def _tracker_lod_threshold(cls): return SimInfoLODLevel.FULL def on_lod_update(self, old_lod, new_lod): if new_lod < self._tracker_lod_threshold: self.clean_up() elif old_lod < self._tracker_lod_threshold: sim_msg = services.get_persistence_service().get_sim_proto_buff( self._sim_info.id) if sim_msg is not None: self._sim_info.set_whim_bucks(sim_msg.gameplay_data.whim_bucks, SetWhimBucks.LOAD) self.cache_whim_goal_proto(sim_msg.gameplay_data.whim_tracker)
class GlobalGenderPreferenceTuning: __qualname__ = 'GlobalGenderPreferenceTuning' GENDER_PREFERENCE = TunableMapping( key_type=TunableEnumEntry( sims.sim_info_types.Gender, sims.sim_info_types.Gender.MALE, description='The gender to index the gender preference to.'), value_type=TunableReference( services.get_instance_manager(sims4.resources.Types.STATISTIC), description= 'The statistic that represents the matching gender preference'), description= 'A mapping between gender and the gender preference statistic for easy lookup.' ) GENDER_PREFERENCE_WEIGHTS = TunableList( description= 'A weightings list for the weighted random choice of sexual preference.', tunable=TunableTuple( gender_preference=TunableEnumEntry( GenderPreference, GenderPreference.LIKES_NEITHER, description='The gender to index the gender preference to.'), weight=Tunable(int, 0, description='The minimum possible skill.'), description= 'A mapping between gender and the gender preference statistic for easy lookup.' )) GENDER_PREFERENCE_MAPPING = TunableMapping( key_type=TunableEnumEntry( GenderPreference, GenderPreference.LIKES_NEITHER, description='The gender to index the gender preference to.'), value_type=TunableMapping( key_type=TunableEnumEntry( sims.sim_info_types.Gender, sims.sim_info_types.Gender.MALE, description='The gender to index the gender preference to.'), value_type=TunableSet( TunableReference( services.get_instance_manager( sims4.resources.Types.STATISTIC), description= 'The statistic that represents the matching gender preference' )), description= 'A mapping between gender and the gender preference statistic for easy lookup.' ), description= 'A mapping between gender and the gender preference statistic for easy lookup.' ) enable_autogeneration_same_sex_preference = False ENABLE_AUTOGENERATION_SAME_SEX_PREFERENCE_THRESHOLD = Tunable( description= "\n A value that, once crossed, indicates the player's allowance of same-\n sex relationships with townie auto-generation.\n ", tunable_type=float, default=1.0) ENABLED_AUTOGENERATION_SAME_SEX_PREFERENCE_WEIGHTS = TunableList( description= '\n An alternative weightings list for the weighted random choice of sexual\n preference after a romantic same-sex relationship has been kindled.\n ', tunable=TunableTuple( gender_preference=TunableEnumEntry( GenderPreference, GenderPreference.LIKES_NEITHER, description='The gender to index the gender preference to.'), weight=Tunable(int, 0, description='The minimum possible skill.'), description= 'A mapping between gender and the gender preference statistic for easy lookup.' ))
class CareerTone(AwayAction): INSTANCE_TUNABLES = { 'dominant_tone_loot_actions': TunableList( description= '\n Loot to apply at the end of a work period if this tone ran for the\n most amount of time out of all tones.\n ', tunable=TunableReference( manager=services.get_instance_manager( sims4.resources.Types.ACTION), class_restrictions=('LootActions', 'RandomWeightedLoot'))), 'performance_multiplier': Tunable( description= '\n Performance multiplier applied to work performance gain.\n ', tunable_type=float, default=1), 'periodic_sim_filter_loot': TunableList( description= '\n Loot to apply periodically to between the working Sim and other\n Sims, specified via a Sim filter.\n \n Example Usages:\n -Gain relationship with 2 coworkers every hour.\n -Every hour, there is a 5% chance of meeting a new coworker.\n ', tunable=TunableTuple( chance=SuccessChance.TunableFactory( description= '\n Chance per hour of loot being applied.\n ' ), sim_filter=TunableSimFilter.TunableReference( description= '\n Filter for specifying who to set at target Sims for loot\n application.\n ' ), max_sims=OptionalTunable( description= '\n If enabled and the Sim filter finds more than the specified\n number of Sims, the loot will only be applied a random\n selection of this many Sims.\n ', tunable=TunableRange(tunable_type=int, default=1, minimum=1)), loot=LootActions.TunableReference( description= '\n Loot actions to apply to the two Sims. The Sim in the \n career is Actor, and if Targeted is enabled those Sims\n will be TargetSim.\n ' ))) } runtime_commodity = None @classmethod def _tuning_loaded_callback(cls): if cls.runtime_commodity is not None: return commodity = RuntimeCommodity.generate(cls.__name__) commodity.decay_rate = 0 commodity.convergence_value = 0 commodity.remove_on_convergence = True commodity.visible = False commodity.max_value_tuning = date_and_time.SECONDS_PER_WEEK commodity.min_value_tuning = 0 commodity.initial_value = 0 commodity._time_passage_fixup_type = CommodityTimePassageFixupType.DO_NOT_FIXUP cls.runtime_commodity = commodity def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._update_alarm_handle = None self._last_update_time = None def run(self, callback): super().run(callback) self._last_update_time = services.time_service().sim_now time_span = clock.interval_in_sim_minutes( Career.CAREER_PERFORMANCE_UPDATE_INTERVAL) self._update_alarm_handle = alarms.add_alarm( self, time_span, lambda alarm_handle: self._update(), repeating=True) def stop(self): if self._update_alarm_handle is not None: alarms.cancel_alarm(self._update_alarm_handle) self._update_alarm_handle = None self._update() super().stop() def _update(self): career = self.sim_info.career_tracker.get_at_work_career() if career is None: logger.error( 'CareerTone {} trying to update performance when Sim {} not at work', self, self.sim_info, owner='tingyul') return if career._upcoming_gig is not None and career._upcoming_gig.odd_job_tuning is not None: return now = services.time_service().sim_now elapsed = now - self._last_update_time self._last_update_time = now career.apply_performance_change(elapsed, self.performance_multiplier) career.resend_career_data() resolver = SingleSimResolver(self.sim_info) for entry in self.periodic_sim_filter_loot: chance = entry.chance.get_chance(resolver) * elapsed.in_hours() if random.random() > chance: continue services.sim_filter_service().submit_filter( entry.sim_filter, self._sim_filter_loot_response, callback_event_data=entry, requesting_sim_info=self.sim_info, gsi_source_fn=self.get_sim_filter_gsi_name) def get_sim_filter_gsi_name(self): return str(self) def _sim_filter_loot_response(self, filter_results, callback_event_data): entry = callback_event_data if entry.max_sims is None: targets = tuple(result.sim_info for result in filter_results) else: sample_size = min(len(filter_results), entry.max_sims) targets = tuple( result.sim_info for result in random.sample(filter_results, sample_size)) for target in targets: resolver = DoubleSimResolver(self.sim_info, target) entry.loot.apply_to_resolver(resolver) def apply_dominant_tone_loot(self): resolver = self.get_resolver() for loot in self.dominant_tone_loot_actions: loot.apply_to_resolver(resolver)
class VetClinicZoneDirector(BusinessZoneDirectorMixin, VisitorSituationOnArrivalZoneDirectorMixin, SchedulingZoneDirector): INSTANCE_TUNABLES = { 'customer_situation_type_curve': SituationCurve.TunableFactory( description= "\n When customer situations are being generated, they'll be pulled\n based on the tuning in this.\n \n The desired count in this tuning is not used.\n \n Otherwise it situation count is pulled from business multipliers.\n ", tuning_group=GroupNames.BUSINESS, get_create_params={'user_facing': False}), 'employee_situation': TunableReference( description= '\n Employee situation to put employees in. \n ', manager=services.get_instance_manager(Types.SITUATION), tuning_group=GroupNames.BUSINESS), 'exam_table_test': TunableObjectMatchesDefinitionOrTagTest( description= '\n Tests used to count number of exam tables that are in this zone. \n The number of these found will limit the number of customers \n situations that are generated.\n ', tuning_group=GroupNames.BUSINESS), 'podium_call_to_action': TunableReference( description= '\n Call to action to use to highlight the vet podium when visiting the vet.\n ', manager=services.get_instance_manager( sims4.resources.Types.CALL_TO_ACTION)), 'waiting_customer_cap': _ObjectBasedWaitingCustomerCap.TunableFactory() } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._num_exam_tables = 0 self._default_uniform = {} self._custom_uniform = {} self._vet_to_customer_assignments = {} self._waiting_situations = OrderedDict() self._reservations = {} self._has_cta_been_seen = False self._cta_disabled = False def _save_custom_zone_director(self, zone_director_proto, writer): writer.write_uint64s(TRACKED_VET_ASSIGNMENTS_VETS, list(self._vet_to_customer_assignments.keys())) for (vet_id, customer_assignments ) in self._vet_to_customer_assignments.items(): writer.write_uint64s( TRACKED_VET_ASSIGNMENTS_CUSTOMERS.format(vet_id), list(customer_assignments)) writer.write_uint64s(TRACKED_WAITING_SITUATION_IDS, list(self._waiting_situations.keys())) for (situation_id, waiting_situations) in self._waiting_situations.items(): writer.write_uint64s( TRACKED_WAITING_SITUATION_CUSTOMERS.format(situation_id), list(waiting_situations)) writer.write_bool(CTA_DISABLED, self._cta_disabled) super()._save_custom_zone_director(zone_director_proto, writer) def _load_custom_zone_director(self, zone_director_proto, reader): if reader is not None: vets_with_assigned_customers = reader.read_uint64s( TRACKED_VET_ASSIGNMENTS_VETS, []) for vet_id in vets_with_assigned_customers: assigned_customers = reader.read_uint64s( TRACKED_VET_ASSIGNMENTS_CUSTOMERS.format(vet_id), []) if assigned_customers: self._vet_to_customer_assignments[vet_id] = list( assigned_customers) waiting_situation_ids = reader.read_uint64s( TRACKED_WAITING_SITUATION_IDS, []) for situation_id in waiting_situation_ids: situation_customers = reader.read_uint64s( TRACKED_WAITING_SITUATION_CUSTOMERS.format(situation_id), []) if situation_customers: self._waiting_situations[situation_id] = list( situation_customers) self._cta_disabled = reader.read_bool(CTA_DISABLED, False) super()._load_custom_zone_director(zone_director_proto, reader) def on_startup(self): super().on_startup() self._load_default_uniforms() self.refresh_configuration() def clear_state(self): self._vet_to_customer_assignments.clear() self._waiting_situations.clear() self._reservations.clear() def on_loading_screen_animation_finished(self): if any(sim_info.is_pet for sim_info in self._traveled_sim_infos): self._trigger_podium_call_to_action() super().on_loading_screen_animation_finished() def handle_sim_summon_request(self, sim_info, purpose): super().handle_sim_summon_request(sim_info, purpose) if sim_info.is_pet: self._trigger_podium_call_to_action() def _trigger_podium_call_to_action(self): if services.current_zone( ).active_household_changed_between_save_and_load( ) or services.current_zone().time_has_passed_in_world_since_zone_save( ): self._cta_disabled = False if self._cta_disabled: return if self._has_cta_been_seen or self._business_manager.is_active_household_and_zone( ): return services.call_to_action_service().begin(self.podium_call_to_action, self) self._has_cta_been_seen = True def on_cta_ended(self, value): self._cta_disabled = True def on_shutdown(self): if self._business_manager is not None: self._business_manager.prepare_for_off_lot_simulation() super().on_shutdown() def on_exit_buildbuy(self): super().on_exit_buildbuy() self.refresh_configuration() def create_situations_during_zone_spin_up(self): if self.business_manager is not None and self.business_manager.is_open: if services.current_zone( ).time_has_passed_in_world_since_zone_save( ) or services.current_zone( ).active_household_changed_between_save_and_load(): self.clear_state() self._business_manager.start_already_opened_business() self._on_customer_situation_request() super().create_situations_during_zone_spin_up() def _process_traveled_sim(self, sim_info): current_zone = services.current_zone() if current_zone.is_first_visit_to_zone or not not ( current_zone.time_has_passed_in_world_since_zone_save() or not (current_zone. active_household_changed_between_save_and_load() or not (sim_info.startup_sim_location is not None and services.active_lot().is_position_on_lot( sim_info.startup_sim_location.transform. translation)))): super()._process_traveled_sim(sim_info) else: self._request_spawning_of_sim_at_location( sim_info, sims.sim_spawner_service.SimSpawnReason.TRAVELING) def _process_zone_saved_sim(self, sim_info): if services.current_zone( ).time_has_passed_in_world_since_zone_save() or services.current_zone( ).active_household_changed_between_save_and_load(): business_manager = services.business_service( ).get_business_manager_for_zone() if business_manager is not None and business_manager.is_employee( sim_info): self._on_reinitiate_zone_saved_sim(sim_info) else: self._on_clear_zone_saved_sim(sim_info) else: super()._process_zone_saved_sim(sim_info) def _should_create_npc_business_manager(self): return True def _get_new_npc_business_manager(self): npc_business_manager = VetClinicManager() npc_business_manager.set_zone_id(services.current_zone_id()) npc_business_manager.set_owner_household_id(None) return npc_business_manager def _get_employee_situation_for_employee_type(self, employee_type): return self.employee_situation def _get_npc_employee_situation_for_employee_type(self, employee_type): return self.employee_situation def _get_desired_employee_count(self, employee_type): return self._num_exam_tables def _on_customer_situation_request(self): self.remove_stale_customer_situations() desired_situation_count = self._get_num_desired_customer_situations() current_customer_count = len(self._customer_situation_ids) if current_customer_count >= desired_situation_count: waiting_customers = sum(1 for _ in self.customer_situations_gen( lambda s: not s.customer_has_been_seen)) waiting_customer_cap = self.waiting_customer_cap.get_cap_amount() if waiting_customer_cap <= waiting_customers: return (new_customer_situation, params ) = self.customer_situation_type_curve.get_situation_and_params() if new_customer_situation is None: return else: situation_id = self.start_customer_situation( new_customer_situation, create_params=params) if situation_id is None: logger.info( 'Trying to create a new customer situation for vet clinic but failed.' ) return def apply_zone_outfit(self, sim_info, situation): (outfit_data, outfit_key) = self.get_zone_outfit(sim_info) if outfit_data is not None: sim_info.generate_merged_outfit(outfit_data, (OutfitCategory.CAREER, 0), sim_info.get_current_outfit(), outfit_key) sim_info.set_current_outfit((OutfitCategory.CAREER, 0)) sim_info.resend_current_outfit() def get_zone_outfit(self, sim_info): gender = sim_info.clothing_preference_gender (outfit_index, outfit_data) = self._custom_uniform.get(gender, (0, None)) if outfit_data is None: outfit_data = self._default_uniform.get(gender, None) return (outfit_data, (OutfitCategory.CAREER, outfit_index)) def _load_default_uniforms(self): self._default_uniform[Gender.MALE] = self._load_uniform_from_resource( VetClinicTuning.UNIFORM_EMPLOYEE_MALE) self._default_uniform[ Gender.FEMALE] = self._load_uniform_from_resource( VetClinicTuning.UNIFORM_EMPLOYEE_FEMALE) def _load_uniform_from_resource(self, uniform_resource): sim_info_wrapper = SimInfoBaseWrapper() sim_info_wrapper.load_from_resource(uniform_resource) sim_info_wrapper.set_current_outfit((OutfitCategory.CAREER, 0)) return sim_info_wrapper def refresh_configuration(self): self._update_from_venue_config() self._update_exam_table_count() def _update_from_venue_config(self): config_data = build_buy.get_current_venue_config( services.current_zone_id()) if config_data is None: return vet_clinic_config = Venue_pb2.VetClinicConfiguration() vet_clinic_config.ParseFromString(config_data) self._custom_uniform.clear() for (i, outfit_data) in enumerate(vet_clinic_config.outfits): if i not in VetEmployeeOutfitType: break gender = Gender.MALE if i == VetEmployeeOutfitType.MALE_EMPLOYEE else Gender.FEMALE sim_info_wrapper = None mannequin_data = outfit_data.mannequin if mannequin_data.HasField('mannequin_id'): sim_info_wrapper = SimInfoBaseWrapper() sim_info_wrapper.load_sim_info(outfit_data.mannequin) sim_info_wrapper.set_current_outfit((OutfitCategory.CAREER, 0)) self._custom_uniform[gender] = (outfit_data.outfit_index, sim_info_wrapper) def _update_exam_table_count(self): self._num_exam_tables = sum( 1 for obj in services.object_manager().get_valid_objects_gen() if self.exam_table_test(objects=(obj, ))) if self._business_manager is not None: self._business_manager.set_exam_table_count(self._num_exam_tables) @property def num_exam_tables(self): return self._num_exam_tables def _get_num_desired_customer_situations(self): business_manager = self._business_manager if business_manager is None or business_manager.is_owned_by_npc: return self._num_exam_tables situation_count = business_manager.get_ideal_customer_count() tracker = services.business_service( ).get_business_tracker_for_household( business_manager.owner_household_id, business_manager.business_type) situation_count += tracker.addtitional_customer_count return situation_count def on_customers_waiting(self, situation_id, customer_ids, player_situation=False): self._waiting_situations[situation_id] = customer_ids if player_situation: self._waiting_situations.move_to_end(situation_id, last=False) def on_vet_assigned(self, situation_id, vet_id, customer_ids): if situation_id in self._reservations: del self._reservations[situation_id] if situation_id in self._waiting_situations: del self._waiting_situations[situation_id] self._vet_to_customer_assignments[vet_id] = customer_ids def on_customer_situation_being_destroyed(self, situation_id): if situation_id in self._waiting_situations: del self._waiting_situations[situation_id] if situation_id in self._reservations: del self._reservations[situation_id] def remove_from_vet(self, vet_id): if vet_id in self._vet_to_customer_assignments.keys(): del self._vet_to_customer_assignments[vet_id] def is_assigned_to_vet(self, customer_id, vet_id=None): if vet_id is not None: customers = self._vet_to_customer_assignments.get(vet_id, tuple()) return customer_id in customers for cust_id in itertools.chain( self._vet_to_customer_assignments.values()): if cust_id == customer_id: return True return False def is_waiting_for_services(self, customer_sim_id): for situation_id in self._waiting_situations: if customer_sim_id in self._waiting_situations[situation_id]: return True return False def is_vet_attending_any_customers(self, vet_id): if vet_id in self._vet_to_customer_assignments.keys(): return len(self._vet_to_customer_assignments[vet_id]) > 0 return False def customer_situations_gen(self, criteria_test=None): situation_manager = services.get_zone_situation_manager() for situation_id in self._customer_situation_ids: situation = situation_manager.get(situation_id) if situation is None: continue if criteria_test is None: yield situation elif criteria_test(situation): yield situation def waiting_sims_gen(self, potential_reserver_id): now = services.time_service().sim_now for situation_id in self._waiting_situations: if situation_id in self._reservations: reservation = self._reservations[situation_id] if not not now < reservation['expiration'] and reservation[ 'reserver_id'] != potential_reserver_id: continue else: for sim_id in self._waiting_situations[situation_id]: yield services.object_manager().get(sim_id) def reserve_waiting_sim(self, reserved_sim_id, reserver_id): for situation_id in self._waiting_situations: if reserved_sim_id in self._waiting_situations[situation_id]: self._reservations[situation_id] = { 'expiration': services.time_service().sim_now + interval_in_sim_minutes(30), 'reserver_id': reserver_id } def bill_owner_for_treatment(self, sim): if self._business_manager is not None: for customer_situation in self.customer_situations_gen(): if not customer_situation.is_sim_in_situation(sim): continue self._business_manager.bill_owner_for_treatment( *customer_situation.get_payment_data()) customer_situation.apply_value_of_service() break @property def supported_business_types(self): return SUPPORTED_BUSINESS_TYPES
class LicenseSongSuperInteraction(SuperInteraction): INSTANCE_TUNABLES = { 'music_styles': TunableList( TunableReference( description= '\n Which music styles are available for this interaction. This\n should be only the Written Music Style for the particular\n instrument.\n ', manager=services.get_instance_manager( sims4.resources.Types.RECIPE), class_restrictions=(MusicStyle, ), reload_dependent=True)) } @classmethod def _verify_tuning_callback(cls): for style in cls.music_styles: for track in style.music_tracks: if not track.check_for_unlock: logger.error( "MusicTrack {} does not have check_for_unlock set to False. This is required for MusicTracks that can be 'Licensed'.", track.__name__) def __init__(self, aop, context, track=None, unlockable_name=None, **kwargs): super().__init__(aop, context, unlockable_name=unlockable_name, **kwargs) self._track = track self._unlockable_name = unlockable_name @flexmethod def _get_name(cls, inst, target=DEFAULT, context=DEFAULT, track=None, unlockable_name=None, **kwargs): if unlockable_name is not None and track.music_track_name is not None: return track.music_track_name(unlockable_name) inst_or_cls = inst if inst is not None else cls return super(SuperInteraction, inst_or_cls)._get_name(target=target, context=context, **kwargs) @classmethod def potential_interactions(cls, target, context, **kwargs): if context.sim is None: return if context.sim.sim_info.unlock_tracker is None: return for style in cls.music_styles: for track in style.music_tracks: unlocks = context.sim.sim_info.unlock_tracker.get_unlocks( track) if unlocks: for unlock in unlocks: yield AffordanceObjectPair( cls, target, cls, None, track=unlock.tuning_class, pie_menu_category=style.pie_menu_category, unlockable_name=unlock.name, **kwargs)
class DoParameterizedAutonomyPingFromRole(HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = { 'description': '\n Parameterized autonomy ping to run either during preroll or\n as soon as the sim is assigned this role.\n ', 'commodities': TunableSet( description= '\n Set of commodities to run parameterized autonomy against after\n running this interaction.\n ', tunable=TunableReference( description= '\n The type of commodity to search for.\n ', manager=services.statistic_manager())), 'static_commodities': TunableSet( description= '\n Set of static commodities to run parameterized autonomy against\n after running this interaction.\n ', tunable=TunableReference( description= '\n The type of static commodity to search for.\n ', manager=services.static_commodity_manager())), 'source': TunableEnumEntry( description= '\n Set this to *spoof* the source of this interaction. This will have\n various gameplay effects and should be used after due consideration.\n ', tunable_type=InteractionSource, default=InteractionSource.AUTONOMY), 'priority': TunableEnumEntry( description= '\n The priority level at which this autonomy will run.\n ', tunable_type=Priority, default=Priority.Low), 'run_priority': OptionalTunable( description= '\n If enabled, specify the run priority at which the selected affordance\n (if any is selected) will run.\n ', tunable=TunableEnumEntry(tunable_type=Priority, default=Priority.Low)), 'radius_to_consider': Tunable( description= '\n The radius around the sim that targets must be in to be valid\n for Parameterized Autonomy. Anything outside this radius will\n be ignored. A radius of 0 is considered infinite.\n ', tunable_type=float, default=0), 'consider_scores_of_zero': Tunable( description= '\n The autonomy request will consider scores of zero. This allows sims to to choose things they \n might not desire.\n ', tunable_type=bool, default=False), 'test_connectivity_to_target': Tunable( description= '\n If checked, this test will ensure the Sim can pass a pt to\n pt connectivity check to the advertising object.\n ', tunable_type=bool, default=True), 'off_lot_rule': OptionalTunable(tunable=TunableOffLotAutonomy()), 'full_ping_on_fail': OptionalTunable( description= '\n If enabled, if the parameterized ping fails to find an interaction\n it will do a full autonomy ping after the specified delay.\n ', tunable=TunableTimeSpan(default_minutes=1)) } def __call__(self, role_state, role_affordance_target, situation=None): sim = role_state.sim context = InteractionContext(sim, self.source, self.priority, run_priority=self.run_priority, bucket=InteractionBucketType.DEFAULT) autonomy_request = autonomy.autonomy_request.AutonomyRequest( sim, FullAutonomy, commodity_list=self.commodities, static_commodity_list=self.static_commodities, apply_opportunity_cost=False, is_script_request=True, context=context, si_state_view=sim.si_state, limited_autonomy_allowed=True, radius_to_consider=self.radius_to_consider, consider_scores_of_zero=self.consider_scores_of_zero, autonomy_mode_label_override='ParameterizedAutonomy', off_lot_autonomy_rule_override=self.off_lot_rule, test_connectivity_to_target_object=self. test_connectivity_to_target, reping_delay_on_fail=self.full_ping_on_fail() if self.full_ping_on_fail is not None else None) sim.queue_autonomy_request(autonomy_request) sim.run_full_autonomy_next_ping()
class DestroySpecifiedObjectsFromTargetInventory(XevtTriggeredElement, HasTunableFactory, AutoFactoryInit): ALL = 'ALL' FACTORY_TUNABLES = { 'description': '\n Destroy every object in the target inventory that passes the tuned\n tests.\n ', 'inventory_owner': TunableEnumEntry( description= '\n The participant of the interaction whose inventory will be checked\n for objects to destroy.\n ', tunable_type=ParticipantType, default=ParticipantType.Object), 'object_tests': TunableTestSet( description= '\n A list of tests to apply to all objects in the target inventory.\n Every object that passes these tests will be destroyed.\n ' ), 'count': TunableVariant( description= "\n The max number of objects to destroy. For example: A Sim has 2\n red guitars and 1 blue guitar, and we're destroying guitars with\n count = 2. Possible destroyed objects are: 2 red guitars, or 1 red\n guitar and 1 blue guitar.\n ", number=TunableRange(tunable_type=int, default=1, minimum=0), locked_args={'all': ALL}, default='all'), 'loots_to_run_before_destroy': TunableList( description= '\n A list of loots to be run before destroying the object. The loots\n will have the tuned participant as the Actor and the object being\n destroyed as the target.\n ', tunable=TunableReference( description= '\n A reference to a loot to run against the object being destroyed.\n ', manager=services.get_instance_manager( sims4.resources.Types.ACTION))) } def _do_behavior(self): participant = self.interaction.get_participant(self.inventory_owner) inventory = participant.inventory_component if inventory is None: logger.error( 'Participant {} does not have an inventory to check for objects to destroy.', participant, owner='tastle') return objects_to_destroy = set() for obj in inventory: single_object_resolver = event_testing.resolver.SingleObjectResolver( obj) if not self.object_tests.run_tests(single_object_resolver): continue objects_to_destroy.add(obj) num_destroyed = 0 for obj in objects_to_destroy: if self.count == self.ALL: count = obj.stack_count() else: count = min(self.count - num_destroyed, obj.stack_count()) resolver = SingleActorAndObjectResolver( participant.sim_info, obj, self) if participant.is_sim else DoubleObjectResolver( obj, self) for loot in self.loots_to_run_before_destroy: loot.apply_to_resolver(resolver) if not inventory.try_destroy_object( obj, count=count, source=inventory, cause= 'Destroying specified objects from target inventory extra.' ): logger.error('Error trying to destroy object {}.', obj, owner='tastle') num_destroyed += count if self.count != self.ALL: if num_destroyed >= self.count: break objects_to_destroy.clear()
class PushAffordanceFromRole(HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = { 'description': '\n Push the specific affordance onto the sim.\n ', 'affordance': TunableReference(manager=services.affordance_manager()), 'super_affordance_for_mixer': OptionalTunable( description= '\n If we want to push mixer directly in the affordance tuning for this\n role state, we would need to provide a super affordance here to\n handle the mixer.\n ', tunable=TunableReference(manager=services.affordance_manager()), disabled_name='do_not_need_si', enabled_name='provide_si'), 'source': TunableEnumEntry(tunable_type=InteractionSource, default=InteractionSource.SCRIPT), 'priority': TunableEnumEntry( description= '\n Priority to push the interaction\n ', tunable_type=Priority, default=Priority.High), 'run_priority': TunableEnumEntry( description= '\n Priority to run the interaction. None means use the (push) priority\n ', tunable_type=Priority, default=None), 'target': TunableEnumEntry( description= '\n The target of the affordance. We will try to get\n the target from the situation the role sim is\n running.\n ', tunable_type=SituationAffordanceTarget, default=SituationAffordanceTarget.NO_TARGET), 'leave_situation_on_failure': Tunable( description= '\n If set to True, when push affordance on the sim fails, sim will\n leave the situation.\n ', tunable_type=bool, default=False), 'add_situation_liability': Tunable( description= '\n If set to True, we will add a liability to the pushed interaction\n such that we will cancel the situation owning this role state\n if the interaction (and its continuations) are completed or \n canceled.\n ', tunable_type=bool, default=False) } def __call__(self, role_state, role_affordance_target, situation=None, **kwargs): sim = role_state.sim affordance = self.affordance source = self.source priority = self.priority run_priority = self.run_priority if run_priority is None: run_priority = priority interaction_context = InteractionContext(sim, source, priority, run_priority=run_priority, **kwargs) target = role_state._get_target_for_push_affordance( self.target, situation=situation, role_affordance_target=role_affordance_target) try: push_result = False if affordance.is_super: push_result = sim.push_super_affordance( affordance, target, interaction_context) else: super_affordance = self.super_affordance_for_mixer if super_affordance is not None: potential_parent_si = sim.si_state.get_si_by_affordance( super_affordance) if potential_parent_si is not None: aop = AffordanceObjectPair(affordance, target, super_affordance, potential_parent_si) push_result = aop.test_and_execute(interaction_context) if push_result: if self.add_situation_liability: liability = SituationLiability(situation) push_result.interaction.add_liability( SITUATION_LIABILITY, liability) elif self.leave_situation_on_failure: situation_manager = services.get_zone_situation_manager() situation_manager.remove_sim_from_situation(sim, situation.id) except AttributeError: logger.error( 'Attribute Error occurred pushing interaction {} on sim: {} for role_state:{}', affordance, sim, role_state, owner='msantander') raise
def __init__(self, **kwargs): super().__init__(skill_to_test=TunableReference( services.statistic_manager(), description='The skill used to earn the Simoleons, if applicable.' ), **kwargs)