class EmergencyFrequency(HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = { 'max_emergency_situations_per_shift': TunableRange( description= '\n The maximum number of times during a shift that an emergency\n situation can be created/started.\n ', tunable_type=int, default=1, minimum=0), 'inital_lockout_in_sim_minutes': TunableSimMinute( description= '\n The time, in Sim minutes, that pass at the beginning of a shift\n before the first check for creating/starting an emergency happens.\n ', default=60), 'cool_down_in_sim_minutes': TunableSimMinute( description= '\n How often a check for whether or not to create/start an emergency\n happens.\n ', default=60), 'percent_chance_of_emergency': TunablePercent( description= '\n The percentage chance that on any given check an emergency is to\n to be created/started.\n ', default=30), 'weighted_situations': TunableList( description= '\n A weighted list of situations to be used as emergencies. When a\n check passes to create/start an emergency, this is the list\n of emergency situations that will be chosen from.\n ', tunable=TunableTuple(situation=Situation.TunableReference(), weight=Tunable(tunable_type=int, default=1))) }
class BillReduction(GlobalPolicyEffect, HasTunableSingletonFactory, HasTunableFactory, AutoFactoryInit): FACTORY_TUNABLES = { 'percent_reduction': TunableTuple( description= '\n A mapping of bill reduction reason to percent reduction. Reasons for bill\n reduction can be added to sims.bills tuning.\n ', reduction_reason=TunableEnumEntry( description= '\n Reason for bill reduction.\n ', tunable_type=BillReductionEnum, default=BillReductionEnum.GlobalPolicy_ControlInvasiveSpecies), reduction_amount=TunablePercent( description= '\n Percent by which all household bills are reduced.\n ', default=50)) } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def turn_on(self, _, from_load=False): services.global_policy_service().add_bill_reduction( self.percent_reduction.reduction_reason, self.percent_reduction.reduction_amount) def turn_off(self, _): services.global_policy_service().remove_bill_reduction( self.percent_reduction.reduction_reason)
def __init__(self, **kwargs): super().__init__(percent_change_amount=TunablePercent( description= '\n Percent of current value of statistic should amount\n be changed. If you want to decrease the amount by\n 50% enter -50% into the tuning field.', default=-50, minimum=-100), **kwargs)
def _get_tunable_household_member_list(template_type, is_optional=False): if template_type == SimTemplateType.PREMADE_HOUSEHOLD: template_reference_type = PremadeSimTemplate else: template_reference_type = TunableSimTemplate tuple_elements = { 'sim_template': template_reference_type.TunableReference( description= ' \n A template to use for creating a household member. If this\n references a resource that is not installed, the household member is\n ignored and the family is going to be created without this\n individual.\n ', pack_safe=is_optional), 'household_member_tag': TunableEnumWithFilter( description= ' \n Tag to be used to create relationship between sim members. This does\n NOT have to be unique for all household templates. If you want to\n add more tags in the tag tuning just add with prefix of\n household_member.r.\n ', tunable_type=tag.Tag, default=tag.Tag.INVALID, filter_prefixes=HOUSEHOLD_FILTER_PREFIX) } if is_optional: tuple_elements['chance'] = TunablePercent( description= '\n The chance that this household member is created when the household\n is created. This is useful for "optional" Sims. For example, you\n might want to tune a third of typical nuclear families to own a dog,\n should the resource be available.\n ', default=100) else: tuple_elements['locked_args'] = {'chance': 1} return TunableList( description= '\n A list of sim templates that will make up the sims in this household.\n ', tunable=TunableTuple(**tuple_elements))
class RelationshipBitLock(metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager( sims4.resources.Types.RELATIONSHIP_LOCK)): INSTANCE_TUNABLES = { 'group_id': TunableEnumEntry( description= '\n The group that this lock applies to. No two locks can belong to\n the same group.\n ', tunable_type=RelationshipBitType, default=RelationshipBitType.Invalid, invalid_enums=(RelationshipBitType.Invalid, RelationshipBitType.NoGroup)), 'timeout': TunableSimMinute( description= '\n The amount of time in Sim minutes that this Relationship Bit Lock\n will be locked before potentially allowing a Relationship Bit\n Change.\n ', default=360, minimum=1), 'relock_percentage': TunablePercent( description= '\n The percent chance that we will just relock this Relationship Bit\n Lock and prevent a change when one attempts to occur. If we are\n relocked then we will not change the bit.\n ', default=0) } relationship_bit_cache = None @classmethod def get_lock_type_for_group_id(cls, group_id): return cls.relationship_bit_cache.get(group_id, None) def __init__(self): self._locked_time = DATE_AND_TIME_ZERO @property def end_time(self): return self._locked_time + clock.interval_in_sim_minutes(self.timeout) def lock(self): self._locked_time = services.time_service().sim_now def unlock(self): self._locked_time = DATE_AND_TIME_ZERO def try_and_aquire_lock_permission(self): if self._locked_time == DATE_AND_TIME_ZERO: return True now = services.time_service().sim_now if now < self.end_time: return False elif sims4.random.random_chance(self.relock_percentage * 100): self.lock() return False return True def save(self, msg): msg.relationship_bit_lock_type = self.guid64 msg.locked_time = self._locked_time.absolute_ticks() def load(self, msg): self._locked_time = DateAndTime(msg.locked_time)
class _TargetActionRules(HasTunableFactory, AutoFactoryInit): FACTORY_TUNABLES = { 'chance': TunablePercent( description= '\n A random chance of this action getting applied (default 100%).\n ', default=100), 'test': TunableTestSet( description= '\n A test to decide whether or not to apply this particular set of actions to the target object.\n ', tuning_group=GroupNames.TESTS), 'actions': TunableList( description= '\n A list of one or more ObjectRoutingBehaviorActions to run on the\n target object after routing to it. These are applied in sequence.\n ', tunable=TunableVariant( play_animation=ObjectRoutingBehaviorActionAnimation. TunableFactory(), destroy_objects=ObjectRoutingBehaviorActionDestroyObjects. TunableFactory(), apply_loot=ObjectRoutingBehaviorActionApplyLoot.TunableFactory( ), default='play_animation')), 'abort_if_applied': Tunable( description= "\n Don't run any further actions from this list of action rules if \n conditions are met and this action is executed.\n ", tunable_type=bool, default=False) }
class BroadcasterEffectStartFire(_BroadcasterEffectTested): __qualname__ = 'BroadcasterEffectStartFire' FACTORY_TUNABLES = {'percent_chance': TunablePercent(description='\n A value between 0 - 100 which represents the percent chance to \n start a fire when reacting to the broadcaster.\n ', default=50)} def _apply_broadcaster_effect(self, broadcaster, affected_object): if random.random() <= self.percent_chance: fire_service = services.get_fire_service() fire_service.spawn_fire_at_object(affected_object)
class SuccessChance(HasTunableSingletonFactory, AutoFactoryInit): __qualname__ = 'SuccessChance' FACTORY_TUNABLES = {'base_chance': TunablePercent(description='\n The basic chance of success.\n ', default=100), 'multipliers': TunableList(description='\n A list of multipliers to apply to base_chance.\n ', tunable=TunableTuple(multiplier=TunableRange(description='\n The multiplier to apply to base_chance if the associated\n tests pass.\n ', tunable_type=float, default=1, minimum=0), tests=TunableTestSet(description='\n A series of tests that must pass in order for multiplier to\n be applied.\n ')))} def get_chance(self, participant_resolver): chance = self.base_chance for multiplier_data in self.multipliers: while multiplier_data.tests.run_tests(participant_resolver): chance *= multiplier_data.multiplier return min(chance, 1)
class ScheduleUtilityShutOff(GlobalPolicyEffect, ZoneModifierHouseholdShutOffUtility, HasTunableFactory): FACTORY_TUNABLES = { 'chance': OptionalTunable( description= '\n The percent chance that, after an effect is turned on, that utility\n will turn off day-to-day. \n ', tunable=TunablePercent(default=10)), 'schedule_data': WeeklySchedule.TunableFactory( description= '\n The information to schedule points during the week that\n the Global Policy Effect, if enacted, will turn off the tuned\n utility.\n ' ) } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._schedule = None self._stop_schedule_entry = None def turn_on(self, global_policy_id, from_load=False): self._schedule = self.schedule_data( start_callback=self.scheduled_start_action, schedule_immediate=True) if not from_load: services.global_policy_service().add_utility_effect( global_policy_id, self) def turn_off(self, global_policy_id): household_id = services.active_household().id self.stop_action(household_id) if self._schedule is not None: self._schedule.destroy() if self._stop_schedule_entry is not None: alarms.cancel_alarm(self._stop_schedule_entry) services.global_policy_service().remove_utility_effect( global_policy_id) def scheduled_start_action(self, scheduler, alarm_data, extra_data): if self.chance and random.random() < self.chance: return household_id = services.active_household().id self.start_action(household_id) blackout_end_time = alarm_data[1] - alarm_data[0] self._stop_schedule_entry = alarms.add_alarm( self, blackout_end_time, lambda _: self.scheduled_stop_action(household_id), cross_zone=True) def scheduled_stop_action(self, household_id): self.stop_action(household_id)
class OutfitGeneratorRandomizationMixin: INSTANCE_TUNABLES = { 'filter_flag': TunableEnumFlags( description= '\n Define how to handle part randomization for the generated outfit.\n ', enum_type=OutfitFilterFlag, default=OutfitFilterFlag.USE_EXISTING_IF_APPROPRIATE | OutfitFilterFlag.USE_VALID_FOR_LIVE_RANDOM, allow_no_flags=True), 'body_type_chance_overrides': TunableMapping( description= '\n Define body type chance overrides for the generate outfit. For\n example, if BODYTYPE_HAT is mapped to 100%, then the outfit is\n guaranteed to have a hat if any hat matches the specified tags.\n \n If used in an appearance modifier, these body types will contribute\n to the flags that determine which body types can be generated,\n regardless of their percent chance.\n ', key_type=BodyType, value_type=TunablePercent( description= '\n The chance that a part is applied to the corresponding body\n type.\n ', default=100)), 'body_type_match_not_found_policy': TunableMapping( description= '\n The policy we should take for a body type that we fail to find a\n match for. Primary example is to use MATCH_NOT_FOUND_KEEP_EXISTING\n for generating a tshirt and making sure a sim wearing full body has\n a lower body cas part.\n \n If used in an appearance modifier, these body types will contribute\n to the flags that determine which body types can be generated.\n ', key_type=BodyType, value_type=MatchNotFoundPolicy) } FACTORY_TUNABLES = INSTANCE_TUNABLES def get_body_type_flags(self): tuned_flags = 0 for body_type in itertools.chain( self.body_type_chance_overrides.keys(), self.body_type_match_not_found_policy.keys()): tuned_flags |= 1 << body_type return tuned_flags or BodyTypeFlag.CLOTHING_ALL def _generate_outfit(self, sim_info, outfit_category, outfit_index=0, tag_list=(), seed=None): sim_info.generate_outfit( outfit_category, outfit_index=outfit_index, tag_list=tag_list, filter_flag=self.filter_flag, body_type_chance_overrides=self.body_type_chance_overrides, body_type_match_not_found_overrides=self. body_type_match_not_found_policy, seed=seed)
class SetFireState(HasTunableFactory, AutoFactoryInit): FACTORY_TUNABLES = {'chance': TunablePercent(description='\n Chance that the fire will trigger\n ', default=100)} def __init__(self, target, *args, **kwargs): super().__init__(*args, **kwargs) self.target = target def start(self, *_, **__): if random.random() < self.chance: fire_service = services.get_fire_service() fire_service.spawn_fire_at_object(self.target) def stop(self, *_, **__): pass
class SimInfoFireMeter: FIREMETER_FREQUENCY = Tunable( description= "\n The game creates a repeating alarm at this frequency to check whether\n the SimInfoManager has more than the fire meter's threshold before\n triggering a purge of sim_infos.\n\n Please consult a member of performance group before updating this\n tuning.\n ", tunable_type=int, default=120) SIM_INFO_ALLOWED_PERCENTAGE_ABOVE_CAP = TunablePercent( description= "\n The game triggers an out-of-cycle story progression action to control\n population when we cross the current sim info cap plus this percentage.\n For instance, if this percentage is set to 15% and the current Sim Info\n cap is 100 Sim Infos, we'll start culling Sim Infos once we cross 115\n (100 + 15%).\n \n Please consult a member of the performance group before updating this\n tuning.\n ", default=15) def __init__(self): self._alarm_handle = alarms.add_alarm( self, create_time_span(minutes=self.FIREMETER_FREQUENCY), self._firemeter_callback, repeating=True, use_sleep_time=False) def shutdown(self): if self._alarm_handle is not None: alarms.cancel_alarm(self._alarm_handle) self._alarm_handle = None def _firemeter_callback(self, _): sim_info_manager = services.sim_info_manager() sim_info_count = len(sim_info_manager) sim_info_cap = sim_info_manager.SIM_INFO_CAP if sim_info_count <= sim_info_cap: return adjusted_sim_info_cap = int( sim_info_cap * (1 + self.SIM_INFO_ALLOWED_PERCENTAGE_ABOVE_CAP)) if sim_info_count < adjusted_sim_info_cap: return logger.debug( 'FireMeter: We have {} sim_infos in the save file. Current cap: {}. Cap after adjustments: {}. Purge begins.', sim_info_count, sim_info_cap, adjusted_sim_info_cap) self.trigger() def trigger(self): story_progression_service = services.get_story_progression_service() if story_progression_service is None: return for action in story_progression_service.ACTIONS: if isinstance(action, StoryProgressionActionMaxPopulation): action.process_action(StoryProgressionFlags.SIM_INFO_FIREMETER) break
def __init__(self, **kwargs): super().__init__( valid_objects=TunableVariant( description= '\n The items to which the rebate will be applied.\n ', by_tag=TunableSet( description= '\n The rebate will only be applied to objects purchased with the\n tags in this list.\n ', tunable=TunableEnumEntry(tunable_type=tag.Tag, default=tag.Tag.INVALID, invalid_enums=(tag.Tag.INVALID, )) ), locked_args={'all_purchases': None}, default='all_purchases'), rebate_payout_type=TunableVariant( percentage=TunablePercent( description= '\n The percentage of the catalog price that the player will get\n back in the rebate.\n ', default=10), per_item=TunableRange( description= '\n The amount per valid object the player will get back in the\n rebate\n ', tunable_type=int, default=1, minimum=1)), notification_text=TunableLocalizedStringFactory( description= '\n A string representing the line item on the notification\n explaining why Sims with this trait received a rebate.\n \n This string is provided one token: the percentage discount\n obtained due to having this trait.\n \n e.g.:\n {0.Number}% off for purchasing Art and leveraging Critical\n Connections.\n ' ), tests_set=TunableTestSet( description= '\n If these tests pass, then the object is scheduled for the next \n scheduled rebate event.\n ' ), rebate_category=TunableVariant( description= '\n Specify a rebate category for this rebate item.\n \n GAMEPLAY_OBJECT: A GAMEPLAY_OBJECT category rebate item has the option\n of either being a one-time rebate or a cyclical rebate. If tests are\n tuned, the object has two opportunities to get added to rebates\n before the next scheduled rebate event: once on add and its tests\n pass, the next when its tests pass.\n \n BUILD_BUY: A BUILD_BUY category rebate item will give a one-time rebate\n of all the valid objects purchased through build-buy.\n ', buildbuy=RebateCategory( locked_args={ 'rebate_category_enum': RebateCategoryEnum.BUILD_BUY, 'cyclical': False }), gameplay_object=RebateCategory(locked_args={ 'rebate_category_enum': RebateCategoryEnum.GAMEPLAY_OBJECT }), default='buildbuy'))
class BalloonCategory(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager( sims4.resources.Types.BALLOON)): INSTANCE_TUNABLES = { 'balloon_type': TunableEnumEntry( description= '\n The visual style of the balloon background.\n ', tunable_type=BalloonTypeEnum, default=BalloonTypeEnum.THOUGHT), 'balloon_chance': TunablePercent( description= '\n The chance that a balloon from the list is actually shown.\n ', default=100), 'balloons': TunableList( description= '\n The list of possible balloons.\n ', tunable=BalloonVariant.TunableFactory(balloon_type=None)) } @classmethod def get_balloon_icons(cls, resolver, balloon_type=DEFAULT, gsi_category=None, **kwargs): if gsi_category is None: gsi_category = cls.__name__ else: gsi_category = '{}/{}'.format(gsi_category, cls.__name__) possible_balloons = [] if random.random() <= cls.balloon_chance: for balloon in cls.balloons: for balloon_icon in balloon.get_balloon_icons( resolver, balloon_type=cls.balloon_type, gsi_category=gsi_category, **kwargs): if balloon_icon: possible_balloons.append(balloon_icon) return possible_balloons
class SuccessChance(HasTunableSingletonFactory, AutoFactoryInit): ONE = None FACTORY_TUNABLES = { 'base_chance': TunablePercent( description= '\n The basic chance of success.\n ', default=100), 'multipliers': TunableList( description= '\n A list of multipliers to apply to base_chance.\n ', tunable=TunableTuple( multiplier=TunableRange( description= '\n The multiplier to apply to base_chance if the associated\n tests pass.\n ', tunable_type=float, default=1, minimum=0), tests=TunableTestSet( description= '\n A series of tests that must pass in order for multiplier to\n be applied.\n ' ))) } def get_chance(self, participant_resolver): chance = self.base_chance for multiplier_data in self.multipliers: if multiplier_data.tests.run_tests(participant_resolver): chance *= multiplier_data.multiplier return min(chance, 1) def __hash__(self): return hash(self.base_chance) ^ hash(self.multipliers) def __eq__(self, other_chance): if type(self) is not type(other_chance): return False return self.base_chance == other_chance.base_chance and self.multipliers == other_chance.multipliers def __ne__(self, other_chance): return not self.__eq__(other_chance)
class OtherEvaluation(EvaluationBase): FACTORY_TUNABLES = { 'base_chance': TunablePercent( description= '\n The base chance a scholarship is earned.\n ', default=20), 'additive_chance_scores': TunableReference( description= '\n For each passing score, the sum is added onto the base\n scholarship acceptance chance.\n ', manager=services.test_based_score_manager()) } def get_value(self, sim_info): pass def get_score(self, _, resolver, **kwargs): return self.base_chance * 100 + self.additive_chance_scores.get_score( resolver)
class NormalizeStatisticsOp(BaseTargetedLootOperation): __qualname__ = 'NormalizeStatisticsOp' FACTORY_TUNABLES = { 'stats_to_normalize': TunableList( description= '\n Stats to be affected by the normalization.\n ', tunable=TunableReference( services.get_instance_manager(sims4.resources.Types.STATISTIC), class_restrictions=statistics.commodity.Commodity)), 'normalize_percent': TunablePercent( description= '\n In seeking the average value, this is the percent of movement toward the average value \n the stat will move to achieve the new value. For example, if you have a Sim with 50 \n fun, and a Sim with 100 fun, and want to normalize them exactly halfway to their \n average of 75, tune this to 100%. A value of 50% would move one Sim to 67.5 and the other\n to 77.5\n ', default=100, maximum=100, minimum=0) } def __init__(self, stats_to_normalize, normalize_percent, **kwargs): super().__init__(**kwargs) self._stats = stats_to_normalize self._normalize_percent = normalize_percent def _apply_to_subject_and_target(self, subject, target, resolver): for stat_type in self._stats: source_tracker = target.get_tracker(stat_type) if not source_tracker.has_statistic(stat_type): pass target_tracker = subject.get_tracker(stat_type) source_value = source_tracker.get_value(stat_type) target_value = target_tracker.get_value(stat_type) average_value = (source_value + target_value) / 2 source_percent_gain = (source_value - average_value) * self._normalize_percent target_percent_gain = (target_value - average_value) * self._normalize_percent target_tracker.set_value(stat_type, source_value - source_percent_gain) source_tracker.set_value(stat_type, target_value - target_percent_gain)
class _Inheritance(HasTunableSingletonFactory, AutoFactoryInit): __qualname__ = 'GardeningTuning._Inheritance' @staticmethod def _verify_tunable_callback(instance_class, tunable_name, source, value): if not value.inherit_from_mother and not value.inherit_from_father: raise ValueError('Must inherit from at least one parent.') FACTORY_TUNABLES = { 'inherited_state': ObjectState.TunableReference( description= '\n Controls the state value that will be inherited by offspring.\n ' ), 'inherit_from_mother': Tunable( description= "\n If checked, the mother's (root stock's) state value and fitness will\n be considered when deciding what state value the child should\n inherit.\n ", tunable_type=bool, needs_tuning=True, default=True), 'inherit_from_father': Tunable( description= "\n If checked, the father's (a spliced fruit's genes) state value and\n fitness will be considered when deciding what state value the child\n should inherit. In the case a plant is spawning the type of fruit\n it grew from, this will be the same as the mother's contribution.\n ", tunable_type=bool, needs_tuning=True, default=True), 'inheritance_chance': TunablePercent( description= "\n The chance the offspring will inherit this state value from its\n parents at all. If the check doesn't pass, the default value for\n the state will be used.\n ", default=1), 'verify_tunable_callback': _verify_tunable_callback }
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 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 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 RabbitholeGig(Gig): INSTANCE_TUNABLES = { 'negative_mood_tuning': TunableTuple( description= '\n Tuning for the negative mood test. If the Sim has the any of the \n negative mood buffs (the Buff test passes), the failure chance \n tunable will be used to determine whether or not to apply the \n FAILURE outcome.\n ', negative_mood_test=sims.sim_info_tests.BuffTest.TunableFactory(), failure_chance=TunablePercent( description= '\n Chance of a FAILURE outcome if the negative mood test passes.\n ', default=0.0)), 'recommended_skill_tuning': OptionalTunable( description= "\n Tuning for the (optional) recommended skill. If the Sim has this\n skill, the outcome will depend on the Sim's skill level relative \n to the recommended skill level.\n ", tunable=TunableTuple( recommended_skill_test=statistics.skill_tests.SkillRangeTest. TunableFactory( description= '\n The recommended skill test for this gig. For Home \n Assignment gigs, the skill range min and max should be the \n same.\n ' ), great_success_chance_multiplier=Tunable( description= '\n The multiplier for determining the chance the Sim will\n receive the GREAT_SUCCESS outcome.\n ', tunable_type=float, default=0.0), failure_chance_multiplier=Tunable( description= '\n The multiplier for determining the chance the Sim will\n receive the FAILURE outcome.\n ', tunable_type=float, default=0.0), critical_failure_skill_level_delta=Tunable( description= '\n The difference in skill levels lower than the recommended\n skill level for a Sim to qualify for a CRITICAL FAILURE \n outcome.\n ', tunable_type=int, default=0))), 'gig_picker_localization_format': TunableLocalizedStringFactory( description= '\n String used to format the description in the gig picker. Currently\n has tokens for name, payout, gig time, tip title, and tip text.\n ' ) } @classmethod def _verify_tuning_callback(cls): if not cls.tip: logger.error( 'No tip tuned for Rabbithole Gig {}. Rabbithole Gigs must have a tip.', cls) def _determine_gig_outcome(self): if not self.has_attended_gig(): self._gig_result = GigResult.CRITICAL_FAILURE self._send_gig_telemetry(TELEMETRY_GIG_PROGRESS_TIMEOUT) return if self._gig_result == GigResult.CANCELED: self._gig_result = GigResult.FAILURE return self._send_gig_telemetry(TELEMETRY_GIG_PROGRESS_COMPLETE) resolver = self.get_resolver_for_gig() if resolver( self.negative_mood_tuning.negative_mood_test ) and random.random() <= self.negative_mood_tuning.failure_chance: self._gig_result = GigResult.FAILURE return if self.recommended_skill_tuning: skill = self._owner.get_statistic( self.recommended_skill_tuning.recommended_skill_test.skill, add=False) sim_skill_level = 0 if skill: sim_skill_level = skill.get_user_value() recommended_level = self.recommended_skill_tuning.recommended_skill_test.skill_range_max if sim_skill_level > recommended_level: chance = ( sim_skill_level - recommended_level ) * self.recommended_skill_tuning.great_success_chance_multiplier if random.random() <= chance: self._gig_result = GigResult.GREAT_SUCCESS else: self._gig_result = GigResult.SUCCESS elif sim_skill_level == recommended_level: self._gig_result = GigResult.SUCCESS else: skill_level_difference = recommended_level - sim_skill_level if skill_level_difference >= self.recommended_skill_tuning.critical_failure_skill_level_delta: self._gig_result = GigResult.CRITICAL_FAILURE else: chance = skill_level_difference * self.recommended_skill_tuning.failure_chance_multiplier if random.random() <= chance: self._gig_result = GigResult.FAILURE else: self._gig_result = GigResult.CRITICAL_FAILURE else: self._gig_result = GigResult.SUCCESS @classmethod def create_picker_row(cls, description=None, scheduled_time=None, owner=None, gig_customer=None, enabled=True, **kwargs): tip = cls.tip duration = TimeSpan.ONE finishing_time = None if scheduled_time is None: logger.error('Rabbit Hole Gig {} : Not a valid scheduled_time.', cls) return for (start_time, end_time) in cls.gig_time().get_schedule_entries(): if scheduled_time.day() == start_time.day(): if scheduled_time.hour() == start_time.hour(): if scheduled_time.minute() == start_time.minute(): duration = end_time - start_time finishing_time = scheduled_time + duration break if finishing_time == None: logger.error( 'Rabbit Hole Gig {} : No gig start_time found for scheduled_time {} ', cls, scheduled_time) return pay_rate = cls.gig_pay.lower_bound / duration.in_hours() description = cls.gig_picker_localization_format( cls.gig_pay.lower_bound, pay_rate, scheduled_time, finishing_time, tip.tip_title(), tip.tip_text(), gig_customer) if not enabled and cls.disabled_tooltip is not None: row_tooltip = lambda *_: cls.disabled_tooltip(owner) elif cls.display_description is None: row_tooltip = None else: row_tooltip = lambda *_: cls.display_description(owner) customer_description = cls.odd_job_tuning.customer_description( gig_customer) row = OddJobPickerRow(customer_id=gig_customer.id, customer_description=customer_description, tip_title=tip.tip_title(), tip_text=tip.tip_text(), tip_icon=tip.tip_icon, name=cls.display_name(owner), icon=cls.display_icon, row_description=description, row_tooltip=row_tooltip, is_enable=enabled) return row
def __init__(self, *args, **kwargs): super().__init__(balloon_target=TunableEnumFlags(ParticipantType, ParticipantType.Invalid, description='\n Who to play balloons over relative to the interaction. \n Generally, balloon tuning will use either balloon_animation_target \n or balloon_target.'), balloon_choices=TunableList(description='\n A list of the balloons and balloon categories\n ', tunable=BalloonVariant.TunableFactory()), balloon_delay=Tunable(float, None, description='\n If set, the number of seconds after the start of the animation to \n trigger the balloon. A negative number will count backwards from the \n end of the animation.'), balloon_delay_random_offset=TunableRange(float, 0, minimum=0, description='\n The amount of randomization that is added to balloon requests. \n Will always offset the delay time later, and requires the delay \n time to be set to a number. A value of 0 has no randomization.'), balloon_chance=TunablePercent(100, description='\n The chance that the balloon will play.'), **kwargs)
class ClientObjectMixin: INITIAL_DEPRECIATION = TunablePercent( 20, description= 'Amount (0%%-100%%) of depreciation to apply to an object after purchase. An item worth 10 in the catalog if tuned at 20%% will be worth 8 after purchase.' ) FADE_DURATION = TunableSimMinute( 1.2, description='Default fade time (in sim minutes) for objects.') VISIBLE_TO_AUTOMATION = True _get_next_ui_metadata_handle = uid.UniqueIdGenerator(min_uid=1) HOVERTIP_HANDLE = 0 ui_metadata = distributor.sparse.SparseField( ui_protocols.UiObjectMetadata, distributor.ops.SetUiObjectMetadata) _generic_ui_metadata_setters = {} FORWARD_OFFSET = 0.04 def __init__(self, definition, **kwargs): super().__init__(definition, **kwargs) if definition is not None: self.apply_definition(definition, **kwargs) self._ui_metadata_stack = None self._ui_metadata_handles = None self._ui_metadata_cache = None self.primitives = distributor.ops.DistributionSet(self) zone_id = services.current_zone_id() self._location = sims4.math.Location( sims4.math.Transform(), routing.SurfaceIdentifier(zone_id, 0, routing.SurfaceType.SURFACETYPE_WORLD)) self._children_objects = None self._scale = 1 self._parent_type = ObjectParentType.PARENT_NONE self._parent_location = 0 self._build_buy_lockout = False self._build_buy_lockout_alarm_handler = None self._tint = None self._opacity = None self._censor_state = None self._geometry_state = None self._geometry_state_overrides = None self._standin_model = None self._visibility = None self._visibility_flags = None self._material_state = None self._reference_arb = None self._audio_effects = None self._video_playlist = None self._painting_state = None self.custom_name = None self.custom_description = None self._multicolor = None self._display_number = None self._awareness_scores = None self._scratched = False self._base_value = definition.price self._needs_post_bb_fixup = False self._needs_depreciation = False self._swapping_to_parent = None self._swapping_from_parent = None self._on_children_changed = None self.allow_opacity_change = True self._wind_speed_effect = None def get_create_op(self, *args, **kwargs): additional_ops = list(self.get_additional_create_ops_gen()) return distributor.ops.ObjectCreate(self, *args, additional_ops=additional_ops, **kwargs) @forward_to_components_gen def get_additional_create_ops_gen(self): pass def get_create_after_objs(self): parent = self.parent_object(child_type=ChildrenType.BB_ONLY) if parent is not None: return (parent, ) return () def get_delete_op(self, fade_duration=0): return distributor.ops.ObjectDelete(fade_duration=fade_duration) @forward_to_components def apply_definition(self, definition, obj_state=0): if not isinstance(definition, objects.definition.Definition): definition = services.definition_manager().get(definition) self._model = definition.get_model(obj_state) self._material_variant = definition.material_variant self._rig = definition.get_rig(obj_state) self._slot = definition.get_slot(obj_state) self._slots_resource = definition.get_slots_resource(obj_state) self._state_index = obj_state def set_definition(self, definition_id, ignore_rig_footprint=False): new_definition = services.definition_manager().get(definition_id) (result, error) = self.definition.is_similar( new_definition, ignore_rig_footprint=ignore_rig_footprint) if not result: logger.error( 'Trying to set the definition {} to an incompatible definition {}.\n {}', self.definition.id, definition_id, error, owner='nbaker') return False services.definition_manager().unregister_definition( self.definition.id, self) self.apply_definition(new_definition, self._state_index) self.definition = new_definition services.definition_manager().register_definition( new_definition.id, self) self.resend_model_with_material_variant() self.resend_slot() self.resend_state_index() op = distributor.ops.SetObjectDefinitionId(definition_id) distributor.system.Distributor.instance().add_op(self, op) return True @property def hover_tip(self): if self._ui_metadata_stack is None or ClientObjectMixin.HOVERTIP_HANDLE not in self._ui_metadata_handles: return (_, _, value) = self._ui_metadata_handles[ClientObjectMixin.HOVERTIP_HANDLE] return value @hover_tip.setter def hover_tip(self, value): if value is not None: if self._ui_metadata_stack is None: self._ui_metadata_stack = [] self._ui_metadata_handles = {} self._ui_metadata_cache = {} data = self._ui_metadata_handles.get( ClientObjectMixin.HOVERTIP_HANDLE) if data is not None: self._ui_metadata_stack.remove(data) data = (ClientObjectMixin.HOVERTIP_HANDLE, 'hover_tip', value) self._ui_metadata_stack.append(data) self._ui_metadata_handles[ClientObjectMixin.HOVERTIP_HANDLE] = data def add_ui_metadata(self, name, value): if self._ui_metadata_stack is None: self._ui_metadata_stack = [] self._ui_metadata_handles = {} self._ui_metadata_cache = {} if name not in self._ui_metadata_cache: default_value = type(self).ui_metadata.generic_getter(name)(self) self._ui_metadata_cache[name] = default_value handle = self._get_next_ui_metadata_handle() data = (handle, name, value) self._ui_metadata_stack.append(data) self._ui_metadata_handles[handle] = data return handle def get_ui_metadata(self, handle): return self._ui_metadata_handles[handle] def remove_ui_metadata(self, handle): if self._ui_metadata_stack is not None: self._ui_metadata_stack.remove(self._ui_metadata_handles[handle]) def update_ui_metadata(self, use_cache=True): if self._ui_metadata_stack is None: return ui_metadata = {} for (_, name, value) in self._ui_metadata_stack: ui_metadata[name] = value for (name, value) in ui_metadata.items(): if name in self._ui_metadata_cache and self._ui_metadata_cache[ name] == value and use_cache: continue if name in self._generic_ui_metadata_setters: setter = self._generic_ui_metadata_setters[name] else: setter = type(self).ui_metadata.generic_setter(name, auto_reset=True) self._generic_ui_metadata_setters[name] = setter try: setter(self, value) except (ValueError, TypeError): logger.error( 'Error trying to set field {} to value {} in object {}.', name, value, self, owner='camilogarcia') for name in self._ui_metadata_cache.keys() - ui_metadata.keys(): try: if name in self._generic_ui_metadata_setters: self._generic_ui_metadata_setters[name](self, None) except (ValueError, TypeError): logger.error( 'Error trying to set field {} to default in object {}.', name, self, owner='nabaker') self._ui_metadata_cache = ui_metadata @property def swapping_to_parent(self): return self._swapping_to_parent @property def swapping_from_parent(self): return self._swapping_from_parent @contextmanager def _swapping_parents(self, old_parent, new_parent): self._swapping_from_parent = old_parent self._swapping_to_parent = new_parent try: yield None finally: self._swapping_from_parent = None self._swapping_to_parent = None @property def location(self): return self._location @distributor.fields.Field(op=distributor.ops.SetLocation) def _location_field_internal(self): return self resend_location = _location_field_internal.get_resend() @location.setter def location(self, new_location): self.set_location_without_distribution(new_location) self.resend_location() def set_location_without_distribution(self, new_location): if not isinstance(new_location, sims4.math.Location): raise TypeError() if not (new_location == self._location and (self.parts is not None and new_location.parent is not None) and new_location.parent.parts is not None): return old_location = self._location events = [(self, old_location)] for child in self.get_all_children_recursive_gen(): events.append((child, child._location)) if new_location.parent != old_location.parent: self.pre_parent_change(new_location.parent) with self._swapping_parents(old_location.parent, new_location.parent): if old_location.parent is not None: old_location.parent._remove_child( self, new_parent=new_location.parent) if new_location.parent is not None: new_location.parent._add_child(self, new_location) visibility_state = self.visibility or VisibilityState() if new_location.parent is not None and new_location.parent._disable_child_footprint_and_shadow: visibility_state.enable_drop_shadow = False else: visibility_state.enable_drop_shadow = True self.visibility = visibility_state if new_location.parent is not None: current_inventory = self.get_inventory() if current_inventory is not None and not current_inventory.try_remove_object_by_id( self.id): raise RuntimeError( 'Unable to remove object: {} from the inventory: {}, parenting request will be ignored.' .format(self, current_inventory)) posture_graph_service = services.current_zone().posture_graph_service with posture_graph_service.object_moving(self): self._location = new_location if self.parts: for part in self.parts: part.on_owner_location_changed() if new_location.parent != old_location.parent: self.on_parent_change(new_location.parent) for (obj, old_value) in events: if obj is not self: new_location = obj.location.clone() obj._location = new_location obj.on_location_changed(old_value) def set_location(self, location): self.location = location def move_to(self, **overrides): self.location = self._location.clone(**overrides) @distributor.fields.Field(op=distributor.ops.SetAudioEffects) def audio_effects(self): return self._audio_effects resend_audio_effects = audio_effects.get_resend() def append_audio_effect(self, key, audio_effect_data): if self._audio_effects is None: self._audio_effects = {} self._audio_effects[key] = audio_effect_data self.resend_audio_effects() def remove_audio_effect(self, key): if self._audio_effects is None: logger.error( 'Found audio effects is None while trying to remove audio effect with key {} on {}', key, self, owner='jdimailig') return if key in self._audio_effects: del self._audio_effects[key] if not self._audio_effects: self._audio_effects = None self.resend_audio_effects() @forward_to_components def on_location_changed(self, old_location): pass @property def transform(self): return self._location.world_transform @transform.setter def transform(self, transform): if self.parent is not None: self.move_to(transform=transform, parent=None, routing_surface=self.parent.routing_surface) return self.move_to(transform=transform) @property def position(self): return self.transform.translation @property def position_with_forward_offset(self): return self.position + self.forward * ClientObjectMixin.FORWARD_OFFSET @property def intended_position_with_forward_offset(self): return self.intended_position + self.intended_forward * ClientObjectMixin.FORWARD_OFFSET @property def orientation(self): return self.transform.orientation @property def forward(self): return self.orientation.transform_vector( self.forward_direction_for_picking) @property def routing_surface(self): return self._location.world_routing_surface @property def level(self): routing_surface = self.routing_surface if routing_surface is None: return return routing_surface.secondary_id @property def routing_location(self): return self.get_routing_location_for_transform(self.transform) def get_routing_location_for_transform(self, transform, routing_surface=DEFAULT): routing_surface = self.routing_surface if routing_surface is DEFAULT else routing_surface return routing.Location(transform.translation, transform.orientation, routing_surface) @property def intended_transform(self): return self.transform @property def intended_position(self): return self.intended_transform.translation @property def intended_forward(self): return self.intended_transform.orientation.transform_vector( self.forward_direction_for_picking) @property def intended_routing_surface(self): return self.routing_surface @property def parent(self): parent = self._location.parent parent = self.attempt_to_remap_parent(parent) if parent is not None: children = parent.children if not (self not in children and self.is_part and self.part_owner in children): return return parent @property def bb_parent(self): return self._location.parent def attempt_to_remap_parent(self, parent): if self.is_part and parent is not None and parent.parts is not None: distance = None found_part = None for part in parent.parts: dot = sims4.math.vector_dot(self.forward, part.forward) if dot < -0.98: new_distance = (part.position - self.position).magnitude_squared() if not distance is None: if new_distance <= distance: if new_distance == distance and found_part.subroot_index is not None: continue distance = new_distance found_part = part if new_distance == distance and found_part.subroot_index is not None: continue distance = new_distance found_part = part return found_part return parent def parent_object(self, child_type=ChildrenType.DEFAULT): if child_type is ChildrenType.BB_ONLY: parent = self.bb_parent else: parent = self.parent if parent is not None: if parent.is_part: parent = parent.part_owner return parent @property def parent_slot(self): parent = self._location.parent if parent is None: return bone_name_hash = self._location.joint_name_or_hash or self._location.slot_hash result = None for runtime_slot in parent.get_runtime_slots_gen( bone_name_hash=bone_name_hash): assert not result is not None result = runtime_slot if result is None: result = RuntimeSlot(parent, bone_name_hash, frozenset()) return result def get_parenting_root(self): result = self next_parent = result.parent while next_parent is not None: result = next_parent next_parent = result.parent return result @property def children(self): if self._children_objects is not None: return self._children_objects[ChildrenType.DEFAULT] return () def children_recursive_gen(self, include_self=False): if include_self: yield self for child in self.children: yield child for grandchild in child.children_recursive_gen(): yield grandchild @assertions.hot_path def _children_recursive_fast_gen(self): yield self for child in self.children: yield from child._children_recursive_fast_gen() def get_all_children_gen(self): if self._children_objects is not None: for children in self._children_objects.values(): yield from children def get_all_children_recursive_gen(self): for child in self.get_all_children_gen(): yield child yield from child.get_all_children_recursive_gen() def clear_default_children(self): if self._children_objects is not None: self._children_objects[ChildrenType.DEFAULT].clear() @assertions.hot_path def parenting_hierarchy_gen(self): self_parent = self.parent if self_parent is not None: master_parent = self_parent master_parent_parent = master_parent.parent while master_parent_parent is not None: master_parent = master_parent_parent master_parent_parent = master_parent.parent yield from master_parent._children_recursive_fast_gen() else: yield from self._children_recursive_fast_gen() def on_reset_send_op(self, reset_reason): super().on_reset_send_op(reset_reason) if self.valid_for_distribution: if reset_reason != ResetReason.BEING_DESTROYED or self.vehicle_component is not None: try: reset_op = distributor.ops.ResetObject(self.id) dist = Distributor.instance() dist.add_op(self, reset_op) except: logger.exception( 'Exception thrown sending reset op for {}', self) def on_reset_internal_state(self, reset_reason): if self.valid_for_distribution and reset_reason != ResetReason.BEING_DESTROYED: self.geometry_state = None self.material_state = None self.resend_location() self._reset_reference_arb() super().on_reset_internal_state(reset_reason) def on_reset_get_interdependent_reset_records(self, reset_reason, reset_records): super().on_reset_get_interdependent_reset_records( reset_reason, reset_records) for child in set(self.get_all_children_gen()): reset_records.append( ResetRecord(child, ResetReason.RESET_EXPECTED, self, 'Child')) @property def slot_hash(self): return self._location.slot_hash @slot_hash.setter def slot_hash(self, value): if self._location.slot_hash != value: self.location = self._location.clone(slot_hash=value) @property def bone_name_hash(self): return self._location.joint_name_or_hash or self._location.slot_hash @property def part_suffix(self) -> str: pass @distributor.fields.Field(op=distributor.ops.SetModel) def model_with_material_variant(self): return (self._model, self._material_variant) resend_model_with_material_variant = model_with_material_variant.get_resend( ) @model_with_material_variant.setter def model_with_material_variant(self, value): (self._model, self._material_variant) = value @property def model(self): return self._model @model.setter def model(self, value): model_res_key = None if isinstance(value, sims4.resources.Key): model_res_key = value elif isinstance(value, Definition): model_res_key = value.get_model(index=0) self.set_definition(value.id, ignore_rig_footprint=True) else: if value is not None: logger.error( 'Trying to set the model of object {} to the invalid value of {}. The object will revert to its default model instead.', self, value, owner='tastle') model_res_key = self.definition.get_model(self._state_index) self.model_with_material_variant = (model_res_key, self._material_variant) @property def material_variant(self): return self._material_variant @material_variant.setter def material_variant(self, value): if value is None: self.model_with_material_variant = (self._model, None) else: if not isinstance(value, str): raise TypeError('Model variant value must be a string') if not value: self.model_with_material_variant = (self._model, None) else: try: variant_value = int(value) except ValueError: variant_value = sims4.hash_util.hash32(value) self.model_with_material_variant = (self._model, variant_value) @distributor.fields.Field(op=distributor.ops.SetStandInModel) def standin_model(self): return self._standin_model @standin_model.setter def standin_model(self, value): self._standin_model = value @distributor.fields.Field(op=distributor.ops.SetObjectDefStateIndex, default=0) def state_index(self): return self._state_index resend_state_index = state_index.get_resend() @distributor.fields.Field(op=distributor.ops.SetRig, priority=distributor.fields.Field.Priority.HIGH) def rig(self): return self._rig @rig.setter def rig(self, value): if not isinstance(value, sims4.resources.Key): raise TypeError self._rig = value @distributor.fields.Field(op=distributor.ops.SetSlot) def slot(self): return self._slot resend_slot = slot.get_resend() @property def slots_resource(self): return self._slots_resource @distributor.fields.Field(op=distributor.ops.SetScale, default=1) def _client_scale(self): scale_value = self.scale for modifier in self.scale_modifiers_gen(): scale_value *= modifier return scale_value _resend_client_scale = _client_scale.get_resend() @property def scale(self): return self._scale @forward_to_components_gen def scale_modifiers_gen(self): pass @scale.setter def scale(self, value): if self._scale != value: self._scale = value self.on_location_changed(self._location) self._resend_client_scale() @property def parent_type(self): return self._parent_type @parent_type.setter def parent_type(self, value): self._parent_type = value self._resend_parent_type_info() @distributor.fields.Field(op=distributor.ops.SetParentType, default=None) def parent_type_info(self): return (self._parent_type, self._parent_location) @parent_type_info.setter def parent_type_info(self, value): (self._parent_type, self._parent_location) = value _resend_parent_type_info = parent_type_info.get_resend() @property def build_buy_lockout(self): return self._build_buy_lockout @distributor.fields.Field(op=distributor.ops.SetTint, default=None) def tint(self): if self.build_buy_lockout and lockout_visualization: return sims4.color.ColorARGB32(23782) return self._tint @tint.setter def tint(self, tint_color): value = getattr(tint_color, 'value', tint_color) if value and not isinstance(value, sims4.color.ColorARGB32): raise TypeError('Tint value must be a Color') if value == sims4.color.Color.WHITE: self._tint = None else: self._tint = value resend_tint = tint.get_resend() @distributor.fields.Field(op=distributor.ops.SetMulticolor, default=None) def multicolor(self): return self._multicolor @multicolor.setter def multicolor(self, value): self._multicolor = value resend_multicolor = multicolor.get_resend() @distributor.fields.Field(op=distributor.ops.SetDisplayNumber, default=None) def display_number(self): return self._display_number @display_number.setter def display_number(self, value): self._display_number = value resend_display_number = display_number.get_resend() def update_display_number(self, display_number=None): if display_number is not None: self.display_number = display_number return if hasattr(self, 'get_display_number'): self.display_number = self.get_display_number() @distributor.fields.Field(op=distributor.ops.SetOpacity, default=None) def opacity(self): return self._opacity @opacity.setter def opacity(self, value): if self.allow_opacity_change: self._opacity = self._clamp_opacity(value) def _clamp_opacity(self, value): if value is None: return try: value = float(value) except: raise TypeError('Opacity value must be a float') return sims4.math.clamp(0.0, value, 1.0) @distributor.fields.Field(op=SetAwarenessSourceOp) def awareness_scores(self): return self._awareness_scores resend_awareness_scores = awareness_scores.get_resend() def add_awareness_scores(self, awareness_sources): if self._awareness_scores is None: self._awareness_scores = Counter() self._awareness_scores.update(awareness_sources) self.resend_awareness_scores() def remove_awareness_scores(self, awareness_sources): if self._awareness_scores is None: return self._awareness_scores.subtract(awareness_sources) for awareness_channel in tuple(self.awareness_scores): if not self._awareness_scores[awareness_channel]: del self._awareness_scores[awareness_channel] if not self._awareness_scores: self._awareness_scores = None self.resend_awareness_scores() def add_geometry_state_override(self, original_geometry_state, override_geometry_state): if self._geometry_state_overrides is None: self._geometry_state_overrides = {} original_state_hash = sims4.hash_util.hash32(original_geometry_state) override_state_hash = sims4.hash_util.hash32(override_geometry_state) logger.assert_raise( original_state_hash not in self._geometry_state_overrides, 'add_geometry_state_override does not support multiple overrides per state' ) self._geometry_state_overrides[ original_state_hash] = override_state_hash self.geometry_state = self.geometry_state def remove_geometry_state_override(self, original_geometry_state): state_hash = sims4.hash_util.hash32(original_geometry_state) if state_hash in self._geometry_state_overrides: del self._geometry_state_overrides[state_hash] if not self._geometry_state_overrides: self._geometry_state_overrides = None @distributor.fields.Field(op=distributor.ops.SetGeometryState, default=None) def geometry_state(self): return self._geometry_state @geometry_state.setter def geometry_state(self, value): self._geometry_state = self._get_geometry_state_for_value(value) def _get_geometry_state_for_value(self, value): if not value: return if isinstance(value, str): state_hash = sims4.hash_util.hash32(value) elif isinstance(value, int): state_hash = value if self._geometry_state_overrides is not None: if state_hash in self._geometry_state_overrides: state_hash = self._geometry_state_overrides[state_hash] return state_hash @distributor.fields.Field(op=distributor.ops.SetCensorState, default=None) def censor_state(self): return self._censor_state @censor_state.setter def censor_state(self, value): try: value = CensorState(value) except: raise TypeError('Censor State value must be an int') self._censor_state = value @distributor.fields.Field(op=distributor.ops.SetVisibility, default=None) def visibility(self): return self._visibility @visibility.setter def visibility(self, value): if not isinstance(value, VisibilityState): raise TypeError( 'Visibility must be set to value of type VisibilityState') self._visibility = value if value is not None: if value.visibility is True: if value.inherits is False: if value.enable_drop_shadow is False: self._visibility = None @distributor.fields.Field(op=distributor.ops.SetVisibilityFlags) def visibility_flags(self): return self._visibility_flags @visibility_flags.setter def visibility_flags(self, value): self._visibility_flags = value @distributor.fields.Field(op=distributor.ops.SetMaterialState, default=None) def material_state(self): return self._material_state @material_state.setter def material_state(self, value): if value is None: self._material_state = None else: if not isinstance(value, MaterialState): raise TypeError( 'Material State must be set to value of type MaterialState' ) if value.state_name_hash == 0: self._material_state = None else: self._material_state = value @property def material_hash(self): if self.material_state is None: return 0 else: return self.material_state.state_name_hash @distributor.fields.Field(op=distributor.ops.StartArb, default=None) def reference_arb(self): return self._reference_arb def update_reference_arb(self, arb): if self._reference_arb is None: self._reference_arb = animation.arb.Arb() native.animation.update_post_condition_arb(self._reference_arb, arb) def _reset_reference_arb(self): if self._reference_arb is not None: reset_arb_element = ArbElement(animation.arb.Arb()) reset_arb_element.add_object_to_reset(self) reset_arb_element.distribute() reset_arb_element.cleanup() self._reference_arb = None _NO_SLOTS = EMPTY_SET @property def deco_slot_size(self): return get_object_decosize(self.definition.id) @property def deco_slot_types(self): return DecorativeSlotTuning.get_slot_types_for_object( self.deco_slot_size) @property def slot_type_set(self): key = get_object_slotset(self.definition.id) return get_slot_type_set_from_key(key) @property def slot_types(self): slot_type_set = self.slot_type_set if slot_type_set is not None: return slot_type_set.slot_types return self._NO_SLOTS @property def ideal_slot_types(self): carryable = self.get_component(CARRYABLE_COMPONENT) if carryable is not None: slot_type_set = carryable.ideal_slot_type_set if slot_type_set is not None: return slot_type_set.slot_types & (self.slot_types | self.deco_slot_types) return self._NO_SLOTS @property def all_valid_slot_types(self): return self.deco_slot_types | self.slot_types def _add_child(self, child, location): if self._children_objects is None: self._children_objects = defaultdict(WeakSet) if not isinstance(self.children, (WeakSet, set)): raise TypeError( "self.children is not a WeakSet or a set, it's {}".format( self.children)) bone_name_hash = location.joint_name_or_hash or location.slot_hash found_runtime_slot = None for runtime_slot in location.parent.get_runtime_slots_gen( bone_name_hash=bone_name_hash): assert not found_runtime_slot is not None found_runtime_slot = runtime_slot if found_runtime_slot is not None: for slot_type in found_runtime_slot.slot_types: if not slot_type.bb_only: self._children_objects[ChildrenType.DEFAULT].add(child) break else: self._children_objects[ChildrenType.BB_ONLY].add(child) else: self._children_objects[ChildrenType.DEFAULT].add(child) if self.parts: for part in self.parts: part.on_children_changed() self.on_child_added(child, location) def _remove_child(self, child, new_parent=None): if not isinstance(self.children, (WeakSet, set)): raise TypeError( "self.children is not a WeakSet or a set, it's {}".format( self.children)) for (_, weak_obj_set) in self._children_objects.items(): if child in weak_obj_set: weak_obj_set.discard(child) break if self.parts: for part in self.parts: part.on_children_changed() self.on_child_removed(child, new_parent=new_parent) @forward_to_components def on_remove_from_client(self): super().on_remove_from_client() for primitive in tuple(self.primitives): primitive.detach(self) def post_remove(self): super().post_remove() for primitive in tuple(self.primitives): primitive.detach(self) self.primitives = None @forward_to_components def on_child_added(self, child, location): if self._on_children_changed is None: return self._on_children_changed(child, location=location) @forward_to_components def on_child_removed(self, child, new_parent=None): if self._on_children_changed is None: return self._on_children_changed(child, new_parent=new_parent) @forward_to_components def pre_parent_change(self, parent): pass @forward_to_components def on_parent_change(self, parent): caches.clear_all_caches() if parent is None: self.parent_type = ObjectParentType.PARENT_NONE else: self.parent_type = ObjectParentType.PARENT_OBJECT def create_parent_location(self, parent, transform=sims4.math.Transform.IDENTITY(), joint_name_or_hash=None, slot_hash=0, routing_surface=None): if parent is not None: if self in parent.ancestry_gen(): raise ValueError( 'Invalid parent value (parent chain is circular)') if joint_name_or_hash: native.animation.get_joint_transform_from_rig( parent.rig, joint_name_or_hash) if slot_hash and not parent.has_slot(slot_hash): raise KeyError( 'Could not slot {}/{} in slot {} on {}/{}'.format( self, self.definition, hex(slot_hash), parent, parent.definition)) part_joint_name = joint_name_or_hash or slot_hash if parent is not None: if part_joint_name is not None: if not parent.is_part: if parent.parts: for part in parent.parts: if part.has_slot(part_joint_name): parent = part break new_location = self._location.clone( transform=transform, joint_name_or_hash=joint_name_or_hash, slot_hash=slot_hash, parent=parent, routing_surface=routing_surface) return new_location def set_parent(self, *args, **kwargs): new_location = self.create_parent_location(*args, **kwargs) self.location = new_location def clear_parent(self, transform, routing_surface): return self.set_parent(None, transform=transform, routing_surface=routing_surface) def remove_reference_from_parent(self): parent = self.bb_parent if parent is not None: parent._remove_child(self, new_parent=UNSET) @distributor.fields.Field(op=distributor.ops.VideoSetPlaylistOp, default=None) def video_playlist(self): return self._video_playlist @video_playlist.setter def video_playlist(self, playlist): self._video_playlist = playlist _resend_video_playlist = video_playlist.get_resend() def fade_opacity(self, opacity: float, duration: float, immediate=False, additional_channels=None): if self.allow_opacity_change: opacity = self._clamp_opacity(opacity) if opacity != self._opacity: self._opacity = opacity fade_op = distributor.ops.FadeOpacity(opacity, duration, immediate=immediate) if additional_channels: for channel in additional_channels: fade_op.add_additional_channel(*channel) distributor.ops.record(self, fade_op) def fade_in(self, fade_duration=None, immediate=False, additional_channels=None): if self.allow_opacity_change: if fade_duration is None: fade_duration = ClientObjectMixin.FADE_DURATION if self.visibility is not None: if not self.visibility.visibility: self.visibility = VisibilityState() self.opacity = 0 self.fade_opacity(1, fade_duration, immediate=immediate, additional_channels=additional_channels) def fade_out(self, fade_duration=None, immediate=False, additional_channels=None): if self.allow_opacity_change: if fade_duration is None: fade_duration = ClientObjectMixin.FADE_DURATION self.fade_opacity(0, fade_duration, immediate=immediate, additional_channels=additional_channels) @distributor.fields.Field(op=distributor.ops.SetValue, default=None) def current_value(self): new_value = self._base_value statistic_component = self.statistic_component if statistic_component is not None: new_value += statistic_component.get_added_monetary_value() state_component = self.state_component if state_component is not None: return max( round(new_value * state_component.state_based_value_mod), 0) return max(new_value, 0) @current_value.setter def current_value(self, value): state_component = self.state_component if state_component is not None: self.base_value = value / state_component.state_based_value_mod else: self.base_value = value _resend_current_value = current_value.get_resend() def update_current_value(self, update_tooltip=True): self._resend_current_value() if update_tooltip: self.update_tooltip_field(TooltipFieldsComplete.simoleon_value, self.current_value) @property def base_value(self): return self._base_value @base_value.setter def base_value(self, value): self._base_value = round(max(value, 0)) update_tooltip = self.get_tooltip_field( TooltipFieldsComplete.simoleon_value) is not None self.update_current_value(update_tooltip=update_tooltip) @property def depreciated_value(self): if not self.definition.get_can_depreciate(): return self.catalog_value return self.catalog_value * (1 - self.INITIAL_DEPRECIATION) @property def catalog_value(self): return self.get_object_property(GameObjectProperty.CATALOG_PRICE) @property def depreciated(self): return not self._needs_depreciation def set_post_bb_fixup_needed(self): self._needs_post_bb_fixup = True self._needs_depreciation = True def try_post_bb_fixup(self, force_fixup=False, active_household_id=0): if force_fixup or self._needs_depreciation: if force_fixup: self._needs_depreciation = True self._on_try_depreciation(active_household_id=active_household_id) if force_fixup or self._needs_post_bb_fixup: self._needs_post_bb_fixup = False self.on_post_bb_fixup() @forward_to_components def on_post_bb_fixup(self): services.get_event_manager().process_events_for_household( test_events.TestEvent.ObjectAdd, services.household_manager().get(self._household_owner_id), obj=self) def _on_try_depreciation(self, active_household_id=0): if self._household_owner_id != active_household_id: return self._needs_depreciation = False if not self.definition.get_can_depreciate(): return self.base_value = floor(self._base_value * (1 - self.INITIAL_DEPRECIATION)) def register_for_on_children_changed_callback(self, callback): if self._on_children_changed is None: self._on_children_changed = CallableList() if callback not in self._on_children_changed: self._on_children_changed.append(callback) def unregister_for_on_children_changed_callback(self, callback): if self._on_children_changed is not None: if callback in self._on_children_changed: self._on_children_changed.remove(callback) if not self._on_children_changed: self._on_children_changed = None @distributor.fields.Field(op=distributor.ops.SetScratched, default=False) def scratched(self): return self._scratched @scratched.setter def scratched(self, scratched): self._scratched = scratched @distributor.fields.Field(op=distributor.ops.SetWindSpeedEffect, default=None) def wind_speed_level(self): return self._wind_speed_effect @wind_speed_level.setter def wind_speed_level(self, value): self._wind_speed_effect = value.wind_speed
class LaundryTuning: GENERATE_CLOTHING_PILE = TunableTuple(description='\n The tunable to generate clothing pile on the lot. This will be called\n when we find laundry hero objects on the lot and there is no hamper\n available.\n ', loot_to_apply=TunableReference(description='\n Loot to apply for generating clothing pile.\n ', manager=services.get_instance_manager(sims4.resources.Types.ACTION), class_restrictions=('LootActions',), pack_safe=True), naked_outfit_category=TunableSet(description="\n Set of outfits categories which is considered naked.\n When Sim switches FROM these outfits, it won't generate the pile.\n When Sim switches TO these outfits, it won't apply laundry reward\n or punishment.\n ", tunable=TunableEnumEntry(tunable_type=OutfitCategory, default=OutfitCategory.EVERYDAY, invalid_enums=(OutfitCategory.CURRENT_OUTFIT,))), no_pile_outfit_category=TunableSet(description="\n Set of outfits categories which will never generate the pile.\n When Sim switches FROM or TO these outfits, it won't generate the\n pile.\n \n Laundry reward or punishment will still be applied to the Sim when \n switching FROM or TO these outfits.\n ", tunable=TunableEnumEntry(tunable_type=OutfitCategory, default=OutfitCategory.EVERYDAY, invalid_enums=(OutfitCategory.CURRENT_OUTFIT,))), no_pile_interaction_tag=TunableEnumWithFilter(description='\n If interaction does spin clothing change and has this tag, it will\n generate no clothing pile.\n ', tunable_type=Tag, default=Tag.INVALID, filter_prefixes=('interaction',))) HAMPER_OBJECT_TAGS = TunableTags(description='\n Tags that considered hamper objects.\n ', filter_prefixes=('func',)) LAUNDRY_HERO_OBJECT_TAGS = TunableTags(description='\n Tags of laundry hero objects. Placing any of these objects on the lot\n will cause the service to generate clothing pile for each Sims on the\n household after spin clothing change.\n ', filter_prefixes=('func',)) NOT_DOING_LAUNDRY_PUNISHMENT = TunableTuple(description='\n If no Sim in the household unload completed laundry in specific\n amount of time, the negative loot will be applied to Sim household \n on spin clothing change to engage them doing laundry.\n ', timeout=TunableSimMinute(description="\n The amount of time in Sim minutes, since the last time they're \n finishing laundry, before applying the loot.\n ", default=2880, minimum=1), loot_to_apply=TunableReference(description='\n Loot defined here will be applied to the Sim in the household\n on spin clothing change if they are not doing laundry for \n a while.\n ', manager=services.get_instance_manager(sims4.resources.Types.ACTION), class_restrictions=('LootActions',), pack_safe=True)) PUT_AWAY_FINISHED_LAUNDRY = TunableTuple(description='\n The tunable to update laundry service on Put Away finished laundry\n interaction.\n ', interaction_tag=TunableEnumWithFilter(description='\n Tag that represent the put away finished laundry interaction which \n will update Laundry Service data.\n ', tunable_type=Tag, default=Tag.INVALID, filter_prefixes=('interaction',)), laundry_condition_states=TunableTuple(description='\n This is the state type of completed laundry object condition \n which will aggregate the data to the laundry service.\n ', condition_states=TunableList(description='\n A list of state types to be stored on laundry service.\n ', tunable=TunableStateTypeReference(pack_safe=True), unique_entries=True), excluded_states=TunableList(description='\n A list of state values of Condition States which will not \n be added to the laundry service.\n ', tunable=TunableStateValueReference(pack_safe=True), unique_entries=True)), laundry_condition_timeout=TunableSimMinute(description='\n The amount of time in Sim minutes that the individual laundry\n finished conditions will be kept in the laundry conditions \n aggregate data.\n ', default=1440, minimum=0), conditions_and_rewards_map=TunableMapping(description='\n Mapping of laundry conditions and loot rewards.\n ', key_type=TunableReference(manager=services.get_instance_manager(sims4.resources.Types.OBJECT_STATE), pack_safe=True), value_type=TunableReference(manager=services.get_instance_manager(sims4.resources.Types.ACTION), class_restrictions=('LootActions',), pack_safe=True))) PUT_CLOTHING_PILE_ON_HAMPER = TunableTuple(description='\n The Tunable to directly put generated clothing pile in the hamper.\n ', chance=TunablePercent(description='\n The chance that a clothing pile will be put directly in the hamper. \n Tune the value in case putting clothing pile in hamper every \n spin-outfit-change feeling excessive.\n ', default=100), clothing_pile=TunableTuple(description="\n Clothing pile object that will be created and put into the hamper \n automatically. \n \n You won't see the object on the lot since it will go directly to \n the hamper. We create it because we need to transfer all of the \n commodities data and average the values into the hamper precisely.\n ", definition=TunablePackSafeReference(description='\n Reference to clothing pile object definition.\n ', manager=services.definition_manager()), initial_states=TunableList(description='\n A list of states to apply to the clothing pile as soon as it \n is created.\n ', tunable=TunableTuple(description='\n The state to apply and optional to decide if the state \n should be applied.\n ', state=TunableStateValueReference(pack_safe=True), tests=TunableTestSet()))), full_hamper_state=TunableStateValueReference(description='\n The state of full hamper which make the hamper is unavailable to \n add new clothing pile in it.\n ', pack_safe=True), loots_to_apply=TunableList(description='\n Loots to apply to the hamper when clothing pile is being put.\n ', tunable=TunableReference(manager=services.get_instance_manager(sims4.resources.Types.ACTION), class_restrictions=('LootActions',), pack_safe=True)), tests=TunableTestSet(description='\n The test to run on the Sim that must pass in order for putting\n clothing pile automatically to the hamper. These tests will only \n be run when we have available hamper on the lot.\n '))
def __init__(self, **kwargs): super().__init__( gender=TunableEnumEntry( description= "\n The Sim's gender.\n ", tunable_type=Gender, default=None), species=TunableEnumEntry( description= "\n The Sim's species.\n ", tunable_type=SpeciesExtended, default=SpeciesExtended.HUMAN, invalid_enums=(SpeciesExtended.INVALID, )), age_variant=TunableVariant( description= "\n The sim's age for creation. Can be a literal age or random\n between two ages.\n ", literal=LiteralAge.TunableFactory(), random=RandomAge.TunableFactory()), resource_key=OptionalTunable( description= '\n If enabled, the Sim will be created using a saved SimInfo file.\n ', tunable=TunableResourceKey( description= '\n The SimInfo file to use.\n ', default=None, resource_types=(sims4.resources.Types.SIMINFO, ))), full_name=TunableVariant( description= '\n If specified, then defines how the Sims name will be determined.\n ', enabled=TunableLocalizedString( description= "\n The Sim's name will be determined by this localized string. \n Their first, last and full name will all be set to this. \n " ), name_type=TunableEnumEntry( description= '\n The sim name type to use when generating the Sims name\n randomly.\n ', tunable_type=SimNameType, default=SimNameType.DEFAULT), locked_args={'disabled': None}, default='disabled'), tunable_tag_set=TunableReference( description= '\n The set of tags that this template uses for CAS creation.\n ', manager=services.get_instance_manager( sims4.resources.Types.TAG_SET), allow_none=True, class_restrictions=('TunableTagSet', )), weighted_tag_lists=TunableList( description= '\n A list of weighted tag lists. Each weighted tag list adds\n a single tag to the set of tags to use for Sim creation.\n ', tunable=TunableReference( description= '\n A weighted tag list. A single tag is added to the set of\n tags for Sim creation from this list based on the weights.\n ', manager=services.get_instance_manager( sims4.resources.Types.TAG_SET), class_restrictions=('TunableWeightedTagList', ))), filter_flag=TunableEnumFlags( description= '\n Define how to handle part randomization for the generated outfit.\n ', enum_type=OutfitFilterFlag, default=OutfitFilterFlag.USE_EXISTING_IF_APPROPRIATE | OutfitFilterFlag.USE_VALID_FOR_LIVE_RANDOM, allow_no_flags=True), body_type_chance_overrides=TunableMapping( description= '\n Define body type chance overrides for the generate outfit. For\n example, if BODYTYPE_HAT is mapped to 100%, then the outfit is\n guaranteed to have a hat if any hat matches the specified tags.\n ', key_type=BodyType, value_type=TunablePercent( description= '\n The chance that a part is applied to the corresponding body\n type.\n ', default=100)), body_type_match_not_found_policy=TunableMapping( description= '\n The policy we should take for a body type that we fail to find a\n match for. Primary example is to use MATCH_NOT_FOUND_KEEP_EXISTING\n for generating a tshirt and making sure a sim wearing full body has\n a lower body cas part.\n ', key_type=BodyType, value_type=MatchNotFoundPolicy), **kwargs)
class RebateManager: __qualname__ = 'RebateManager' TRAIT_REBATE_MAP = TunableMapping( description= '\n A mapping of traits and the tags of objects which provide a rebate for\n the given trait.\n ', key_type=TunableReference( description= '\n If the Sim has this trait, any objects purchased with the given\n tag(s) below will provide a rebate.\n ', manager=services.trait_manager()), value_type=TunableTuple( description= '\n The information about the rebates the player should get for having\n the mapped trait.\n ', valid_objects=TunableVariant( description= '\n The items to which the rebate will be applied.\n ', by_tag=TunableSet( description= '\n The rebate will only be applied to objects purchased with the\n tags in this list.\n ', tunable=TunableEnumEntry(tag.Tag, tag.Tag.INVALID)), locked_args={'all_purchases': None}), rebate_percentage=TunablePercent( description= '\n The percentage of the catalog price that the player will get\n back in the rebate.\n ', default=10))) REBATE_PAYMENT_SCHEDULE = TunableWeeklyScheduleFactory( description= '\n The schedule when accrued rebates will be paid out.\n ' ) REBATE_NOTIFICATION = UiDialogNotification.TunableFactory( description= '\n The notification that will show when the player receives their rebate money.\n ', locked_args={'text': None}) REBATE_NOTIFICATION_HEADER = TunableLocalizedString( description= '\n The header for the rebate notification that displays when the households\n gets their rebate payout.\n ' ) REBATE_NOTIFICATION_LINE_ITEM = TunableLocalizedStringFactory( description= '\n Each trait that gave rebates will generate a new line item on the notification.\n {0.String} = trait name\n {1.Money} = amount of rebate money received from the trait.\n ' ) def __init__(self, household): self._household = household self._rebates = Counter() self._schedule = None def add_rebate_for_object(self, obj): for (trait, rebate_info) in self.TRAIT_REBATE_MAP.items(): rebate_percentage = rebate_info.rebate_percentage valid_objects = rebate_info.valid_objects while self._sim_in_household_has_trait(trait): if valid_objects is None or self._object_has_required_tags( obj, valid_objects): self._rebates[ trait] += obj.catalog_value * rebate_percentage if self._rebates: self.start_rebate_schedule() def _sim_in_household_has_trait(self, trait): return any( s.trait_tracker.has_trait(trait) for s in self._household.sim_info_gen()) @staticmethod def _object_has_required_tags(obj, valid_tags): return set(obj.tags) & set(valid_tags) def clear_rebates(self): self._rebates.clear() def start_rebate_schedule(self): if self._schedule is None: self._schedule = self.REBATE_PAYMENT_SCHEDULE( start_callback=self._payout_rebates, schedule_immediate=False) def _payout_rebates(self, *_): if not self._rebates: return active_sim = self._household.client.active_sim line_item_text = LocalizationHelperTuning.get_new_line_separated_strings( *(self.REBATE_NOTIFICATION_LINE_ITEM(t.display_name(active_sim), a) for (t, a) in self._rebates.items())) notification_text = LocalizationHelperTuning.get_new_line_separated_strings( self.REBATE_NOTIFICATION_HEADER, line_item_text) dialog = self.REBATE_NOTIFICATION( active_sim, text=lambda *_, **__: notification_text) dialog.show_dialog() total_rebate_amount = sum(self._rebates.values()) self._household.funds.add( total_rebate_amount, reason=Consts_pb2.TELEMETRY_MONEY_ASPIRATION_REWARD, sim=active_sim) self.clear_rebates()
class ObjectTeleportationComponent(Component, HasTunableFactory, AutoFactoryInit, component_name=types.OBJECT_TELEPORTATION_COMPONENT): ON_CLIENT_CONNECT = 0 FACTORY_TUNABLES = {'when_to_teleport': TunableVariant(description='\n When this object should teleport around.\n ', locked_args={'on_client_connect': ON_CLIENT_CONNECT}, default='on_client_connect'), 'chance_to_teleport': TunablePercent(description='\n A percent chance that this object will teleport when the\n appropriate situation arises.\n ', default=100), 'required_states': OptionalTunable(TunableList(description='\n The states this object is required to be in in order to teleport.\n ', tunable=TunableStateValueReference())), 'objects_to_teleport_near': TunableList(description='\n A tunable list of static commodities, weights and behavior. When\n choosing where to teleport, objects with higher weights have a\n greater chance of being chosen.\n \n If we fail to find a valid location near an object advertising the\n chosen static commodity, we will search try again with a new object\n until the list has been exhausted.\n ', tunable=TunableTuple(description='\n A static commodity and weight.\n ', weight=TunableRange(description='\n A weight, between 0 and 1, that determines how likely this\n static commodity is to be chosen over the others listed.\n ', tunable_type=float, minimum=0, maximum=1, default=1), static_commodity=TunableReference(description='\n Reference to a type of static commodity.\n ', manager=services.static_commodity_manager()), state_change=OptionalTunable(TunableStateValueReference(description='\n A state value to apply to the object advertising this\n commodity if the teleport succeeds.\n '))))} def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) zone = services.current_zone() if self.when_to_teleport == self.ON_CLIENT_CONNECT and not zone.is_zone_running: zone.register_callback(zone_types.ZoneState.CLIENT_CONNECTED, self.teleport) def teleport(self): if random.random() > self.chance_to_teleport: return if self.required_states is not None: for state in self.required_states: if not self.owner.state_value_active(state): return weights_and_commodities = [(obj_dict.weight, obj_dict.static_commodity, obj_dict.state_change) for obj_dict in self.objects_to_teleport_near] while weights_and_commodities: index = sims4.random._weighted(weights_and_commodities) (_, static_commodity, state_change) = weights_and_commodities.pop(index) motives = set() motives.add(static_commodity) all_objects = list(services.object_manager().valid_objects()) random.shuffle(all_objects) for obj in all_objects: if obj is self.owner: continue if obj.commodity_flags & motives: starting_location = placement.create_starting_location(position=obj.position) if self.owner.is_sim: fgl_context = placement.create_fgl_context_for_sim(starting_location, self.owner) else: fgl_context = placement.create_fgl_context_for_object(starting_location, self.owner) (position, orientation) = placement.find_good_location(fgl_context) if position is not None and orientation is not None: self.owner.transform = sims4.math.Transform(position, orientation) if state_change is not None: obj.set_state(state_change.state, state_change) break
class RestaurantTuning: MENU_PRESETS = TunableMapping( description= '\n The map to tune preset of menus that player to select to use in\n restaurant customization.\n ', key_type=TunableEnumEntry(tunable_type=MenuPresets, default=MenuPresets.CUSTOMIZE, binary_type=EnumBinaryExportType.EnumUint32), value_type=TunableTuple( description='\n Menu preset contents.\n ', preset_name=TunableLocalizedString( description= '\n Menu preset name that appear in both menu customize UI and in\n game menu UI.\n ' ), recipe_map=TunableMapping( description= "\n The map that represent a menu preset. It's organized with courses\n like drink, appetizer, entree etc, and in each course there are\n options of recipes.\n ", key_type=TunableEnumWithFilter( tunable_type=Tag, filter_prefixes=['recipe_course'], default=Tag.INVALID, invalid_enums=(Tag.INVALID, ), pack_safe=True, binary_type=EnumBinaryExportType.EnumUint32), value_type=TunableSet( tunable=TunableReference(manager=services.recipe_manager(), class_restrictions=('Recipe', ), pack_safe=True)), key_name='course_tags', value_name='recipes', tuple_name='MenuCourseMappingTuple'), show_in_restaurant_menu=Tunable( description= "\n If this is enabled, this menu preset will show up on restaurant\n menus. If not, it won't. Currently, only home-chef menus\n shouldn't show up on restaurant menus.\n ", tunable_type=bool, default=True), export_class_name='MenuPresetContentTuple'), key_name='preset_enum', value_name='preset_contents', tuple_name='MenuPresetMappingTuple', export_modes=ExportModes.All) MENU_TAG_DISPLAY_CONTENTS = TunableMapping( description= '\n The map to tune menu tags to display contents.\n ', key_type=TunableEnumWithFilter( tunable_type=Tag, filter_prefixes=['recipe'], default=Tag.INVALID, invalid_enums=(Tag.INVALID, ), pack_safe=True, binary_type=EnumBinaryExportType.EnumUint32), value_type=TunableTuple( description= '\n menu tag display contents.\n ', menu_tag_name=TunableLocalizedString(), menu_tag_icon=TunableResourceKey( description= '\n This will display as the filter icon in the course recipe picker UI.\n ', resource_types=sims4.resources.CompoundTypes.IMAGE), export_class_name='MenuTagDisplayTuple'), key_name='menu_tags', value_name='menu_tag_display_contents', tuple_name='MenuTagDisplayMappingTuple', export_modes=ExportModes.ClientBinary) COURSE_SORTING_SEQUENCE = TunableSet( description= '\n This set determines the sorting sequence for courses in both menu\n customize UI and in game menu UI.\n ', tunable=TunableEnumWithFilter( tunable_type=Tag, filter_prefixes=['recipe_course'], default=Tag.INVALID, invalid_enums=(Tag.INVALID, ), pack_safe=True, binary_type=EnumBinaryExportType.EnumUint32), export_modes=ExportModes.ClientBinary) DAILY_SPECIAL_DISCOUNT = TunablePercent( description= '\n The percentage of the base price when an item is the daily special.\n For example, if the base price is $10 and this is tuned to 80%, the\n discounted price will be $10 x 80% = $8\n ', default=80) INVALID_DAILY_SPECIAL_RECIPES = TunableList( description= '\n A list of recipes that should not be considered for daily specials.\n i.e. Glass of water.\n ', tunable=TunableReference( description= '\n The recipe to disallow from being a daily special.\n ', manager=services.recipe_manager(), class_restrictions=('Recipe', ), pack_safe=True)) COURSE_TO_FILTER_TAGS_MAPPING = TunableMapping( description= '\n Mapping from course to filter tags for food picker UI.\n ', key_type=TunableEnumWithFilter( description= '\n The course associated with the list of filters.\n ', tunable_type=Tag, filter_prefixes=['recipe_course'], default=Tag.INVALID, invalid_enums=(Tag.INVALID, ), pack_safe=True, binary_type=EnumBinaryExportType.EnumUint32), value_type=TunableList( description= '\n This list of filter tags for the food picker UI for the course\n specified.\n ', tunable=TunableEnumWithFilter( tunable_type=Tag, filter_prefixes=['recipe_category'], default=Tag.INVALID, invalid_enums=(Tag.INVALID, ), pack_safe=True, binary_type=EnumBinaryExportType.EnumUint32)), key_name='course_key', value_name='course_filter_tags', tuple_name='CourseToFilterTuple', export_modes=ExportModes.ClientBinary) CUSTOMER_QUALITY_STAT = TunablePackSafeReference( description= '\n The Customer Quality stat applied to food/drink the restaurant customer\n eats/drinks. This is how we apply buffs to the Sim at the time they\n consume the food/drink.\n \n The Customer Quality value is determined by multiplying the Final\n Quality To Customer Quality Multiplier (found in Final Quality State\n Data Mapping) by the Food Difficulty To Customer Quality Multiplier\n (found in the Ingredient Quality State Data Mapping).\n ', manager=services.get_instance_manager(sims4.resources.Types.STATISTIC)) CUSTOMER_VALUE_STAT = TunablePackSafeReference( description= '\n The Customer Value stat applied to food/drink the restaurant customer\n eats/drinks. This is how we apply buffs to the Sim at the time they\n consume the food/drink.\n \n The Customer Value value is determined by multiplying the Final Quality\n To Customer Value Multiplier (found in Final Quality State Data Mapping)\n by the Markup To Customer Value Multiplier (found in the Markup Data\n Mapping).\n ', manager=services.get_instance_manager(sims4.resources.Types.STATISTIC)) RECIPE_DIFFICULTY_DATA_MAPPING = TunableMapping( description= '\n A mapping of the recipe difficulty for restaurants to the appropriate\n data.\n ', key_name='recipe_difficulty', key_type=TunableEnumEntry( description= "\n The recipe difficulty for chef's at a restaurant.\n ", tunable_type=RecipeDifficulty, default=RecipeDifficulty.NORMAL), value_name='recipe_difficulty_data', value_type=TunableTuple( description= '\n The tuning associated with the provided recipe difficulty.\n ', recipe_difficulty_to_final_quality_adder=Tunable( description= '\n This value is added to the Ingredient Quality To Final Quality Adder\n and the Cooking Speed To Final Quality Adder to determine the player-\n facing recipe quality.\n ', tunable_type=float, default=0), recipe_difficulty_to_customer_quality_multiplier=Tunable( description= "\n This value is multiplied by the Final Quality To Customer\n Quality Multiplier to determine the customer's perceived quality\n of the recipe.\n ", tunable_type=float, default=1))) DEFAULT_INGREDIENT_QUALITY = TunableEnumEntry( description= '\n The default ingredient quality for a restaurant.\n ', tunable_type=RestaurantIngredientQualityType, default=RestaurantIngredientQualityType.INVALID, invalid_enums=(RestaurantIngredientQualityType.INVALID, )) INGREDIENT_QUALITY_DATA_MAPPING = TunableMapping( description= '\n The mapping between ingredient enum and the ingredient data for\n that type.\n ', key_type=TunableEnumEntry( description= '\n The ingredient type. Organic, normal, lousy, etc...\n ', tunable_type=RestaurantIngredientQualityType, default=RestaurantIngredientQualityType.INVALID, invalid_enums=(RestaurantIngredientQualityType.INVALID, ), binary_type=EnumBinaryExportType.EnumUint32), value_type=TunableTuple( description= '\n Data associated with this type of ingredient.\n ', ingredient_quality_type_name=TunableLocalizedString( description= '\n The localized name of this ingredient used in various places in\n the UI.\n ' ), ingredient_quality_to_final_quality_adder=Tunable( description= '\n This value is added to the Recipe Difficulty To Final Quality\n Adder and the Cooking Speed To Final Quality Adder to determine\n the player-facing recipe quality.\n ', tunable_type=float, default=0), ingredient_quality_to_restaurant_expense_multiplier=TunableRange( description= '\n This value is multiplied by the Base Restaurant Price (found in\n the Recipe tuning) for each recipe served to determine what the\n cost is to the restaurant for preparing that recipe.\n ', tunable_type=float, default=0.5, minimum=0), export_class_name='IngredientDataTuple'), key_name='ingredient_enum', value_name='ingredient_data', tuple_name='IngredientEnumDataMappingTuple', export_modes=ExportModes.All) COOKING_SPEED_DATA_MAPPING = TunableMapping( description= '\n A mapping from chef cooking speed to the data associated with that\n cooking speed.\n ', key_name='cooking_speed_buff', key_type=TunableReference( description= '\n The cooking speed buff that is applied to the chef.\n ', manager=services.get_instance_manager(sims4.resources.Types.BUFF), pack_safe=True), value_name='cooking_speed_data', value_type=TunableTuple( description= '\n The data associated with the tuned cooking speed.\n ', cooking_speed_to_final_quality_adder=Tunable( description= '\n This value is added to the Recipe Difficulty To Final Quality\n Adder and the Ingredient Quality To Final Quality Adder to\n determine the player-facing recipe quality.\n ', tunable_type=float, default=0), active_cooking_states_delta=Tunable( description= '\n The amount by which to adjust the number of active cooking\n states the chef must complete before completing the order. For\n instance, if a -1 is tuned here, the chef will have to complete\n one less state than normal. Regardless of how the buffs are\n tuned, the chef will always run at least one state before\n completing the order.\n ', tunable_type=int, default=-1))) CHEF_SKILL_TO_FOOD_FINAL_QUALITY_ADDER_DATA = TunableTuple( description= '\n Pairs a skill with a curve to determine the additional value to add to\n the final quality of a food made at an owned restaurant.\n ', skill=TunablePackSafeReference( description= '\n The skill used to determine the adder for the final quality of food.\n ', manager=services.get_instance_manager( sims4.resources.Types.STATISTIC), class_restrictions=('Skill', )), final_quality_adder_curve=TunableCurve( description= "\n Maps the chef's current level of the tuned skill to a value that\n will be added to the final quality statistic for food recipes cooked\n at an owned restaurant.\n ", x_axis_name='Skill Level', y_axis_name='Food Final Quality Adder')) CHEF_SKILL_TO_DRINK_FINAL_QUALITY_ADDER_DATA = TunableTuple( description= '\n Pairs a skill with a curve to determine the additional value to add to\n the final quality of a drink made at an owned restaurant.\n ', skill=TunablePackSafeReference( description= '\n The skill used to determine the adder for the final quality of drinks.\n ', manager=services.get_instance_manager( sims4.resources.Types.STATISTIC), class_restrictions=('Skill', )), final_quality_adder_curve=TunableCurve( description= "\n Maps the chef's current level of the tuned skill to a value that\n will be added to the final quality statistic for drink recipes\n cooked at an owned restaurant.\n ", x_axis_name='Skill Level', y_axis_name='Food Final Quality Adder')) FINAL_QUALITY_STATE_DATA_MAPPING = TunableMapping( description= '\n A mapping of final quality recipe states (Poor, Normal, Outstanding) to\n the data associated with that recipe quality.\n ', key_name='recipe_quality_state', key_type=TunableReference( description= '\n The recipe quality state value.\n ', manager=services.get_instance_manager( sims4.resources.Types.OBJECT_STATE), class_restrictions='ObjectStateValue', pack_safe=True), value_name='recipe_quality_state_value_data', value_type=TunableTuple( description= '\n The data associated with the tuned recipe quality state value.\n ', final_quality_to_customer_quality_multiplier=Tunable( description= '\n This value is multiplied by the Recipe Difficulty To Customer\n Quality Multiplier to determine the Customer Quality State value\n of the recipe.\n ', tunable_type=float, default=1), final_quality_to_customer_value_multiplier=Tunable( description= '\n This value is multiplied by the Markup To Customer Value\n Multiplier to determine the value of the Customer Value Stat\n value of the recipe.\n ', tunable_type=float, default=1))) PRICE_MARKUP_DATA_MAPPING = TunableMapping( description= '\n A mapping of the current price markup of the restaurant to the data\n associated with that markup.\n ', key_name='markup_multiplier', key_type=Tunable( description= '\n The markup multiplier. this needs to be in line with the available\n markups tuned on the restaurant business.\n ', tunable_type=float, default=1.5), value_name='markup_multiplier_data', value_type=TunableTuple( description= '\n The data associated with the tuned markup multiplier.\n ', markup_to_customer_value_multiplier=Tunable( description='\n ', tunable_type=float, default=1))) BUSINESS_FUNDS_CATEGORY_FOR_COST_OF_INGREDIENTS = TunableEnumEntry( description= '\n When a Chef cooks an order, the restaurant has to pay for the\n ingredients. This is the category for those expenses.\n ', tunable_type=BusinessFundsCategory, default=BusinessFundsCategory.NONE, invalid_enums=(BusinessFundsCategory.NONE, )) ATTIRE = TunableList( description= '\n List of attires player can select to apply to the restaurant.\n ', tunable=TunableEnumEntry(tunable_type=OutfitCategory, default=OutfitCategory.EVERYDAY, binary_type=EnumBinaryExportType.EnumUint32), export_modes=ExportModes.All) UNIFORM_CHEF_MALE = TunablePackSafeResourceKey( description= '\n The SimInfo file to use to edit male chef uniforms.\n ', default=None, resource_types=(sims4.resources.Types.SIMINFO, ), export_modes=ExportModes.All) UNIFORM_CHEF_FEMALE = TunablePackSafeResourceKey( description= '\n The SimInfo file to use to edit female chef uniforms.\n ', default=None, resource_types=(sims4.resources.Types.SIMINFO, ), export_modes=ExportModes.All) UNIFORM_WAITSTAFF_MALE = TunablePackSafeResourceKey( description= '\n The SimInfo file to use to edit waiter uniforms.\n ', default=None, resource_types=(sims4.resources.Types.SIMINFO, ), export_modes=ExportModes.All) UNIFORM_WAITSTAFF_FEMALE = TunablePackSafeResourceKey( description= '\n The SimInfo file to use to edit waitress uniforms.\n ', default=None, resource_types=(sims4.resources.Types.SIMINFO, ), export_modes=ExportModes.All) UNIFORM_HOST_MALE = TunablePackSafeResourceKey( description= '\n The SimInfo file to use to edit male host uniforms.\n ', default=None, resource_types=(sims4.resources.Types.SIMINFO, ), export_modes=ExportModes.All) UNIFORM_HOST_FEMALE = TunablePackSafeResourceKey( description= '\n The SimInfo file to use to edit female host uniforms.\n ', default=None, resource_types=(sims4.resources.Types.SIMINFO, ), export_modes=ExportModes.All) RESTAURANT_VENUE = TunablePackSafeReference( description= '\n This is a tunable reference to the type of Venue that will describe\n a Restaurant. To be used for code references to restaurant venue types\n in code.\n ', manager=services.get_instance_manager(sims4.resources.Types.VENUE)) HOST_SITUATION = TunablePackSafeReference( description= '\n The situation that Sims working as a Host will have.\n ', manager=services.get_instance_manager(sims4.resources.Types.SITUATION)) WAITSTAFF_SITUATION = TunablePackSafeReference( description= '\n The situation that Sims working as a Waiter will have.\n ', manager=services.get_instance_manager(sims4.resources.Types.SITUATION)) CHEF_SITUATION = TunablePackSafeReference( description= '\n The situation that Sims working as a Chef will have.\n ', manager=services.get_instance_manager(sims4.resources.Types.SITUATION)) HOME_CHEF_SITUATION_TAG = TunableEnumWithFilter( description= '\n Tag that we use on all the home chef situations.\n ', tunable_type=Tag, filter_prefixes=['situation'], default=Tag.INVALID, invalid_enums=(Tag.INVALID, ), pack_safe=True) DINING_SITUATION_TAG = TunableEnumWithFilter( description= "\n The tag used to find dining situations. \n \n This shouldn't need to be re-tuned after being set initially. If you\n need to re-tune this you should probably talk to a GPE first.\n ", tunable_type=Tag, filter_prefixes=['situation'], default=Tag.INVALID, pack_safe=True) TABLE_FOOD_SLOT_TYPE = TunableReference( description= '\n The slot type of the food slot on the dining table.\n ', manager=services.get_instance_manager(sims4.resources.Types.SLOT_TYPE)) TABLE_DRINK_SLOT_TYPE = TunableReference( description= '\n The slot type of the drink slot on the dining table.\n ', manager=services.get_instance_manager(sims4.resources.Types.SLOT_TYPE)) FOOD_AUTONOMY_PREFERENCE = TunableAutonomyPreference( description= '\n The Autonomy Preference for the delivered food items.\n ', is_scoring=False) DRINK_AUTONOMY_PREFERENCE = TunableAutonomyPreference( description= '\n The Autonomy Preference for the delivered drink items.\n ', is_scoring=False) CONSUMABLE_FULL_STATE_VALUE = TunableReference( description= '\n The Consumable_Full state value. Food in restaurants will be set to\n this value instead of defaulting to Consumable_Untouched to avoid other\n Sims from eating your own food.\n ', manager=services.get_instance_manager( sims4.resources.Types.OBJECT_STATE), class_restrictions=('ObjectStateValue', )) CONSUMABLE_EMPTY_STATE_VALUE = TunableReference( description= "\n The Consumable_Empty state value. This is the state we'll use to\n determine if food/drink is empty or not.\n ", manager=services.get_instance_manager( sims4.resources.Types.OBJECT_STATE), class_restrictions=('ObjectStateValue', )) FOOD_DELIVERED_TO_TABLE_NOTIFICATION = TunableUiDialogNotificationSnippet( description= "\n The notification shown when the food is delivered to the player's table.\n " ) FOOD_STILL_ON_TABLE_NOTIFICATION = TunableUiDialogNotificationSnippet( description= "\n The notification that the player will see if the waitstaff try and\n deliver food but there's still food on the table.\n " ) STAND_UP_INTERACTION = TunableReference( description= '\n A reference to sim-stand so that sim-stand can be pushed on every sim\n that is sitting at a table that is abandoned.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION)) DEFAULT_MENU = TunableEnumEntry( description= '\n The default menu setting for a brand new restaurant.\n ', tunable_type=MenuPresets, default=MenuPresets.CUSTOMIZE, export_modes=ExportModes.All, binary_type=EnumBinaryExportType.EnumUint32) SWITCH_SEAT_INTERACTION = TunableReference( description= '\n This is a reference to the interaction that gets pushed on whichever Sim\n is sitting in the seat that the Actor is switching to. The interaction \n will be pushed onto the sseated Sim and will target the Actor Sims \n current seat before the switch.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION)) RECOMMENDED_ORDER_INTERACTION = TunableReference( description= '\n This is a reference to the interaction that will get pushed on the active Sim\n to recommend orders to the Sim AFTER the having gone through the Menu UI.\n \n It will continue to retain the previous target.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION), pack_safe=True) INGREDIENT_PRICE_PERK_MAP = TunableMapping( description= '\n Maps the various ingredient price perks with their corresponding\n discount.\n ', key_name='Ingredient Price Perk', key_type=TunableReference( description= '\n A perk that gives a tunable multiplier to the price of ingredients\n for restaurants.\n ', manager=services.get_instance_manager( sims4.resources.Types.BUCKS_PERK), pack_safe=True), value_name='Ingredient Price Multiplier', value_type=TunableRange( description= '\n If the household has the corresponding perk, this value will be\n multiplied by the final cost of each recipe to the restaurant.\n ', tunable_type=float, default=1, minimum=0)) CUSTOMERS_ORDER_EXPENSIVE_FOOD_PERK_DATA = TunableTuple( description= '\n The perk that makes customers order more expensive food, and the off-lot\n multiplier for that perk.\n ', perk=TunablePackSafeReference( description= '\n If the owning household has this perk, customers will pick two dishes to\n order and then pick the most expensive of the two.\n ', manager=services.get_instance_manager( sims4.resources.Types.BUCKS_PERK)), off_lot_multiplier=TunableRange( description= '\n When calculating off-lot profits, this is applied if the household\n has this perk.\n ', tunable_type=float, default=1.1, minimum=1)) UNOWNED_RESTAURANT_PRICE_MULTIPLIER = TunableRange( description= '\n The amount each item in the menu will be multiplied by on unowned\n restaurant lots.\n ', tunable_type=float, default=1.2, minimum=0, export_modes=ExportModes.All) CHEF_NOT_SKILLED_ENOUGH_THRESHOLD = Tunable( description= '\n This is the value that a chef must reach when preparing a meal for a\n customer without displaying the "Chef isn\'t skilled enough to make \n receiver X" \n \n The number that must reach this value is the skill adder\n of the chef and recipe difficulty adder.\n ', tunable_type=int, default=-30) CHEF_NOT_SKILLED_ENOUGH_NOTIFICATION = TunableUiDialogNotificationSnippet( description= '\n The notification shown when the chef is working on a recipe that is \n too difficult for their skill.\n ' ) DEFAULT_PROFIT_PER_MEAL_FOR_OFF_LOT_SIMULATION = TunableRange( description= '\n This is used as the default profit for a meal for off-lot simulation. Once\n enough actual meals have been sold, this value becomes irrelevant and\n the MEAL_COUNT_FOR_OFF_LOT_PROFIT_PER_MEAL tunable comes into use.\n ', tunable_type=int, default=20, minimum=1) MEAL_COUNT_FOR_OFF_LOT_PROFIT_PER_MEAL = TunableRange( description= '\n The number of meals to keep a running average of for the profit per meal\n calculations during off lot simulations.\n ', tunable_type=int, default=10, minimum=2) ADVERTISING_DATA_MAP = TunableMapping( description= '\n The mapping between advertising type and the data for that type.\n ', key_name='Advertising_Type', key_type=TunableEnumEntry( description='\n The Advertising Type .\n ', tunable_type=BusinessAdvertisingType, default=BusinessAdvertisingType.INVALID, invalid_enums=(BusinessAdvertisingType.INVALID, ), binary_type=EnumBinaryExportType.EnumUint32), value_name='Advertising_Data', value_type=TunableTuple( description= '\n Data associated with this advertising type.\n ', cost_per_hour=TunableRange( description= '\n How much, per hour, it costs to use this advertising type.\n ', tunable_type=int, default=10, minimum=0), customer_count_multiplier=TunableRange( description= '\n This amount is multiplied by the ideal customer count for owned\n restaurants.\n ', tunable_type=float, default=0.8, minimum=0), ui_sort_order=TunableRange( description= '\n Value representing how map entries will be sorted in the UI.\n 1 represents the first entry. Avoid duplicate values\n within the map.\n ', tunable_type=int, minimum=1, default=1), export_class_name='RestaurantAdvertisingData'), tuple_name='RestaurantAdvertisingDataMapping', export_modes=ExportModes.All) TODDLER_SENT_TO_DAYCARE_FOR_RESTAURANTS = TunableUiDialogNotificationSnippet( description= '\n The notification shown when a toddler is sent to daycare upon traveling\n to a restaurant venue.\n ' ) TIME_OF_DAY_TO_CUSTOMER_COUNT_MULTIPLIER_CURVE = TunableCurve( description= '\n A curve that lets you tune a specific customer count multiplier\n based on the time of day. Time of day should range between 0 and 23,\n 0 being midnight.\n ', x_axis_name='time_of_day', y_axis_name='customer_count_multiplier')
class ModifyAllLotItems(HasTunableFactory, AutoFactoryInit): DESTROY_OBJECT = 0 SET_STATE = 1 INVENTORY_TRANSFER = 2 DELIVER_BILLS = 3 SET_ON_FIRE = 4 CLEANUP_VEHICLE = 5 LOOT = 6 FACTORY_TUNABLES = { 'description': '\n Tune modifications to apply to all objects on a lot.\n Can do state changes, destroy certain items, etc.\n \n EX: for auto cleaning, tune to have objects with Dirtiness state that\n equals dirty to be set to the clean state and tune to have dirty dishes\n and spoiled food to be deleted\n ', 'modifications': TunableList( description= "\n A list of where the elements define how to modify objects on the\n lot. Each entry is a triplet of an object modification action\n (currently either destroy the object or set its state), a list of\n tests to run on the object to determine if we should actually apply\n the modification, and a priority in case some modifications should\n take precedence over other ones when both of their tests pass.\n \n EX: test list: object's dirtiness state != dirtiness clean\n action: set state to Dirtiness_clean\n \n So dirty objects will become clean\n ", tunable=TunableTuple( action=TunableVariant( set_state=TunableTuple( action_value=TunableStateValueReference( description='An object state to set the object to', pack_safe=True), locked_args={'action_type': SET_STATE}), destroy_object=TunableTuple( locked_args={'action_type': DESTROY_OBJECT}), inventory_transfer=TunableTuple( action_value=InventoryTransferFakePerform. TunableFactory(), locked_args={'action_type': INVENTORY_TRANSFER}), deliver_bills=TunableTuple( action_value=DeliverBillFakePerform.TunableFactory(), locked_args={'action_type': DELIVER_BILLS}), set_on_fire=TunableTuple( locked_args={'action_type': SET_ON_FIRE}), cleanup_vehicle=TunableTuple( description= '\n Cleanup vehicles that are left around.\n ', locked_args={'action_type': CLEANUP_VEHICLE}), loot=TunableTuple( description= '\n Apply loots to the object.\n ', loot_actions=TunableSet( description= '\n Loot(s) to apply.\n ', tunable=TunableReference( manager=services .get_instance_manager( Types.ACTION), pack_safe =True)), locked_args={'action_type': LOOT})), chance=TunablePercent( description= '\n Chance this modification will occur.\n ', default=100, minimum=1), global_tests=TunableObjectModifyGlobalTestList( description= "\n Non object-related tests that gate this modification from occurring. Use this for any global\n tests that don't require the object, such as zone/location/time-elapsed tests. These tests\n will run only ONCE for this action, unlike 'Tests', which runs PER OBJECT. \n " ), tests=TunableObjectModifyTestSet( description= '\n All least one subtest group (AKA one list item) must pass\n within this list before the action associated with this\n tuning will be run.\n ', additional_tests={ 'elapsed_time': TimeElapsedZoneTest.TunableFactory( locked_args={'tooltip': None}), 'statistic': StatThresholdTest.TunableFactory( locked_args={'tooltip': None}) }), weighted_tests=TunableList( description= '\n Weighted tests for the individual object. One is chosen \n based on weight, and all objects are run against that chosen\n test set.\n ', tunable=TunableTuple( tests=TunableObjectModifyTestSet( description= '\n All least one subtest group (AKA one list item) must pass\n within this list before the action associated with this\n tuning will be run.\n ', additional_tests={ 'elapsed_time': TimeElapsedZoneTest.TunableFactory( locked_args={'tooltip': None}), 'statistic': StatThresholdTest.TunableFactory( locked_args={'tooltip': None}) }), weight=TunableRange( description= '\n Weight to use.\n ', tunable_type=int, default=1, minimum=1))))) } def modify_objects(self, object_criteria=None): objects_to_destroy = [] num_modified = 0 modifications = defaultdict(CompoundTestList) for mod in self.modifications: if not random_chance(mod.chance * 100): continue if mod.global_tests and not mod.global_tests.run_tests( GlobalResolver()): continue if mod.tests: modifications[mod.action].extend(mod.tests) if mod.weighted_tests: weighted_tests = [] for test_weight_pair in mod.weighted_tests: weighted_tests.append( (test_weight_pair.weight, test_weight_pair.tests)) modifications[mod.action].extend( weighted_random_item(weighted_tests)) if not modifications: return num_modified all_objects = list(services.object_manager().values()) for obj in all_objects: if obj.is_sim: continue if object_criteria is not None and not object_criteria(obj): continue resolver = SingleObjectResolver(obj) modified = False for (action, tests) in modifications.items(): if not tests.run_tests(resolver): continue modified = True action_type = action.action_type if action_type == ModifyAllLotItems.DESTROY_OBJECT: objects_to_destroy.append(obj) break elif action_type == ModifyAllLotItems.SET_STATE: new_state_value = action.action_value if obj.state_component and obj.has_state( new_state_value.state): obj.set_state(new_state_value.state, new_state_value, immediate=True) if action_type in ( ModifyAllLotItems.INVENTORY_TRANSFER, ModifyAllLotItems.DELIVER_BILLS): element = action.action_value() element._do_behavior() elif action_type == ModifyAllLotItems.SET_ON_FIRE: fire_service = services.get_fire_service() fire_service.spawn_fire_at_object(obj) elif action_type == ModifyAllLotItems.CLEANUP_VEHICLE: if self._should_cleanup_vehicle(obj): objects_to_destroy.append(obj) if action_type == ModifyAllLotItems.LOOT: for loot_action in action.loot_actions: loot_action.apply_to_resolver(resolver) else: raise NotImplementedError else: raise NotImplementedError elif action_type == ModifyAllLotItems.LOOT: for loot_action in action.loot_actions: loot_action.apply_to_resolver(resolver) else: raise NotImplementedError else: raise NotImplementedError elif action_type in (ModifyAllLotItems.INVENTORY_TRANSFER, ModifyAllLotItems.DELIVER_BILLS): element = action.action_value() element._do_behavior() elif action_type == ModifyAllLotItems.SET_ON_FIRE: fire_service = services.get_fire_service() fire_service.spawn_fire_at_object(obj) elif action_type == ModifyAllLotItems.CLEANUP_VEHICLE: if self._should_cleanup_vehicle(obj): objects_to_destroy.append(obj) if action_type == ModifyAllLotItems.LOOT: for loot_action in action.loot_actions: loot_action.apply_to_resolver(resolver) else: raise NotImplementedError else: raise NotImplementedError elif action_type == ModifyAllLotItems.LOOT: for loot_action in action.loot_actions: loot_action.apply_to_resolver(resolver) else: raise NotImplementedError else: raise NotImplementedError if modified: num_modified += 1 for obj in objects_to_destroy: obj.destroy(source=self, cause='Destruction requested by modify lot tuning') objects_to_destroy = None return num_modified def _should_cleanup_vehicle(self, obj): vehicle_component = obj.get_component(VEHICLE_COMPONENT) if vehicle_component is None: return False household_owner_id = obj.get_household_owner_id() if household_owner_id is not None and household_owner_id != 0: return False elif obj.interaction_refs: return False return True