class FanTuning: FANNABLE_CELEBRITY_SITUATION = TunablePackSafeReference(description='\n Situation that will store all celebrity Sims that can be used to\n spawn fan or stan situations. \n ', manager=services.get_instance_manager(Types.SITUATION), class_restrictions='FannableCelebritySimsSituation') FAN_TARGETTING_TAG = TunableTag(description='\n Tag applied to celebrities that can be targeted by fans.\n \n Used in conjunction with autonomy modifiers so we can consider\n celebrities without caring about whether or not they are on or off lot.\n ', filter_prefixes=('Func',)) FAN_SITUATION_TAG = TunableTag(description='\n Tag which delineates which situations are fan situations.\n ', filter_prefixes=('Situation',)) STAN_DISABLING_BITS = TunableSet(description='\n Rel bits, which if any are found on the stan, prevents the stan\n from kicking off their situation. \n \n Used to prevent a stan from being too stanny. \n ', tunable=TunableReference(services.get_instance_manager(Types.RELATIONSHIP_BIT), pack_safe=True)) STAN_FILTER = TunablePackSafeReference(description='\n Filter used to find the stan for a given Sim. This should \n only contain the minimum required filter terms for a Stan.\n ', manager=services.get_instance_manager(Types.SIM_FILTER)) STAN_PERK = TunablePackSafeReference(description='\n Perk used to determine if a Sim is stannable.\n ', manager=services.get_instance_manager(Types.BUCKS_PERK)) @classproperty def are_fans_supported(cls): return cls.FANNABLE_CELEBRITY_SITUATION is not None
def recipe_factory_tuning(pack_safe=False): return { 'recipe_tag': TunableTag( description= '\n The recipe tag to use to create the object.\n ', filter_prefixes=('Recipe', ), pack_safe=pack_safe) }
class TrendTuning: TREND_DATA = TunableList( description='\n A list of data about trends.\n ', tunable=TunableTuple( description= '\n The data about this trend.\n ', trend_tag=TunableTag( description= '\n The tag for this trend.\n ', filter_prefixes=('func_trend', )), trend_type=TunableEnumEntry( description= '\n The type of this trend.\n ', tunable_type=TrendType, default=TrendType.INVALID, invalid_enums=(TrendType.INVALID, )), trend_name=TunableLocalizedString( description= '\n The name for this trend. This will show up in a bulleted\n list when a player researches current trends.\n ' ))) TREND_REFRESH_COOLDOWN = TunableTimeSpan( description= '\n The amount of time it takes before trends refresh.\n ', default_days=2) TREND_TIME_REMAINING_DESCRIPTION = TunableMapping( description= '\n A mapping of thresholds, in Sim Hours, to descriptions used when\n describing the amount of time remaining in the study trends\n notification.\n ', key_name='sim_hours', key_type=int, value_name='description_string', value_type=TunableLocalizedString()) TODDLER_CHILD_TREND = TunableTag( description= '\n The tag we use to indicate Toddler or Child trends.\n ', filter_prefixes=('func_trend', )) CELEBRITY_TREND = TunableTag( description= '\n The tag we use to indicate Celebrity Trends.\n ', filter_prefixes=('func_trend', )) TRENDLESS_VIDEO_DEFINITION = TunableReference( description= '\n The object definition to use if a Sim records a trendless video.\n ', manager=services.definition_manager(), pack_safe=True)
class TunableWeightedTagList(metaclass=TunedInstanceMetaclass, manager=services.get_instance_manager( sims4.resources.Types.TAG_SET)): INSTANCE_TUNABLES = { 'weighted_tags': TunableList( description='\n A list of weighted tags.\n ', tunable=TunableTuple( description= '\n A tag and the weight associated with it.\n ', tag=TunableTag(), weight=TunableRange(tunable_type=float, default=1, minimum=0))) }
class _TestRecipeByTag(HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = { 'required_tag': TunableTag( description= '\n The tag that must exist on the recipe\n ', filter_prefixes=('Recipe', )) } def test_recipe(self, recipe): if self.required_tag not in recipe.recipe_tags: return TestResult( False, f'{recipe} does not have required tag {self.required_tag}') return TestResult.TRUE
class VehicleTuning: SURFACE_FAVORITES = TunableMapping( description= '\n Tuning that specifies which favorite tag to search for when a Sim\n attempts to deploy a vehicle on a given surface.\n \n Example: Sim is in the water and wants to deploy a water vehicle. They\n have both an Aqua Zip and an Island Canoe, but only the Aqua Zip is the\n favorite. We want to ask the favorites tracker if a given favorite\n water vehicle has been set, which is based on the tag tuned here.\n ', key_name='Surface', value_name='Favorite Tag', key_type=TunableEnumEntry( description= '\n The Surface we want to apply a favorite tag to. If the Sim is on\n this surface and has an opportunity to deploy a vehicle, then we\n use the corresponding tag to choose it.\n ', tunable_type=SurfaceType, default=SurfaceType.SURFACETYPE_WORLD, invalid_enums=(SurfaceType.SURFACETYPE_UNKNOWN, )), value_type=TunableTag( description= '\n The favorite tag we search the inventory for when deploying vehicles.\n ', filter_prefixes=('Func', )))
class BaseCivicPolicyProvider(ComponentContainer, HasStatisticComponent, HasTunableFactory, AutoFactoryInit): CIVIC_POLICY_SCHEDULE = TunableTuple(description='\n Global schedule to control when voting on civic policies is active.\n ', voting_open=TunableTimeOfWeek(description='\n The time of the week that voting for civic policies starts.\n ', default_day=Days.MONDAY, default_hour=8, default_minute=0), voting_close=TunableTimeOfWeek(description='\n The time of the week that voting for civic policies ends. Votes are\n tallied and policies are modified at this time.\n ', default_day=Days.SUNDAY, default_hour=16, default_minute=0), voting_close_warning_duration=TunableTimeSpan(description='\n Duration before the Voting Close to warn players that voting is about to close.\n ', default_hours=8), schedule_text=TunableLocalizedStringFactory(description='\n Text for the schedule string.\n ')) INFLUENCE_BUCK_TYPE = TunableEnumEntry(description='\n The type of Bucks used to hold Influence.\n ', tunable_type=BucksType, default=BucksType.INVALID, pack_safe=True) INFLUENCE_TO_VOTE_COST = Tunable(description='\n The amount of influence used with 1 vote.\n ', tunable_type=int, default=10, export_modes=ExportModes.All) REPEAL_PETITION_THRESHOLD = Tunable(description='\n The number of petition signatures required to have a policy repealed.\n ', tunable_type=int, default=10, export_modes=ExportModes.All) COMMUNITY_BOARD_TAG = TunableTag(description='\n The tag of the community boards so we can find them in the world.\n ') VOTING_OPEN_NOTIFICATION = UiDialogNotification.TunableFactory(description='\n A TNS that will fire when the voting period opens.\n ') VOTING_OPEN_MAX_ENABLED_NOTIFICATION = UiDialogNotification.TunableFactory(description='\n A TNS that will fire when the voting period opens with maximum enabled policies.\n ') VOTING_CLOSE_WARNING_NOTIFICATION = UiDialogNotification.TunableFactory(description='\n A TNS that will fire when the voting close approaches.\n ') VOTING_CLOSE_WARNING_MAX_ENABLED_NOTIFICATION = UiDialogNotification.TunableFactory(description='\n A TNS that will fire when the voting close approaches with maximum enabled policies and\n a policy being repealed.\n ') VOTING_CLOSE_NOTIFICATION = UiDialogNotification.TunableFactory(description='\n A TNS that will fire when the voting period closes.\n ') VOTING_CLOSE_MAX_ENABLED_NOTIFICATION_SUCCESS = UiDialogNotification.TunableFactory(description='\n A TNS that will fire when the voting period closes with maximum enabled\n policies and a policy being successfully repealed.\n ') VOTING_CLOSE_MAX_ENABLED_NOTIFICATION_FAIL = UiDialogNotification.TunableFactory(description='\n A TNS that will fire when the voting period closes with maximum enabled\n policies and a policy being unsuccessfully repealed.\n ') VOTING_CONTINUATION_AUTONOMY_COMMODITIES = TunableList(description='\n A list of static commodities that will be solved for by autonomy to\n find and push the vote interaction after viewing the community board.\n ', tunable=StaticCommodity.TunableReference(description='\n A static commodity that is solved for by autonomy to find the vote\n interaction to push. \n ', pack_safe=True)) COMMUNITY_BOARD_TEXT = TunableTuple(voting_closed_policy_tooltip_text=TunableLocalizedStringFactory(description="\n String to insert into the policy tooltips when voting isn't possible\n because voting is closed.\n "), voting_open_add_policy_tooltip_text=TunableLocalizedStringFactory(description="\n Text for the tooltip on the add policy button when it's disabled because\n voting is open. \n "), ineligible_voter_policy_tooltip_text=TunableLocalizedStringFactory(description="\n String to insert into the policy tooltips when voting isn't possible because\n the sim (first token) lives on a different street.\n "), ineligible_voter_confirm_tooltip_text=TunableLocalizedStringFactory(description='\n Text for the tooltip on the confirm button when the button is disabled because\n the sim (first token) lives on a different street.\n '), no_room_confirm_tooltip_text=TunableLocalizedStringFactory(description='\n Text for the tooltip on the confirm button when the button is disabled because\n already full up on enacted policies.\n '), no_room_policy_tooltip_text=TunableLocalizedStringFactory(description="\n String to insert into the policy tooltips when voting isn't possible because\n already full up on enacted policies.\n "), add_policy_picker=TunablePickerDialogVariant(description='\n The item picker dialog.\n ', available_picker_flags=ObjectPickerTuningFlags.ITEM)) CALL_TO_ACTIONS = TunableList(description='\n List of Call to Action that should be started to introduce the Civic Policy features.\n ', tunable=TunableReference(manager=services.get_instance_manager(sims4.resources.Types.CALL_TO_ACTION), pack_safe=True)) FACTORY_TUNABLES = {'civic_policies': TunableSet(description='\n The civic policies that may be enacted.\n ', tunable=TunableReference(description='\n A civic policy.\n ', manager=services.get_instance_manager(sims4.resources.Types.SNIPPET), class_restrictions=('BaseCivicPolicy',), pack_safe=True, export_modes=ExportModes.All)), 'voting_open_loot': TunableList(description='\n Loot applied to Resident Sims when voting opens.\n ', tunable=LootActions.TunableReference(description='\n Loot to apply on voting open.\n ', pack_safe=True), export_modes=ExportModes.ServerXML), 'voting_close_loot': TunableList(description='\n Loot applied to Resident Sims when voting opens.\n ', tunable=LootActions.TunableReference(description='\n Loot to apply on voting open.\n ', pack_safe=True), export_modes=ExportModes.ServerXML), 'community_board_dialog_title': TunableLocalizedStringFactoryVariant(description="\n The Community Board Dialog's title text.\n ", export_modes=ExportModes.ServerXML), 'initial_vote_test': TunableTestSet(description='\n If at least one test passes, and the user option is enabled, initial voting will\n be performed when voting opens.\n ', export_modes=ExportModes.ServerXML), 'daily_random_vote_test': TunableTestSet(description='\n If at least one test passes, and the user option is enabled, daily random voting\n will be performed at midnight.\n ', export_modes=ExportModes.ServerXML)} CIVIC_POLICY_TEST_EVENTS = (TestEvent.CivicPolicyOpenVoting, TestEvent.CivicPolicyDailyRandomVoting, TestEvent.CivicPolicyCloseVoting) def __init__(self, **kwargs): super().__init__(**kwargs) self.add_statistic_component() self._enacted_policies = set() self._balloted_policies = set() self._up_for_repeal_policies = set() self._civic_policies = set() for policy in self.civic_policies: self._civic_policies.add(policy(self)) @constproperty def is_sim(): return False @property def is_downloaded(self): return False def get_enacted_policies(self, tuning=False): if tuning: return set([type(p) for p in self._enacted_policies]) return self._enacted_policies def get_balloted_policies(self, tuning=False): if tuning: return set([type(p) for p in self._balloted_policies]) return self._balloted_policies def get_up_for_repeal_policies(self, tuning=False): if tuning: return set([type(p) for p in self._up_for_repeal_policies]) return self._up_for_repeal_policies def get_dormant_policies(self, tuning=False): policies = self._civic_policies - self._enacted_policies - self._balloted_policies - self._up_for_repeal_policies if tuning: return set([type(p) for p in policies]) return policies def get_civic_policies(self, tuning=False): if tuning: return self.civic_policies return self._civic_policies def new_enact_max_count(self): return min(len(self.civic_policies), self.max_enacted_policy_count) - len(self._enacted_policies) def _reset_voting_statistics(self): for policy in self._civic_policies: if policy.vote_count_statistic is None: logger.error('{} tuned without voting statistic', policy) else: self.set_stat_value(policy.vote_count_statistic, 0) self.commodity_tracker.check_for_unneeded_initial_statistics() self.statistic_tracker.check_for_unneeded_initial_statistics() def run_sim_voting_loot(self, loot_actions): for resolver in self.open_close_voting_loot_resolver_list: for loot in loot_actions: loot.apply_to_resolver(resolver) def run_auto_voting_tests(self, test_set): if not services.street_service().enable_automatic_voting: return False if not test_set: return True return test_set.run_tests(GlobalResolver()) def open_voting(self): self._reset_voting_statistics() self.finalize_startup() self.run_sim_voting_loot(self.voting_open_loot) if self.run_auto_voting_tests(self.initial_vote_test): for policy in self._balloted_policies: self.vote(policy, policy.get_initial_vote_count()) def close_voting(self): def get_most_voted_for_policy(policies): if not policies: return set() policy = max(policies, key=lambda policy: self.get_stat_value(policy.vote_count_statistic)) if self.get_stat_value(policy.vote_count_statistic) <= 0: return set() return set((policy,)) balloted_policies = self.get_balloted_policies() to_enact = get_most_voted_for_policy(balloted_policies) repealable_policies = self.get_up_for_repeal_policies() to_repeal = set() for policy in repealable_policies: if BaseCivicPolicyProvider.REPEAL_PETITION_THRESHOLD <= self.get_stat_value(policy.vote_count_statistic): to_repeal.add(policy) to_repeal -= to_enact to_enact -= self._enacted_policies self._enacted_policies -= to_repeal for policy in to_repeal: policy.repeal() to_enact_max_count = self.new_enact_max_count() while len(to_enact) > to_enact_max_count: to_enact.pop() self._enacted_policies.update(to_enact) for policy in to_enact: policy.enact() self._balloted_policies = set() self._up_for_repeal_policies = set() self._reset_voting_statistics() self.finalize_startup() self.run_sim_voting_loot(self.voting_close_loot) def get_schedule_text(self): return self.CIVIC_POLICY_SCHEDULE.schedule_text(self.CIVIC_POLICY_SCHEDULE.voting_open(), self.CIVIC_POLICY_SCHEDULE.voting_close()) def do_daily_vote(self): if self.run_auto_voting_tests(self.daily_random_vote_test): for policy in self._balloted_policies: self.vote(policy, policy.get_daily_vote_count()) @property def max_enacted_policy_count(self): raise NotImplementedError @property def max_balloted_policy_count(self): raise NotImplementedError @property def initial_balloted_policy_count(self): raise NotImplementedError @property def max_repealable_policy_count(self): raise NotImplementedError @property def open_close_voting_loot_resolver_list(self): raise NotImplementedError @classproperty def provider_type_id(cls): raise NotImplementedError def get_world_description_id(self): return 0 def is_eligible_voter(self, sim_info): raise NotImplementedError def is_new_policy_allowed(self, sim_info): return False def _select_balloted_policies(self): self._balloted_policies.clear() count_needed = self.initial_balloted_policy_count r = random.Random() dormant_policies = list(self.get_dormant_policies()) while dormant_policies: while len(self._balloted_policies) < count_needed: policy = r.choice(dormant_policies) dormant_policies.remove(policy) self._balloted_policies.add(policy) def finalize_startup(self): statistic_component = self.get_component(objects.components.types.STATISTIC_COMPONENT) statistic_component.on_finalize_load() if not self._civic_policies: return services.get_event_manager().unregister(self, BaseCivicPolicyProvider.CIVIC_POLICY_TEST_EVENTS) services.get_event_manager().register(self, BaseCivicPolicyProvider.CIVIC_POLICY_TEST_EVENTS) if not self._balloted_policies: self._select_balloted_policies() for policy in self._civic_policies: policy.finalize_startup() def stop_civic_policy_provider(self): services.get_event_manager().unregister(self, BaseCivicPolicyProvider.CIVIC_POLICY_TEST_EVENTS) def get_policy_instance_for_tuning(self, policy_guid64): for inst in self._civic_policies: if policy_guid64 == inst.guid64: return inst def enact(self, policy): policy = self.get_policy_instance_for_tuning(policy.guid64) if policy is None or policy in self._enacted_policies: return False if self.new_enact_max_count() == 0: return False self._enacted_policies.add(policy) self._balloted_policies.discard(policy) self._up_for_repeal_policies.discard(policy) policy.enact() return True def repeal(self, policy): policy = self.get_policy_instance_for_tuning(policy.guid64) if policy is None or policy not in self._enacted_policies: return False self._enacted_policies.discard(policy) self._up_for_repeal_policies.discard(policy) policy.repeal() return True def vote(self, policy, count=1, user_directed=False, lobby_interaction=False): policy_instance = self.get_policy_instance_for_tuning(policy.guid64) if policy_instance is None: return False return self.vote_by_instance(policy_instance, count, user_directed, lobby_interaction) def vote_by_instance(self, policy_instance, count=1, user_directed=False, lobby_interaction=False): if policy_instance.vote_count_statistic is not None: policy_list = None def get_current_rank(): policy_list.sort(key=lambda policy: (self.get_stat_value(policy.vote_count_statistic), policy.guid64), reverse=True) return policy_list.index(policy_instance) if user_directed: factor = 0 if policy_instance in self._balloted_policies: policy_list = list(self._balloted_policies) factor = 1 elif policy_instance in self._up_for_repeal_policies: policy_list = list(self._up_for_repeal_policies) factor = -1 orig_rank = get_current_rank() elif lobby_interaction: factor = 0 if policy_instance in self._balloted_policies: factor = 1 elif policy_instance in self._up_for_repeal_policies: factor = -1 value = self.get_stat_value(policy_instance.vote_count_statistic) + count self.set_stat_value(policy_instance.vote_count_statistic, value) services.street_service().update_community_board_tooltip(self) if user_directed: if policy_list is not None: with telemetry_helper.begin_hook(civic_policy_telemetry_writer, TELEMETRY_HOOK_CIVIC_POLICY_VOTE) as hook: hook.write_guid(TELEMETRY_FIELD_NEIGHBORHOOD, self.get_world_description_id()) hook.write_guid(TELEMETRY_FIELD_POLICY, policy_instance.guid64) hook.write_guid(TELEMETRY_FIELD_VOTES, factor*value) hook.write_guid(TELEMETRY_FIELD_PLAYER_VOTES, factor*count) hook.write_guid(TELEMETRY_FIELD_OLD_RANK, orig_rank) hook.write_guid(TELEMETRY_FIELD_NEW_RANK, get_current_rank()) if lobby_interaction: with telemetry_helper.begin_hook(civic_policy_telemetry_writer, TELEMETRY_HOOK_CIVIC_POLICY_LOBBY) as hook: hook.write_guid(TELEMETRY_FIELD_NEIGHBORHOOD, self.get_world_description_id()) hook.write_guid(TELEMETRY_FIELD_POLICY, policy_instance.guid64) hook.write_guid(TELEMETRY_FIELD_VOTES, factor*value) return True return False def _log_propose_telemetry(self, policy_instance, action): with telemetry_helper.begin_hook(civic_policy_telemetry_writer, TELEMETRY_HOOK_CIVIC_POLICY_PROPOSE) as hook: hook.write_guid(TELEMETRY_FIELD_NEIGHBORHOOD, self.get_world_description_id()) hook.write_guid(TELEMETRY_FIELD_POLICY, policy_instance.guid64) hook.write_guid(TELEMETRY_FIELD_PROPOSE_ACTION, action) def add_to_ballot(self, policy_instance): if policy_instance.vote_count_statistic is not None and policy_instance not in self._balloted_policies: self._balloted_policies.add(policy_instance) self._log_propose_telemetry(policy_instance, TELEMETRY_FIELD_ACTION_VALUE_BALLOT) return True return False def add_for_repeal(self, policy): policy = self.get_policy_instance_for_tuning(policy.guid64) if policy is None: return False if policy not in self._enacted_policies: return False if policy in self._up_for_repeal_policies: return False self._up_for_repeal_policies.add(policy) self._log_propose_telemetry(policy, TELEMETRY_FIELD_ACTION_VALUE_REPEAL) return True def remove_from_repeal(self, policy): policy = self.get_policy_instance_for_tuning(policy.guid64) if policy is None: return False if policy not in self._up_for_repeal_policies: return False self._up_for_repeal_policies.discard(policy) self._log_propose_telemetry(policy, TELEMETRY_FIELD_ACTION_VALUE_CANCEL_REPEAL) return True def save(self, parent_data_msg): parent_data_msg.ClearField('policy_data') for policy in self._civic_policies: policy.save(parent_data_msg) parent_data_msg.ClearField('balloted_policy_ids') for policy in self._balloted_policies: parent_data_msg.balloted_policy_ids.append(policy.guid64) parent_data_msg.ClearField('up_for_repeal_policy_ids') for policy in self._up_for_repeal_policies: parent_data_msg.up_for_repeal_policy_ids.append(policy.guid64) parent_data_msg.ClearField('commodity_tracker') parent_data_msg.ClearField('statistics_tracker') parent_data_msg.ClearField('ranked_statistic_tracker') self.update_all_commodities() (commodites, _, ranked_statistics) = self.commodity_tracker.save() parent_data_msg.commodity_tracker.commodities.extend(commodites) regular_statistics = self.statistic_tracker.save() parent_data_msg.statistics_tracker.statistics.extend(regular_statistics) parent_data_msg.ranked_statistic_tracker.ranked_statistics.extend(ranked_statistics) def load(self, parent_data_msg): self.commodity_tracker.load(parent_data_msg.commodity_tracker.commodities) self.statistic_tracker.load(parent_data_msg.statistics_tracker.statistics) self.commodity_tracker.load(parent_data_msg.ranked_statistic_tracker.ranked_statistics) self._enacted_policies.clear() for policy_data in parent_data_msg.policy_data: policy = self.get_policy_instance_for_tuning(policy_data.policy_id) if policy: policy.load(policy_data) if policy.enacted: self._enacted_policies.add(policy) for policy_id in parent_data_msg.balloted_policy_ids: policy = self.get_policy_instance_for_tuning(policy_id) if policy: self._balloted_policies.add(policy) for policy_id in parent_data_msg.up_for_repeal_policy_ids: policy = self.get_policy_instance_for_tuning(policy_id) if policy: self._up_for_repeal_policies.add(policy) def handle_event(self, sim_info, event, resolver): if event == TestEvent.CivicPolicyDailyRandomVoting: self.do_daily_vote() elif event == TestEvent.CivicPolicyOpenVoting: self.open_voting() elif event == TestEvent.CivicPolicyCloseVoting: self.close_voting() def get_influence(self, sim_info): tracker = BucksUtils.get_tracker_for_bucks_type(self.INFLUENCE_BUCK_TYPE, owner_id=sim_info.id, add_if_none=False) if tracker is None: return 0 return tracker.get_bucks_amount_for_type(self.INFLUENCE_BUCK_TYPE) def modify_influence(self, sim_info, delta): if delta == 0: return tracker = BucksUtils.get_tracker_for_bucks_type(self.INFLUENCE_BUCK_TYPE, owner_id=sim_info.id, add_if_none=True) if tracker is None: return tracker.try_modify_bucks(self.INFLUENCE_BUCK_TYPE, delta) def populate_community_board_op(self, sim_info, op, target_id): op.sim_id = sim_info.id op.target_id = target_id op.influence_points = self.get_influence(sim_info) op.title = self.community_board_dialog_title() if hasattr(op, 'schedule_text'): op.schedule_text = self.get_schedule_text() for policy in self._enacted_policies: with ProtocolBufferRollback(op.enacted_policies) as enacted_policy: enacted_policy.policy_id = policy.guid64 if policy in self._up_for_repeal_policies: if policy.vote_count_statistic is None: enacted_policy.count = 0 else: enacted_policy.count = int(self.get_stat_value(policy.vote_count_statistic)) for policy in self._balloted_policies: with ProtocolBufferRollback(op.balloted_policies) as balloted_policy: balloted_policy.policy_id = policy.guid64 stat = policy.vote_count_statistic if stat is None: balloted_policy.count = 0 else: balloted_policy.count = int(self.get_stat_value(stat)) balloted_policy.max_count = stat.max_value op.provider_type = self.provider_type_id op.new_policy_allowed = self.is_new_policy_allowed(sim_info) if not services.street_service().voting_open: op.policy_disabled_tooltip = self.COMMUNITY_BOARD_TEXT.voting_closed_policy_tooltip_text() if not self.is_eligible_voter(sim_info): op.disabled_tooltip = self.COMMUNITY_BOARD_TEXT.ineligible_voter_confirm_tooltip_text(sim_info) op.policy_disabled_tooltip = self.COMMUNITY_BOARD_TEXT.ineligible_voter_policy_tooltip_text(sim_info) def _on_add_picker_selected(self, dialog): tag_objs = dialog.get_result_tags() if not tag_objs: return num_tags = len(tag_objs) can_add_more = dialog.max_selectable.number_selectable - num_tags > 0 if can_add_more: can_add_more = len(dialog.picker_rows) > num_tags op = CommunityBoardAddPolicy(tag_objs, dialog.target_sim.sim_id, can_add_more) Distributor.instance().add_op_with_no_owner(op) def create_add_policy_picker(self, sim_info, used_policy_ids): resolver = SingleSimResolver(sim_info) dialog = self.COMMUNITY_BOARD_TEXT.add_policy_picker(sim_info, resolver=resolver) for policy in self.get_dormant_policies(): if policy.guid64 not in used_policy_ids: tooltip = lambda *_, tooltip=policy.display_description: tooltip(sim_info) dialog.add_row(BasePickerRow(name=policy.display_name(sim_info), icon=policy.display_icon, tag=policy.guid64, row_tooltip=tooltip)) dialog.max_selectable.number_selectable = min(len(dialog.picker_rows), self.max_balloted_policy_count - len(self._balloted_policies) - len(used_policy_ids)) dialog.set_target_sim(sim_info) dialog.add_listener(self._on_add_picker_selected) dialog.show_dialog() def handle_vote_interaction(self, sim_info, target_id, push_continuation): sim = sim_info.get_sim_instance(allow_hidden_flags=ALL_HIDDEN_REASONS) if sim is None: return target_object = services.object_manager().get(target_id) if not target_object: return for current_interaction in sim.si_state: interaction_target = current_interaction.target if interaction_target is None: continue if interaction_target.is_part: if interaction_target.part_owner is target_object: target_object = interaction_target break if interaction_target is target_object: break else: return if push_continuation: context = current_interaction.context.clone_for_continuation(current_interaction) autonomy_request = autonomy.autonomy_request.AutonomyRequest(sim, FullAutonomy, static_commodity_list=self.VOTING_CONTINUATION_AUTONOMY_COMMODITIES, object_list=(target_object,), insert_strategy=QueueInsertStrategy.NEXT, apply_opportunity_cost=False, is_script_request=True, ignore_user_directed_and_autonomous=True, context=context, si_state_view=sim.si_state, limited_autonomy_allowed=True, autonomy_mode_label_override='ParameterizedAutonomy', off_lot_autonomy_rule_override=UNLIMITED_AUTONOMY_RULE) autonomy_service = services.autonomy_service() results = autonomy_service.score_all_interactions(autonomy_request) chosen_interaction = autonomy_service.choose_best_interaction(results, autonomy_request) autonomy_request.invalidate_created_interactions(excluded_si=chosen_interaction) if chosen_interaction: target_affordance = current_interaction.generate_continuation_affordance(chosen_interaction.affordance) sim.push_super_affordance(target_affordance, target_object, context) current_interaction.cancel(FinishingType.NATURAL, 'Finished viewing board')
class Skill(HasTunableReference, ProgressiveStatisticCallbackMixin, statistics.continuous_statistic_tuning.TunedContinuousStatistic, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(sims4.resources.Types.STATISTIC)): SKILL_LEVEL_LIST = TunableMapping(description='\n A mapping defining the level boundaries for each skill type.\n ', key_type=SkillLevelType, value_type=TunableList(description='\n The level boundaries for skill type, specified as a delta from the\n previous value.\n ', tunable=Tunable(tunable_type=int, default=0)), tuple_name='SkillLevelListMappingTuple', export_modes=ExportModes.All) SKILL_EFFECTIVENESS_GAIN = TunableMapping(description='\n Skill gain points based on skill effectiveness.\n ', key_type=SkillEffectiveness, value_type=TunableCurve()) DYNAMIC_SKILL_INTERVAL = TunableRange(description='\n Interval used when dynamic loot is used in a\n PeriodicStatisticChangeElement.\n ', tunable_type=float, default=1, minimum=1) INSTANCE_TUNABLES = {'stat_name': TunableLocalizedString(description='\n The name of this skill.\n ', export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'skill_description': TunableLocalizedString(description="\n The skill's normal description.\n ", export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'locked_description': TunableLocalizedString(description="\n The skill description when it's locked.\n ", allow_none=True, export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'icon': TunableIcon(description='\n Icon to be displayed for the Skill.\n ', export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'tooltip_icon_list': TunableList(description='\n A list of icons to show in the tooltip of this\n skill.\n ', tunable=TunableIcon(description='\n Icon that is displayed what types of objects help\n improve this skill.\n '), export_modes=(ExportModes.ClientBinary,), tuning_group=GroupNames.UI), 'tutorial': TunableReference(description='\n Tutorial instance for this skill. This will be used to bring up the\n skill lesson from the first notification for Sim to know this skill.\n ', manager=services.get_instance_manager(sims4.resources.Types.TUTORIAL), allow_none=True, class_restrictions=('Tutorial',), tuning_group=GroupNames.UI), 'priority': Tunable(description="\n Skill priority. Higher priority skills will trump other skills when\n being displayed on the UI. When a Sim gains multiple skills at the\n same time, only the highest priority one will display a progress bar\n over the Sim's head.\n ", tunable_type=int, default=1, export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'next_level_teaser': TunableList(description='\n Tooltip which describes what the next level entails.\n ', tunable=TunableLocalizedString(), export_modes=(ExportModes.ClientBinary,), tuning_group=GroupNames.UI), 'mood_id': TunableReference(description='\n When this mood is set and active sim matches mood, the UI will\n display a special effect on the skill bar to represent that this\n skill is getting a bonus because of the mood.\n ', manager=services.mood_manager(), allow_none=True, export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'stat_asm_param': TunableStatAsmParam.TunableFactory(tuning_group=GroupNames.ANIMATION), 'hidden': Tunable(description='\n If checked, this skill will be hidden.\n ', tunable_type=bool, default=False, export_modes=ExportModes.All, tuning_group=GroupNames.AVAILABILITY), 'update_client_for_npcs': Tunable(description="\n Whether this skill will send update messages to the client\n for non-active household sims (NPCs).\n \n e.g. A toddler's communication skill determines the VOX they use, so\n the client needs to know the skill level for all toddlers in order\n for this work properly.\n ", tunable_type=bool, default=False, tuning_group=GroupNames.UI), 'is_default': Tunable(description='\n Whether Sim will default has this skill.\n ', tunable_type=bool, default=False, tuning_group=GroupNames.AVAILABILITY), 'ages': TunableSet(description='\n Allowed ages for this skill.\n ', tunable=TunableEnumEntry(tunable_type=Age, default=Age.ADULT, export_modes=ExportModes.All), tuning_group=GroupNames.AVAILABILITY), 'ad_data': TunableList(description='\n A list of Vector2 points that define the desire curve for this\n commodity.\n ', tunable=TunableVector2(description='\n Point on a Curve\n ', default=sims4.math.Vector2(0, 0)), tuning_group=GroupNames.AUTONOMY), 'weight': Tunable(description="\n The weight of the Skill with regards to autonomy. It's ignored for\n the purposes of sorting stats, but it's applied when scoring the\n actual statistic operation for the SI.\n ", tunable_type=float, default=0.5, tuning_group=GroupNames.AUTONOMY), 'statistic_multipliers': TunableMapping(description='\n Multipliers this skill applies to other statistics based on its\n value.\n ', key_type=TunableReference(description='\n The statistic this multiplier will be applied to.\n ', manager=services.statistic_manager(), reload_dependent=True), value_type=TunableTuple(curve=TunableCurve(description='\n Tunable curve where the X-axis defines the skill level, and\n the Y-axis defines the associated multiplier.\n ', x_axis_name='Skill Level', y_axis_name='Multiplier'), direction=TunableEnumEntry(description="\n Direction where the multiplier should work on the\n statistic. For example, a tuned decrease for an object's\n brokenness rate will not also increase the time it takes to\n repair it.\n ", tunable_type=StatisticChangeDirection, default=StatisticChangeDirection.INCREASE), use_effective_skill=Tunable(description='\n If checked, this modifier will look at the current\n effective skill value. If unchecked, this modifier will\n look at the actual skill value.\n ', tunable_type=bool, needs_tuning=True, default=True)), tuning_group=GroupNames.MULTIPLIERS), 'success_chance_multipliers': TunableList(description='\n Multipliers this skill applies to the success chance of\n affordances.\n ', tunable=TunableSkillMultiplier(), tuning_group=GroupNames.MULTIPLIERS), 'monetary_payout_multipliers': TunableList(description='\n Multipliers this skill applies to the monetary payout amount of\n affordances.\n ', tunable=TunableSkillMultiplier(), tuning_group=GroupNames.MULTIPLIERS), 'tags': TunableList(description='\n The associated categories of the skill\n ', tunable=TunableEnumEntry(tunable_type=tag.Tag, default=tag.Tag.INVALID, pack_safe=True), tuning_group=GroupNames.CORE), 'skill_level_type': TunableEnumEntry(description='\n Skill level list to use.\n ', tunable_type=SkillLevelType, default=SkillLevelType.MAJOR, export_modes=ExportModes.All, tuning_group=GroupNames.CORE), 'level_data': TunableMapping(description='\n Level-specific information, such as notifications to be displayed to\n level up.\n ', key_type=int, value_type=TunableTuple(level_up_notification=UiDialogNotification.TunableFactory(description='\n The notification to display when the Sim obtains this level.\n The text will be provided two tokens: the Sim owning the\n skill and a number representing the 1-based skill level\n ', locked_args={'text_tokens': DEFAULT, 'icon': None, 'primary_icon_response': UiDialogResponse(text=None, ui_request=UiDialogResponse.UiDialogUiRequest.SHOW_SKILL_PANEL), 'secondary_icon': None}), level_up_screen_slam=OptionalTunable(description='\n Screen slam to show when reaches this skill level.\n Localization Tokens: Sim - {0.SimFirstName}, Skill Name - \n {1.String}, Skill Number - {2.Number}\n ', tunable=ui.screen_slam.TunableScreenSlamSnippet()), skill_level_buff=OptionalTunable(tunable=TunableReference(description='\n The buff to place on a Sim when they reach this specific\n level of skill.\n ', manager=services.buff_manager())), rewards=TunableList(description='\n A reward to give for achieving this level.\n ', tunable=rewards.reward_tuning.TunableSpecificReward(pack_safe=True)), loot=TunableList(description='\n A loot to apply for achieving this level.\n ', tunable=TunableReference(manager=services.get_instance_manager(sims4.resources.Types.ACTION), class_restrictions=('LootActions',))), super_affordances=TunableSet(description='\n Super affordances this adds to the Sim.\n ', tunable=TunableReference(description='\n A super affordance added to this Sim.\n ', manager=services.get_instance_manager(sims4.resources.Types.INTERACTION), class_restrictions=('SuperInteraction',), pack_safe=True)), target_super_affordances=TunableProvidedAffordances(description='\n Super affordances this adds to the target.\n ', locked_args={'target': ParticipantType.Object, 'carry_target': ParticipantType.Invalid, 'is_linked': False, 'unlink_if_running': False}), actor_mixers=TunableMapping(description='\n Mixers this adds to an associated actor object. (When targeting\n something else.)\n ', key_type=TunableReference(description='\n The super affordance these mixers are associated with.\n ', manager=services.get_instance_manager(sims4.resources.Types.INTERACTION), class_restrictions=('SuperInteraction',), pack_safe=True), value_type=TunableSet(description='\n Set of mixer affordances associated with the super affordance.\n ', tunable=TunableReference(description='\n Linked mixer affordance.\n ', manager=services.get_instance_manager(sims4.resources.Types.INTERACTION), category='asm', class_restrictions=('MixerInteraction',), pack_safe=True)))), tuning_group=GroupNames.CORE), 'age_up_skill_transition_data': OptionalTunable(description='\n Data used to modify the value of a new skill based on the level\n of this skill.\n \n e.g. Toddler Communication skill transfers into Child Social skill.\n ', tunable=TunableTuple(new_skill=TunablePackSafeReference(description='\n The new skill.\n ', manager=services.get_instance_manager(sims4.resources.Types.STATISTIC)), skill_data=TunableMapping(description="\n A mapping between this skill's levels and the\n new skill's internal value.\n \n The keys are user facing skill levels.\n \n The values are the internal statistic value, not the user\n facing skill level.\n ", key_type=Tunable(description="\n This skill's level.\n \n This is the actual user facing skill level.\n ", tunable_type=int, default=0), value_type=Tunable(description='\n The new skill\'s value.\n \n This is the internal statistic\n value, not the user facing skill level."\n ', tunable_type=int, default=0))), tuning_group=GroupNames.SPECIAL_CASES), 'skill_unlocks_on_max': TunableList(description='\n A list of skills that become unlocked when this skill is maxed.\n ', tunable=TunableReference(description='\n A skill to unlock.\n ', manager=services.get_instance_manager(sims4.resources.Types.STATISTIC), class_restrictions=('Skill',), pack_safe=True), tuning_group=GroupNames.SPECIAL_CASES), 'trend_tag': OptionalTunable(description='\n If enabled, we associate this skill with a particular trend via tag\n which you can find in trend_tuning.\n ', tunable=TunableTag(description='\n The trend tag we associate with this skill\n ', filter_prefixes=('func_trend',)))} REMOVE_INSTANCE_TUNABLES = ('min_value_tuning', 'max_value_tuning', 'decay_rate', '_default_convergence_value') def __init__(self, tracker): self._skill_level_buff = None super().__init__(tracker, self.initial_value) self._delta_enabled = True self._max_level_update_sent = False @classmethod def _tuning_loaded_callback(cls): super()._tuning_loaded_callback() level_list = cls.get_level_list() cls.max_level = len(level_list) cls.min_value_tuning = 0 cls.max_value_tuning = sum(level_list) cls._default_convergence_value = cls.min_value_tuning cls._build_utility_curve_from_tuning_data(cls.ad_data) for stat in cls.statistic_multipliers: multiplier = cls.statistic_multipliers[stat] curve = multiplier.curve direction = multiplier.direction use_effective_skill = multiplier.use_effective_skill stat.add_skill_based_statistic_multiplier(cls, curve, direction, use_effective_skill) for multiplier in cls.success_chance_multipliers: curve = multiplier.curve use_effective_skill = multiplier.use_effective_skill for affordance in multiplier.affordance_list: affordance.add_skill_multiplier(affordance.success_chance_multipliers, cls, curve, use_effective_skill) for multiplier in cls.monetary_payout_multipliers: curve = multiplier.curve use_effective_skill = multiplier.use_effective_skill for affordance in multiplier.affordance_list: affordance.add_skill_multiplier(affordance.monetary_payout_multipliers, cls, curve, use_effective_skill) @classmethod def _verify_tuning_callback(cls): success_multiplier_affordances = [] for multiplier in cls.success_chance_multipliers: success_multiplier_affordances.extend(multiplier.affordance_list) if len(success_multiplier_affordances) != len(set(success_multiplier_affordances)): logger.error("The same affordance has been tuned more than once under {}'s success multipliers, and they will overwrite each other. Please fix in tuning.", cls, owner='tastle') monetary_payout_multiplier_affordances = [] for multiplier in cls.monetary_payout_multipliers: monetary_payout_multiplier_affordances.extend(multiplier.affordance_list) if len(monetary_payout_multiplier_affordances) != len(set(monetary_payout_multiplier_affordances)): logger.error("The same affordance has been tuned more than once under {}'s monetary payout multipliers, and they will overwrite each other. Please fix in tuning.", cls, owner='tastle') @classproperty def skill_type(cls): return cls @constproperty def is_skill(): return True @classproperty def autonomy_weight(cls): return cls.weight @constproperty def remove_on_convergence(): return False @classproperty def valid_for_stat_testing(cls): return True @classmethod def can_add(cls, owner, force_add=False, **kwargs): if force_add: return True if owner.age not in cls.ages: return False return super().can_add(owner, **kwargs) @classmethod def convert_to_user_value(cls, value): level_list = cls.get_level_list() if not level_list: return 0 current_value = value for (level, level_threshold) in enumerate(level_list): current_value -= level_threshold if current_value < 0: return level return level + 1 @classmethod def convert_from_user_value(cls, user_value): (level_min, _) = cls._get_level_bounds(user_value) return level_min @classmethod def create_skill_update_msg(cls, sim_id, stat_value): skill_msg = Commodities_pb2.Skill_Update() skill_msg.skill_id = cls.guid64 skill_msg.curr_points = int(stat_value) skill_msg.sim_id = sim_id return skill_msg @classmethod def get_level_list(cls): return cls.SKILL_LEVEL_LIST.get(cls.skill_level_type) @classmethod def get_skill_effectiveness_points_gain(cls, effectiveness_level, level): skill_gain_curve = cls.SKILL_EFFECTIVENESS_GAIN.get(effectiveness_level) if skill_gain_curve is not None: return skill_gain_curve.get(level) logger.error('{} does not exist in SKILL_EFFECTIVENESS_GAIN mapping', effectiveness_level) return 0 def _get_level_data_for_skill_level(self, skill_level): level_data = self.level_data.get(skill_level) if level_data is None: logger.debug('No level data found for skill [{}] at level [{}].', self, skill_level) return level_data @property def is_initial_value(self): return self.initial_value == self.get_value() def should_send_update(self, sim_info, stat_value): if sim_info.is_npc and not self.update_client_for_npcs: return False if self.hidden: return False if Skill.convert_to_user_value(stat_value) == 0: return False if self.reached_max_level: if self._max_level_update_sent: return False self._max_level_update_sent = True return True def on_initial_startup(self): super().on_initial_startup() skill_level = self.get_user_value() self._update_skill_level_buff(skill_level) def on_add(self): super().on_add() self._tracker.owner.add_modifiers_for_skill(self) level_data = self._get_level_data_for_skill_level(self.get_user_value()) if level_data is not None: provided_affordances = [] for provided_affordance in level_data.target_super_affordances: provided_affordance_data = ProvidedAffordanceData(provided_affordance.affordance, provided_affordance.object_filter, provided_affordance.allow_self) provided_affordances.append(provided_affordance_data) self._tracker.add_to_affordance_caches(level_data.super_affordances, provided_affordances) self._tracker.add_to_actor_mixer_cache(level_data.actor_mixers) sim = self._tracker._owner.get_sim_instance() apply_super_affordance_commodity_flags(sim, self, level_data.super_affordances) def on_remove(self, on_destroy=False): super().on_remove(on_destroy=on_destroy) self._destory_callback_handle() if not on_destroy: self._send_skill_delete_message() if self._skill_level_buff is not None: self._tracker.owner.remove_buff(self._skill_level_buff) self._skill_level_buff = None if not on_destroy: self._tracker.update_affordance_caches() sim = self._tracker._owner.get_sim_instance() remove_super_affordance_commodity_flags(sim, self) def on_zone_load(self): self._max_level_update_sent = False def _apply_multipliers_to_continuous_statistics(self): for stat in self.statistic_multipliers: if stat.continuous: owner_stat = self.tracker.get_statistic(stat) if owner_stat is not None: owner_stat._recalculate_modified_decay_rate() @classproperty def default_value(cls): return cls.initial_value @flexmethod @caches.cached def get_user_value(cls, inst): inst_or_cls = inst if inst is not None else cls return super(__class__, inst_or_cls).get_user_value() def _clear_user_value_cache(self): self.get_user_value.func.cache.clear() def set_value(self, value, *args, from_load=False, interaction=None, **kwargs): old_value = self.get_value() super().set_value(value, *args, **kwargs) if not caches.skip_cache: self._clear_user_value_cache() if from_load: return event_manager = services.get_event_manager() sim_info = self._tracker._owner new_value = self.get_value() new_level = self.convert_to_user_value(value) if old_value == self.initial_value or old_value != new_value: event_manager.process_event(test_events.TestEvent.SkillValueChange, sim_info=sim_info, skill=self, statistic=self.stat_type, custom_keys=(self.stat_type,)) old_level = self.convert_to_user_value(old_value) if old_level < new_level or old_value == self.initial_value: self._apply_multipliers_to_continuous_statistics() event_manager.process_event(test_events.TestEvent.SkillLevelChange, sim_info=sim_info, skill=self, new_level=new_level, custom_keys=(self.stat_type,)) def add_value(self, add_amount, interaction=None, **kwargs): old_value = self.get_value() if old_value == self.initial_value: telemhook = TELEMETRY_HOOK_SKILL_INTERACTION_FIRST_TIME else: telemhook = TELEMETRY_HOOK_SKILL_INTERACTION super().add_value(add_amount, interaction=interaction) if not caches.skip_cache: self._clear_user_value_cache() if interaction is not None: interaction_name = interaction.affordance.__name__ else: interaction_name = TELEMETRY_INTERACTION_NOT_AVAILABLE self.on_skill_updated(telemhook, old_value, self.get_value(), interaction_name) def _update_value(self): old_value = self._value if gsi_handlers.sim_handlers_log.skill_change_archiver.enabled: last_update = self._last_update time_delta = super()._update_value() if not caches.skip_cache: self._clear_user_value_cache() new_value = self._value if old_value < new_value: event_manager = services.get_event_manager() sim_info = self._tracker._owner if self._tracker is not None else None if old_value == self.initial_value: telemhook = TELEMETRY_HOOK_SKILL_INTERACTION_FIRST_TIME self.on_skill_updated(telemhook, old_value, new_value, TELEMETRY_INTERACTION_NOT_AVAILABLE) event_manager.process_event(test_events.TestEvent.SkillValueChange, sim_info=sim_info, skill=self, statistic=self.stat_type, custom_keys=(self.stat_type,)) old_level = self.convert_to_user_value(old_value) new_level = self.convert_to_user_value(new_value) if gsi_handlers.sim_handlers_log.skill_change_archiver.enabled and self.tracker.owner.is_sim: gsi_handlers.sim_handlers_log.archive_skill_change(self.tracker.owner, self, time_delta, old_value, new_value, new_level, last_update) if old_level < new_level or old_value == self.initial_value: if self._tracker is not None: self._tracker.notify_watchers(self.stat_type, self._value, self._value) event_manager.process_event(test_events.TestEvent.SkillLevelChange, sim_info=sim_info, skill=self, new_level=new_level, custom_keys=(self.stat_type,)) def _on_statistic_modifier_changed(self, notify_watcher=True): super()._on_statistic_modifier_changed(notify_watcher=notify_watcher) if not self.reached_max_level: return event_manager = services.get_event_manager() sim_info = self._tracker._owner if self._tracker is not None else None event_manager.process_event(test_events.TestEvent.SkillValueChange, sim_info=sim_info, skill=self, statistic=self.stat_type, custom_keys=(self.stat_type,)) def on_skill_updated(self, telemhook, old_value, new_value, affordance_name): owner_sim_info = self._tracker._owner if owner_sim_info.is_selectable: with telemetry_helper.begin_hook(skill_telemetry_writer, telemhook, sim_info=owner_sim_info) as hook: hook.write_guid(TELEMETRY_FIELD_SKILL_ID, self.guid64) hook.write_string(TELEMETRY_FIELD_SKILL_AFFORDANCE, affordance_name) hook.write_bool(TELEMETRY_FIELD_SKILL_AFFORDANCE_SUCCESS, True) hook.write_int(TELEMETRY_FIELD_SKILL_AFFORDANCE_VALUE_ADD, new_value - old_value) if old_value == self.initial_value: skill_level = self.convert_to_user_value(old_value) self._handle_skill_up(skill_level) def _send_skill_delete_message(self): if self.tracker.owner.is_npc: return skill_msg = Commodities_pb2.SkillDelete() skill_msg.skill_id = self.guid64 op = GenericProtocolBufferOp(Operation.SIM_SKILL_DELETE, skill_msg) Distributor.instance().add_op(self.tracker.owner, op) @staticmethod def _callback_handler(stat_inst): new_level = stat_inst.get_user_value() old_level = new_level - 1 stat_inst.on_skill_level_up(old_level, new_level) stat_inst.refresh_threshold_callback() def _handle_skill_up(self, skill_level): self._show_level_notification(skill_level) self._update_skill_level_buff(skill_level) self._try_give_skill_up_payout(skill_level) self._tracker.update_affordance_caches() sim = self._tracker._owner.get_sim_instance() remove_super_affordance_commodity_flags(sim, self) super_affordances = tuple(self._tracker.get_cached_super_affordances_gen()) apply_super_affordance_commodity_flags(sim, self, super_affordances) def _recalculate_modified_decay_rate(self): pass def refresh_level_up_callback(self): self._destory_callback_handle() def _on_level_up_callback(stat_inst): new_level = stat_inst.get_user_value() old_level = new_level - 1 stat_inst.on_skill_level_up(old_level, new_level) stat_inst.refresh_level_up_callback() self._callback_handle = self.create_and_add_callback_listener(Threshold(self._get_next_level_bound(), operator.ge), _on_level_up_callback) def on_skill_level_up(self, old_level, new_level): tracker = self.tracker sim_info = tracker._owner if self.reached_max_level: for skill in self.skill_unlocks_on_max: skill_instance = tracker.add_statistic(skill, force_add=True) skill_instance.set_value(skill.initial_value) with telemetry_helper.begin_hook(skill_telemetry_writer, TELEMETRY_HOOK_SKILL_LEVEL_UP, sim_info=sim_info) as hook: hook.write_guid(TELEMETRY_FIELD_SKILL_ID, self.guid64) hook.write_int(TELEMETRY_FIELD_SKILL_LEVEL, new_level) self._handle_skill_up(new_level) services.get_event_manager().process_event(test_events.TestEvent.SkillValueChange, sim_info=sim_info, statistic=self.stat_type, custom_keys=(self.stat_type,)) def _show_level_notification(self, skill_level, ignore_npc_check=False): sim_info = self._tracker._owner if not (ignore_npc_check or not sim_info.is_npc): if skill_level == 1: tutorial_service = services.get_tutorial_service() if tutorial_service is not None and tutorial_service.is_tutorial_running(): return level_data = self._get_level_data_for_skill_level(skill_level) if level_data is not None: tutorial_id = None if self.tutorial is not None: if skill_level == 1: tutorial_id = self.tutorial.guid64 notification = level_data.level_up_notification(sim_info, resolver=SingleSimResolver(sim_info)) notification.show_dialog(icon_override=IconInfoData(icon_resource=self.icon), secondary_icon_override=IconInfoData(obj_instance=sim_info), additional_tokens=(skill_level,), tutorial_id=tutorial_id) if level_data.level_up_screen_slam is not None: level_data.level_up_screen_slam.send_screen_slam_message(sim_info, sim_info, self.stat_name, skill_level) def _update_skill_level_buff(self, skill_level): level_data = self._get_level_data_for_skill_level(skill_level) new_buff = level_data.skill_level_buff if level_data is not None else None if self._skill_level_buff is not None: self._tracker.owner.remove_buff(self._skill_level_buff) self._skill_level_buff = None if new_buff is not None: self._skill_level_buff = self._tracker.owner.add_buff(new_buff) def _try_give_skill_up_payout(self, skill_level): level_data = self._get_level_data_for_skill_level(skill_level) if level_data is None: return if level_data.rewards: for reward in level_data.rewards: reward().open_reward(self._tracker.owner, reward_destination=RewardDestination.SIM, reward_source=self) if level_data.loot: resolver = SingleSimResolver(self._tracker.owner) for loot in level_data.loot: loot.apply_to_resolver(resolver) def force_show_level_notification(self, skill_level): self._show_level_notification(skill_level, ignore_npc_check=True) @classmethod def send_commodity_update_message(cls, sim_info, old_value, new_value): stat_instance = sim_info.get_statistic(cls.stat_type, add=False) if stat_instance is None or not stat_instance.should_send_update(sim_info, new_value): return msg = cls.create_skill_update_msg(sim_info.id, new_value) add_object_message(sim_info, MSG_SIM_SKILL_UPDATE, msg, False) change_rate = stat_instance.get_change_rate() hide_progress_bar = False if sim_info.is_npc or sim_info.is_skill_bar_suppressed(): hide_progress_bar = True op = distributor.ops.SkillProgressUpdate(cls.guid64, change_rate, new_value, hide_progress_bar) distributor.ops.record(sim_info, op) def save_statistic(self, commodities, skills, ranked_stats, tracker): current_value = self.get_saved_value() if current_value == self.initial_value: return message = protocols.Skill() message.name_hash = self.guid64 message.value = current_value if self._time_of_last_value_change: message.time_of_last_value_change = self._time_of_last_value_change.absolute_ticks() skills.append(message) def unlocks_skills_on_max(self): return True def can_decay(self): return False def get_skill_provided_affordances(self): level_data = self._get_level_data_for_skill_level(self.get_user_value()) if level_data is None: return ((), ()) return (level_data.super_affordances, level_data.target_super_affordances) def get_skill_provided_actor_mixers(self): level_data = self._get_level_data_for_skill_level(self.get_user_value()) if level_data is None: return return level_data.actor_mixers def get_actor_mixers(self, super_interaction): level_data = self._get_level_data_for_skill_level(self.get_user_value()) if level_data is None: return [] mixers = level_data.actor_mixers.get(super_interaction, tuple()) if level_data is not None else [] return mixers @flexmethod def populate_localization_token(cls, inst, token): inst_or_cls = inst if inst is not None else cls token.type = LocalizedStringToken.STRING token.text_string = inst_or_cls.stat_name
class FishingTuning: BAIT_TAG_DATA_MAP = TunableMapping( description= '\n Mapping between fishing bait tag and fishing bait data.\n ', key_type=TunableTag( description= '\n The bait tag to which we want to map a bait data.\n ', filter_prefixes=('func_bait', )), key_name='Bait Tag', value_type=TunableFishingBaitReference( description='\n The bait data.\n ', pack_safe=True), value_name='Bait Data') @staticmethod def get_fishing_bait_data(obj_def): bait_data = None for (tag, data) in FishingTuning.BAIT_TAG_DATA_MAP.items(): if obj_def.has_build_buy_tag(tag): if not bait_data is None: if bait_data.bait_priority < data.bait_priority: bait_data = data bait_data = data return bait_data @staticmethod def get_fishing_bait_data_set(obj_def_ids): if obj_def_ids is None: return frozenset() definition_manager = services.definition_manager() bait_data_guids = set() for def_id in obj_def_ids: bait_def = definition_manager.get(def_id) if bait_def is None: continue bait_data = FishingTuning.get_fishing_bait_data(bait_def) if bait_data is None: logger.error( 'Object {} failed trying to get fishing bait data category. Make sure the object has bait category tag.', bait_def) else: bait_data_guids.add(bait_data.guid64) return bait_data_guids @staticmethod def get_fishing_bait_description(obj): bait_data = FishingTuning.get_fishing_bait_data(obj.definition) if bait_data is not None: return bait_data.bait_description() @staticmethod def add_bait_notebook_entry(sim, created_fish, bait): if sim.sim_info.notebook_tracker is None: return sub_entries = None if bait: bait_data = FishingTuning.get_fishing_bait_data(bait.definition) if bait_data is not None: sub_entries = (SubEntryData(bait_data.guid64, True), ) sim.sim_info.notebook_tracker.unlock_entry( NotebookCustomTypeTuning.BAIT_NOTEBOOK_ENTRY( created_fish.definition.id, sub_entries=sub_entries))
class SetFavoriteLootOp(BaseTargetedLootOperation): FACTORY_TUNABLES = {'favorite_type': TunableVariant(description="\n The type of favorite action to apply.\n \n Preferred Object: Sets the object as a sim's preferred object\n to use for a specific func tag.\n Favorite Stack: Sets the object's stack of the sim's favorites\n in their inventory.\n ", preferred_object=TunableTuple(description='\n Data for setting this item as preferred.\n ', tag=TunableTag(description='\n The tag that represents this type of favorite.\n ', filter_prefixes=('Func',))), locked_args={'favorite_stack': None}, default='preferred_object'), 'unset': Tunable(description='\n If checked, this will unset the target as the favorite instead of setting\n it.\n ', tunable_type=bool, default=False)} def __init__(self, favorite_type, unset, **kwargs): super().__init__(**kwargs) self._favorite_type = favorite_type self._unset = unset def _apply_to_subject_and_target(self, subject, target, resolver): if subject is None or target is None: logger.error('Trying to run a SetFavorite loot without a valid Subject or Target') return if target.is_sim: logger.error("Trying to set a Sim {} as a Favorite of another Sim {}. This isn't possible.", target, subject) return favorites_tracker = subject.sim_info.favorites_tracker if favorites_tracker is None: logger.error('Trying to set a favorite for Sim {} but they have no favorites tracker.', subject) return if self._favorite_type is not None: if self._unset: favorites_tracker.unset_favorite(self._favorite_type.tag, target.id, target.definition.id) else: favorites_tracker.set_favorite(self._favorite_type.tag, target.id, target.definition.id) return if self._unset: favorites_tracker.unset_favorite_stack(target) else: favorites_tracker.set_favorite_stack(target) target.inventoryitem_component.get_inventory().push_inventory_item_stack_update_msg(target)
class InFootprintTest(HasTunableSingletonFactory, AutoFactoryInit, event_testing.test_base.BaseTest): BY_PARTICIPANT = 0 BY_TAG = 1 test_events = () FACTORY_TUNABLES = { 'actor': TunableEnumEntry( description= '\n The actor whose location will be used.\n ', tunable_type=ParticipantTypeSingleSim, default=ParticipantTypeSingleSim.Actor), 'footprint_target': TunableVariant( description= '\n The object whose footprint to check against.\n ', by_participant=TunableTuple( description= '\n Get footprint from a participant.\n ', locked_args={'target_type': BY_PARTICIPANT}, participant=TunableEnumEntry( description= '\n The participant whose required slot count we consider.\n ', tunable_type=ParticipantTypeSingle, default=ParticipantTypeSingle.Object)), by_tag=TunableTuple( description= '\n Get footprint from an object with this tag. If there are\n multiple, the test passes as long as one passes.\n ', tag=TunableTag( description= '\n Tag to find objects by.\n ' ), locked_args={'target_type': BY_TAG}), default='by_participant'), 'footprint_names': OptionalTunable( description= "\n Specific footprints to check against. If left unspecified, we\n check against the object's default footprints (i.e. the ones\n enabled in Medator).\n ", tunable=TunableSet(tunable=TunableStringHash32( description= '\n Name of footprint. Can be looked up in Medator. If in\n doubt, consult the modeler.\n ' ), minlength=1)), 'invert': Tunable( description= '\n If checked, test will pass if the actor is not in the footprint.\n ', tunable_type=bool, default=False) } def get_expected_args(self): kwargs = {} kwargs['actors'] = self.actor if self.footprint_target.target_type == self.BY_PARTICIPANT: kwargs['footprint_target'] = self.footprint_target.participant return kwargs def _test_if_sim_in_target_footprint(self, sim, target): if self.footprint_names is None: polygon = target.footprint_polygon else: polygon = target.get_polygon_from_footprint_name_hashes( self.footprint_names) if polygon is not None and polygon.contains(sim.position): return True return False @cached_test def __call__(self, actors=(), footprint_target=None): actor = next(iter(actors), None) if actor is None: return TestResult(False, 'No actors', tooltip=self.tooltip) actor_sim = actor.get_sim_instance() if actor_sim is None: return TestResult( False, "Actor is not an instantiated Sim. Can't check position: {}", actor[0], tooltip=self.tooltip) if self.footprint_target.target_type == self.BY_PARTICIPANT: if footprint_target is None: return TestResult(False, 'Missing participant.', tooltip=self.tooltip) targets = (footprint_target, ) elif self.footprint_target.target_type == self.BY_TAG: targets = services.object_manager().get_objects_with_tag_gen( self.footprint_target.tag) else: return TestResult(False, 'Unknown target type: {}', self.footprint_target.target_type, tooltip=self.tooltip) if self.invert: if any( self._test_if_sim_in_target_footprint(actor_sim, target) for target in targets): return TestResult(False, 'In footprint, inverted', tooltip=self.tooltip) elif not any( self._test_if_sim_in_target_footprint(actor_sim, target) for target in targets): return TestResult(False, 'Not in footprint', tooltip=self.tooltip) return TestResult.TRUE
class ActingStudioZoneDirector(CareerEventZoneDirector): INSTANCE_TUNABLES = {'stage_marks': TunableMapping(description='\n A mapping of stage marker tags to the interactions that should be\n added to them for this gig. These interactions will be applied to\n the stage mark/object on zone load.\n ', key_name='stage_mark_tag', key_type=TunableTag(description='\n The tag for the stage mark object the tuned scene interactions\n should be on.\n ', filter_prefixes=('func',)), value_name='scene_interactions', value_type=TunableSet(description='\n The set of interactions that will be added to the stage mark\n object.\n ', tunable=TunableReference(description='\n A Super Interaction that should be added to the stage mark\n object.\n ', manager=services.affordance_manager(), class_restrictions='SuperInteraction')), tuning_group=GroupNames.CAREER), 'performance_objects': TunableMapping(description='\n A mapping of performance objects (i.e. lights, green screen, vfx\n machine) and the state they should be put into when the performance\n starts/stops.\n ', key_name='performance_object_tag', key_type=TunableTag(description='\n The tag for the performance object.\n ', filter_prefixes=('func',)), value_name='performance_object_states', value_type=TunableTuple(description="\n States that should be applied to the objects before, during, and\n after the performance. If the object doesn't have the necessary\n state then nothing will happen.\n ", pre_performance_states=TunableSet(description='\n States to set on the object when the zone loads.\n ', tunable=TunableTuple(description='\n A state to set on an object as well as a perk that will\n skip setting the state.\n ', state_value=TunableReference(description='\n A state value to set on the object.\n ', manager=services.get_instance_manager(sims4.resources.Types.OBJECT_STATE), class_restrictions=('ObjectStateValue',)), skip_with_perk=OptionalTunable(description='\n If enabled, allows skipping this state change if the\n active Sim has a tuned perk.\n ', tunable=TunableReference(description="\n If the active Sim has this perk, this state won't be\n set on the tuned objects. For instance, if the Sim\n has the Established Name perk, they don't need to\n use the hair and makeup chair. This can prevent\n those objects from glowing in that case.\n ", manager=services.get_instance_manager(sims4.resources.Types.BUCKS_PERK))))), post_performance_states=TunableSet(description='\n States set on the object when the performance is over.\n ', tunable=TunableReference(description='\n A state value to set on the object.\n ', manager=services.get_instance_manager(sims4.resources.Types.OBJECT_STATE), class_restrictions=('ObjectStateValue',))), performance_states=TunableSet(description='\n States to set on the object when the performance starts.\n ', tunable=TunableReference(description='\n A state value to set on the object.\n ', manager=services.get_instance_manager(sims4.resources.Types.OBJECT_STATE), class_restrictions=('ObjectStateValue',)))), tuning_group=GroupNames.CAREER), 'start_performance_interaction': TunableReference(description='\n A reference to the interaction that indicates the performance is\n starting. This is what triggers all of the state changes in the\n Performance Object tuning.\n ', manager=services.affordance_manager(), class_restrictions='SuperInteraction', tuning_group=GroupNames.CAREER), 'lot_load_loot': TunableMapping(description='\n A mapping of Object IDs and loots to apply to those objects when the\n lot loads. This can be used for things like applying specific locks\n to door.\n ', key_name='object_tag', key_type=TunableTag(description='\n All objects with this tag will have the tuned loot applied on\n lot load..\n ', filter_prefixes=('func',)), value_name='loot', value_type=TunableSet(description='\n A set of loots to apply to all objects with the specified tag.\n ', tunable=TunableVariant(description='\n A specific loot to apply.\n ', lock_door=LockDoor.TunableFactory())), tuning_group=GroupNames.CAREER), 'thats_a_wrap_audio': TunablePlayAudio(description='\n The sound to play when the player has completed the performance and\n the Post Performance Time To Wrap Callout time has passed.\n '), 'post_performance_time_remaining': TunableTimeSpan(description="\n This is how long the gig should last once the player completes the\n final interaction. Regardless of how long the timer shows, once the\n player finishes the final interaction, we'll set the gig to end in\n this tuned amount of time.\n \n Note: This should be enough time to encompass both the Post\n Performance Time To Wrap Callout and Post Performance time Between\n Wrap And Lights time spans.\n ", default_minutes=20, locked_args={'days': 0}), 'post_performance_time_to_wrap_callout': TunableTimeSpan(description='\n How long, after the Player completes the entire gig, until the\n "That\'s a wrap" sound should play.\n ', default_minutes=5, locked_args={'days': 0, 'hours': 0}), 'post_performance_time_between_wrap_and_lights': TunableTimeSpan(description='\n How long after the "that\'s a wrap" sound until the post-performance\n state should be swapped on all the objects (lights, greenscreen,\n etc.)\n ', default_minutes=5, locked_args={'days': 0, 'hours': 0})} ACTING_STUDIO_EVENTS = (TestEvent.InteractionComplete, TestEvent.MainSituationGoalComplete) STATE_PRE_PERFORMANCE = 0 STATE_PERFORMANCE = 1 STATE_POST_PERFORMANCE = 2 SAVE_DATA_STATE = 'acting_studio_state' def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._reset_data() def _reset_data(self): self._stage_marks = set() self._performance_object_data = [] self._post_performance_state_alarm = None self._post_performance_call_out_alarm = None self._current_state = self.STATE_PRE_PERFORMANCE def on_startup(self): super().on_startup() services.get_event_manager().register(self, self.ACTING_STUDIO_EVENTS) def on_cleanup_zone_objects(self): object_manager = services.object_manager() self._init_stage_marks(object_manager) self._init_performance_object_data(object_manager) self._apply_lot_load_loot(object_manager) def _apply_lot_load_loot(self, object_manager): active_sim_info = services.active_sim_info() for (tag, loots) in self.lot_load_loot.items(): objects = object_manager.get_objects_matching_tags((tag,)) for obj in objects: resolver = SingleActorAndObjectResolver(active_sim_info, obj, source=self) for loot in loots: loot.apply_to_resolver(resolver) def on_shutdown(self): super().on_shutdown() services.get_event_manager().unregister(self, self.ACTING_STUDIO_EVENTS) self._reset_data() def on_career_event_stop(self): services.get_event_manager().unregister(self, self.ACTING_STUDIO_EVENTS) def handle_event(self, sim_info, event, resolver): career = services.get_career_service().get_career_in_career_event() if career.sim_info is not sim_info: return if event == TestEvent.InteractionComplete and isinstance(resolver.interaction, self.start_performance_interaction) and not resolver.interaction.has_been_reset: self._start_performance() elif event == TestEvent.MainSituationGoalComplete: self._end_performance(career) def _save_custom_zone_director(self, zone_director_proto, writer): writer.write_uint32(self.SAVE_DATA_STATE, self._current_state) super()._save_custom_zone_director(zone_director_proto, writer) def _load_custom_zone_director(self, zone_director_proto, reader): if reader is not None: self._current_state = reader.read_uint32(self.SAVE_DATA_STATE, self.STATE_PRE_PERFORMANCE) super()._load_custom_zone_director(zone_director_proto, reader) def _start_performance(self): for performance_object_data in self._performance_object_data: performance_object_data.set_performance_states() self._current_state = self.STATE_PERFORMANCE def _end_performance(self, career): new_end_time = services.time_service().sim_now + self.post_performance_time_remaining() career.set_career_end_time(new_end_time, reset_warning_alarm=False) self._post_performance_state_alarm = alarms.add_alarm(self, self.post_performance_time_to_wrap_callout(), self._post_performance_wrap_callout) self._current_state = self.STATE_POST_PERFORMANCE def _post_performance_wrap_callout(self, _): play_tunable_audio(self.thats_a_wrap_audio) self._post_performance_state_alarm = alarms.add_alarm(self, self.post_performance_time_between_wrap_and_lights(), self._post_performance_state_change) def _post_performance_state_change(self, _): for performance_object_data in self._performance_object_data: performance_object_data.set_post_performance_states() self._post_performance_state_alarm = None def _init_stage_marks(self, object_manager): for (tag, interactions) in self.stage_marks.items(): marks = object_manager.get_objects_matching_tags((tag,)) if not marks: continue self._stage_marks.update(marks) for obj in marks: obj.add_dynamic_component(types.STAGE_MARK_COMPONENT, performance_interactions=interactions) def _init_performance_object_data(self, object_manager): for (tag, states) in self.performance_objects.items(): performance_objects = object_manager.get_objects_matching_tags((tag,)) if not performance_objects: continue performance_object_data = PerformanceObjectData(performance_objects, states.pre_performance_states, states.performance_states, states.post_performance_states) self._performance_object_data.append(performance_object_data) if self._current_state == self.STATE_PRE_PERFORMANCE: performance_object_data.set_pre_performance_states()
class DoCommand(XevtTriggeredElement, HasTunableFactory): ARG_TYPE_PARTICIPANT = 0 ARG_TYPE_LITERAL = 1 ARG_TYPE_TAG = 3 @staticmethod def _verify_tunable_callback(source, *_, command, **__): command_name = command.split(' ', 1)[0] command_restrictions = get_command_restrictions(command_name) command_type = get_command_type(command_name) if command_restrictions is None or command_type is None: logger.error('Command {} specified in {} does not exist.', command_name, source) else: if command_restrictions & CommandRestrictionFlags.RESTRICT_SAVE_UNLOCKED and source.allow_while_save_locked: logger.error( 'Command {} specified in {} is unavailable during save lock. The interaction should not be available during save lock either.', command_name, source) if command_type != CommandType.Live and not source.debug and not source.cheat: logger.error( 'Command {} is {} command tuned on non-debug interaction {}. The command type should be CommandType.Live.', command_name, command_type, source) if command_type < CommandType.Cheat and source.cheat: logger.error( 'Command {} is {} command tuned on cheat interaction {}. The command type should be CommandType.Cheat or above.', command_name, command_type, source) FACTORY_TUNABLES = { 'command': Tunable(description='\n The command to run.\n ', tunable_type=str, default=None), 'arguments': TunableList( description= "\n The arguments for this command. Arguments will be added after the\n command in the order they're listed here.\n ", tunable=TunableVariant( description= '\n The argument to use. In most cases, the ID of the participant\n will be used.\n ', participant=TunableTuple( description= '\n An argument that is a participant in the interaction. The\n ID will be used as the argument for the command.\n ', argument=TunableEnumEntry( description= '\n The participant argument. The ID will be used in the\n command.\n ', tunable_type=ParticipantType, default=ParticipantTypeSingle.Object), locked_args={'arg_type': ARG_TYPE_PARTICIPANT}), string=TunableTuple( description= "\n An argument that's a string.\n ", argument=Tunable( description= '\n The string argument.\n ', tunable_type=str, default=None), locked_args={'arg_type': ARG_TYPE_LITERAL}), number=TunableTuple( description= '\n An argument that is a number. This can be a float or an int.\n ', argument=Tunable( description= '\n The number argument.\n ', tunable_type=float, default=0), locked_args={'arg_type': ARG_TYPE_LITERAL}), tag=TunableTuple( description= '\n An argument that is a tag.\n ', argument=TunableTag( description= '\n The tag argument.\n ' ), locked_args={'arg_type': ARG_TYPE_TAG}), boolean=TunableTuple( description= '\n An argument that is a boolean.\n ', argument=Tunable( description= '\n The number argument.\n ', tunable_type=bool, default=True), locked_args={'arg_type': ARG_TYPE_LITERAL}))), 'verify_tunable_callback': _verify_tunable_callback } def _do_behavior(self): full_command = self.command for arg in self.arguments: if arg.arg_type == self.ARG_TYPE_PARTICIPANT: for participant in self.interaction.get_participants( arg.argument): if hasattr(participant, 'id'): full_command += ' {}'.format(participant.id) else: full_command += ' {}'.format(participant) else: if arg.arg_type == self.ARG_TYPE_LITERAL: full_command += ' {}'.format(arg.argument) elif arg.arg_type == self.ARG_TYPE_TAG: full_command += ' {}'.format(int(arg.argument)) else: logger.error( 'Trying to run the Do Command element with an invalid arg type, {}.', arg.arg_type, owner='trevor') return False elif arg.arg_type == self.ARG_TYPE_LITERAL: full_command += ' {}'.format(arg.argument) elif arg.arg_type == self.ARG_TYPE_TAG: full_command += ' {}'.format(int(arg.argument)) else: logger.error( 'Trying to run the Do Command element with an invalid arg type, {}.', arg.arg_type, owner='trevor') return False client_id = services.client_manager().get_first_client_id() commands.execute(full_command, client_id) return True
class BaseAppearanceModification(HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = { '_is_combinable_with_same_type': Tunable( description= '\n True if this modifier type is able to be combined with another\n of its type. If True, and two modifiers conflict, then the tuned\n priority will be used to resolve the conflict. If False, only\n a single modifier of this type with the highest priority will be shown.\n ', tunable_type=bool, default=True), 'outfit_type_compatibility': OptionalTunable( description= '\n If enabled, will verify when switching outfits if the new\n outfit is compatible with this appearance modifier.\n ', disabled_name="Don't_Test", tunable=TunableWhiteBlackList( description= '\n The outfit category must match the whitelist and blacklist\n to be applied.\n ', tunable=TunableEnumEntry( description= '\n The outfit category want to test against the \n apperance modifier.\n ', tunable_type=OutfitCategory, default=OutfitCategory.EVERYDAY))), 'appearance_modifier_tag': OptionalTunable( description= '\n If enabled, a tag used to reference this appearance modifier.\n ', tunable=TunableTag( description= '\n Tag associated with this appearance modifier.\n ' )) } def modify_sim_info(self, source_sim_info, modified_sim_info, random_seed): raise NotImplementedError( 'Attempting to use the BaseAppearanceModification base class, use sub-classes instead.' ) @property def is_permanent_modification(self): return False @property def modifier_type(self): raise NotImplementedError( 'Attempting to use the BaseAppearanceModification base class, use sub-classes instead.' ) @property def is_combinable_with_same_type(self): return self._is_combinable_with_same_type @property def combinable_sorting_key(self): raise NotImplementedError( 'Attempting to use the BaseAppearanceModification base class, use sub-classes instead.' ) def is_compatible_with_outfit(self, outfit_category): if self.outfit_type_compatibility is None: return True return self.outfit_type_compatibility.test_item(outfit_category)
class SituationSchedulerComponent(Component, HasTunableFactory, AutoFactoryInit, allow_dynamic=True, component_name=SITUATION_SCHEDULER_COMPONENT, persistence_key=protocols.PersistenceMaster. PersistableData.SituationSchedulerComponent): FACTORY_TUNABLES = { 'object_based_situations_schedule': OptionalTunable( description= '\n If enabled, the object provides its own situation schedule.\n ', tunable=TunableTuple( description= '\n Data associated with situations schedule.\n ', tag=TunableTag( description= '\n An object tag. If the object exist on the zone lot, situations\n will be scheduled. The basic assumption is that this tag matches\n one of the tags associated with this object, but this is not \n enforced.\n ', filter_prefixes=('func', )), situation_schedule=SituationWeeklyScheduleVariant( description= '\n The schedule to trigger the different situations.\n ', pack_safe=True, affected_object_cap=True), schedule_immediate=Tunable( description= '\n This controls the behavior of scheduler if the current time\n happens to fall within a schedule entry. If this is True, \n a start_callback will trigger immediately for that entry.\n If False, the next start_callback will occur on the next entry.\n ', tunable_type=bool, default=False), consider_off_lot_objects=Tunable( description= '\n If True, consider all objects in lot and the open street for\n this object situation. If False, only consider objects on\n the active lot.\n ', tunable_type=bool, default=True), tests=TunableTestSet( description= "\n Tests to determine if this Tag should be added to the active\n Zone Director's Situations Schedule. Test is performed\n when the schedule is rebuilt. This is currently on Zone\n Spin Up and Build Buy Exit.\n " ))) } def __init__(self, *args, scheduler=None, **kwargs): if 'object_based_situations_schedule' not in kwargs: kwargs['object_based_situations_schedule'] = None super().__init__(*args, **kwargs) self._situation_scheduler = scheduler self._generated_situation_ids = set() @componentmethod def set_situation_scheduler(self, scheduler): self._destroy_situation_scheduler() self._situation_scheduler = scheduler @componentmethod def create_situation(self, situation_type, **params): if not situation_type.situation_meets_starting_requirements(): return situation_manager = services.get_zone_situation_manager() self._cleanup_generated_situations(situation_manager) running_situation = self._get_same_situation_running( situation_manager, situation_type) if running_situation is not None: situation_manager.destroy_situation_by_id(running_situation.id) guest_list = situation_type.get_predefined_guest_list( ) or SituationGuestList(invite_only=True) merged_params = dict(params, guest_list=guest_list, user_facing=False, scoring_enabled=False, spawn_sims_during_zone_spin_up=True, creation_source=str(self), default_target_id=self.owner.id) situation_id = situation_manager.create_situation( situation_type, **merged_params) if situation_id is None: return self._generated_situation_ids.add(situation_id) def on_remove(self, *_, **__): self.destroy_scheduler_and_situations() def destroy_scheduler_and_situations(self): self._destroy_situation_scheduler() self._destroy_generated_situations() def _cleanup_generated_situations(self, situation_manager): for situation_id in list(self._generated_situation_ids): running_situation = situation_manager.get(situation_id) if running_situation is None: self._generated_situation_ids.remove(situation_id) def _get_same_situation_running(self, situation_manager, situation_type): for situation_id in self._generated_situation_ids: running_situation = situation_manager.get(situation_id) if situation_type is type(running_situation): return running_situation def _destroy_situation_scheduler(self): if self._situation_scheduler is not None: self._situation_scheduler.destroy() self._situation_scheduler = None def _destroy_generated_situations(self): situation_manager = services.get_zone_situation_manager() for situation_id in self._generated_situation_ids: situation_manager.destroy_situation_by_id(situation_id) self._generated_situation_ids.clear() @property def can_remove_component(self): return not hasattr(self, 'object_based_situations_schedule') or ( self.object_based_situations_schedule is None or (self.object_based_situations_schedule.tag == Tag.INVALID or not self.object_based_situations_schedule.situation_schedule)) def save(self, persistence_master_message): persistable_data = protocols.PersistenceMaster.PersistableData() persistable_data.type = protocols.PersistenceMaster.PersistableData.SituationSchedulerComponent component_data = persistable_data.Extensions[ protocols.PersistableSituationSchedulerComponent.persistable_data] if self._generated_situation_ids: component_data.situation_ids.extend(self._generated_situation_ids) persistence_master_message.data.extend([persistable_data]) def load(self, persistable_data): component_data = persistable_data.Extensions[ protocols.PersistableSituationSchedulerComponent.persistable_data] for situation_id in component_data.situation_ids: self._generated_situation_ids.add(situation_id)
class OceanTuning: BEACH_LOCATOR_TAG = TunableTag( description= '\n The tag we can use to get the beach locator definition.\n ' ) OCEAN_DATA = TunableMapping( description= '\n The species-age mapping to ocean data. This defines what\n ages and species can wade in the water and what the water level\n restrictions are as well as beach portal access objects.\n ', key_name='species', key_type=TunableEnumEntry( description= '\n The extended species that this data is for.\n ', tunable_type=SpeciesExtended, default=SpeciesExtended.HUMAN), value_name='age_data', value_type=TunableList( description='\n The ages and their data.\n ', tunable=TunableTuple( description= '\n The ages and their ocean data.\n ', ages=TunableEnumSet( description= '\n The age of the actor.\n ', enum_type=Age), ocean_data=TunableTuple( description= '\n The ocean data for this Age.\n ', wading_interval=TunableInterval( description= '\n The wading interval for Sims at this age and species. The lower\n bound indicates the minimum water height required to apply the\n wading walkstyle, and the upper bound indicates the maximum\n height we can walk into the water until we can potentially\n swim.\n ', tunable_type=float, default_lower=0.1, default_upper=1.0, minimum=0.01), beach_portal_data=OptionalTunable( description= '\n An optional portal definition to allow sims to swim in\n the ocean. Without this, Sims at this age and species\n cannot swim in the ocean.\n ', tunable=TunableReference( description= '\n The portals this age/species will use to swim in the ocean.\n ', manager=services.snippet_manager(), class_restrictions=('PortalData', ), pack_safe=True)), water_depth_error=TunableRange( description= '\n The error, in meters, that we allow for the swimming beach\n portals.\n ', tunable_type=float, default=0.05, minimum=0.01), swimwear_change_water_depth=TunableRange( description= "\n If a Sim's path includes water where the depth is at\n least the tuned value, in meters, they will switch into\n the outfit based on the outfit change reasonat the \n start of the path.\n ", tunable_type=float, default=0.1, minimum=0), swimwear_change_outfit_reason=OptionalTunable( description= '\n If enabled, the outfit change reason that determines which outfit\n category a Sim automatically changes into when \n entering water.\n ', tunable=TunableEnumEntry( tunable_type=OutfitChangeReason, default=OutfitChangeReason.Invalid, invalid_enums=(OutfitChangeReason.Invalid, ))))))) beach_locator_definition = None @staticmethod def get_beach_locator_definition(): if OceanTuning.beach_locator_definition is None: for definition in services.definition_manager( ).get_definitions_for_tags_gen((OceanTuning.BEACH_LOCATOR_TAG, )): OceanTuning.beach_locator_definition = definition break return OceanTuning.beach_locator_definition @staticmethod def get_actor_ocean_data(actor): if not actor.is_sim and not isinstance(actor, StubActor): return species_data = OceanTuning.OCEAN_DATA.get(actor.extended_species, None) if species_data is None: return actor_age = actor.age for age_data in species_data: if actor_age in age_data.ages: return age_data.ocean_data @staticmethod def get_actor_wading_interval(actor): ocean_data = OceanTuning.get_actor_ocean_data(actor) if ocean_data is not None: return ocean_data.wading_interval else: interval_actor = actor if not isinstance(actor, StubActor): if actor.vehicle_component is not None: drivers = actor.get_users(sims_only=True) for driver in drivers: if driver.posture.is_vehicle: if driver.posture.target is actor: interval_actor = driver break ocean_data = OceanTuning.get_actor_ocean_data(interval_actor) if ocean_data is not None: return ocean_data.wading_interval @staticmethod def get_actor_swimwear_change_info(actor): ocean_data = OceanTuning.get_actor_ocean_data(actor) if ocean_data is not None: return (ocean_data.swimwear_change_water_depth, ocean_data.swimwear_change_outfit_reason) return (None, None) @staticmethod def make_depth_bounds_safe_for_surface_and_sim(routing_surface, sim, min_water_depth=None, max_water_depth=None): interval = OceanTuning.get_actor_wading_interval(sim) return OceanTuning.make_depth_bounds_safe_for_surface( routing_surface, wading_interval=interval, min_water_depth=min_water_depth, max_water_depth=max_water_depth) @staticmethod def make_depth_bounds_safe_for_surface(routing_surface, wading_interval=None, min_water_depth=None, max_water_depth=None): if routing_surface.type == SurfaceType.SURFACETYPE_WORLD: surface_min_water_depth = min_water_depth if wading_interval is not None: if max_water_depth is None: surface_max_water_depth = wading_interval.upper_bound else: surface_max_water_depth = min(wading_interval.upper_bound, max_water_depth) surface_max_water_depth = 0 else: surface_max_water_depth = 0 elif routing_surface.type == SurfaceType.SURFACETYPE_POOL: if wading_interval is not None: if min_water_depth is None: surface_min_water_depth = wading_interval.upper_bound else: surface_min_water_depth = max(wading_interval.upper_bound, min_water_depth) else: surface_min_water_depth = min_water_depth surface_max_water_depth = max_water_depth else: surface_min_water_depth = min_water_depth surface_max_water_depth = max_water_depth return (surface_min_water_depth, surface_max_water_depth)
class TempleTuning: TEMPLES = TunableMapping( description= '\n A Mapping of Temple Templates (house descriptions) and the data\n associated with each temple.\n ', key_name='Template House Description', key_type=TunableHouseDescription(pack_safe=True), value_name='Temple Data', value_type=TunableTuple( description= '\n The data associated with the mapped temple template.\n ', rooms=TunableMapping( description= '\n A mapping of room number to the room data. Room number 0 will be\n the entrance room to the temple, room 1 will be the first room\n that needs to be unlocked, and so on.\n ', key_name='Room Number', key_type=int, value_name='Room Data', value_type=TunableTempleRoomData(pack_safe=True)), enter_lot_loot=TunableSet( description= '\n Loot applied to Sims when they enter or spawn in to this Temple.\n \n NOTE: Exit Lot Loot is not guaranteed to be given. For example,\n if the Sim walks onto the lot, player switches to a different\n zone, then summons that Sim, that Sim will bypass getting the\n exit loot.\n ', tunable=LootActions.TunableReference(pack_safe=True)), exit_lot_loot=TunableSet( description= '\n Loot applied to Sims when they exit or spawn out of this Temple.\n \n NOTE: This loot is not guaranteed to be given after Enter Lot\n Loot. For example, if the Sim walks onto the lot, player\n switches to a different zone, then summons that Sim, that Sim\n will bypass getting the exit loot.\n ', tunable=LootActions.TunableReference(pack_safe=True)))) GATE_TAG = TunableTag( description= '\n The tag used to find the gate objects inside Temples.\n ', filter_prefixes=('func_temple', )) TRAP_TAG = TunableTag( description= '\n The tag used to identify traps inside temples. This will be used to find\n placeholder traps as well.\n ', filter_prefixes=('func_temple', )) CHEST_TAG = TunableTag( description= "\n The tag used to identify the final chest of a temple. If it's in the\n open state, the temple will be considered solved.\n ", filter_prefixes=('func_temple', )) CHEST_OPEN_STATE = TunablePackSafeReference( description= '\n The state that indicates the chest is open.\n ', manager=services.get_instance_manager( sims4.resources.Types.OBJECT_STATE), class_restrictions='ObjectStateValue') GATE_STATE = TunablePackSafeReference( description= '\n The state for temple gates. Used for easy look up.\n ', manager=services.get_instance_manager( sims4.resources.Types.OBJECT_STATE), class_restrictions='ObjectState') GATE_UNLOCK_STATE = TunablePackSafeReference( description='\n The unlock state for temple gates.\n ', manager=services.get_instance_manager( sims4.resources.Types.OBJECT_STATE), class_restrictions='ObjectStateValue') TEMPLE_LOT_DESCRIPTION = TunablePackSafeLotDescription( description= '\n A reference to the lot description file for the temple lot. This is used\n for easier zone ID lookups.\n ' ) GATE_LOCK_LOOT = LockDoor.TunableFactory( description= '\n The LockDoor loot to run on the gates in the temple on load when they\n should be locked.\n ' ) GATE_UNLOCK_LOOT = UnlockDoor.TunableFactory( description= '\n The UnlockDoor loot to run on the gates when they should be unlocked.\n ' ) CHEST_OPEN_INTEARCTION = TunableInteractionOfInterest( description= '\n A reference to the open interaction for chests. This interaction will be\n listened for to determine temple completion.\n ' )
class ObjectBasedSituationZoneDirectorMixin: INSTANCE_TUNABLES = { 'object_based_situations_schedule': TunableMapping( description= '\n Mapping of object tag to situations schedule. \n When the object in the tag is exist on the zone lot, the situations\n will be spawned based on the schedule.\n ', key_type=TunableTag( description= '\n An object tag. If the object exist on the zone lot, situations\n will be scheduled.\n ', filter_prefixes=('func', )), value_type=TunableTuple( description= '\n Data associated with situations schedule.\n ', situation_schedule=SituationWeeklyScheduleVariant( description= '\n The schedule to trigger the different situations.\n ', pack_safe=True, affected_object_cap=True), schedule_immediate=Tunable( description= '\n This controls the behavior of scheduler if the current time\n happens to fall within a schedule entry. If this is True, \n a start_callback will trigger immediately for that entry.\n If False, the next start_callback will occur on the next entry.\n ', tunable_type=bool, default=False), consider_off_lot_objects=Tunable( description= '\n If True, consider all objects in lot and the open street for\n this object situation. If False, only consider objects on\n the active lot.\n ', tunable_type=bool, default=True))), 'use_object_provided_situations_schedule': Tunable( description= '\n If checked, objects on the lot and supplement or replace elements of\n Object Based Situations Schedule.\n ', tunable_type=bool, default=True) } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.affected_objects_map = {} self._object_based_situations_schedule = {} def create_situations_during_zone_spin_up(self): super().create_situations_during_zone_spin_up() self._update_object_based_situations_schedule() self._cleanup_affected_objects() self._setup_affected_objects() def on_exit_buildbuy(self): super().on_exit_buildbuy() self._update_object_based_situations_schedule() self._setup_affected_objects() def _update_object_based_situations_schedule(self): self._object_based_situations_schedule.clear() for (object_tag, data) in self.object_based_situations_schedule.items(): self._object_based_situations_schedule[object_tag] = data if not self.use_object_provided_situations_schedule: return object_manager = services.object_manager() for obj in object_manager.get_valid_objects_gen(): situation_scheduler_component = obj.get_component( SITUATION_SCHEDULER_COMPONENT) if not situation_scheduler_component is None: if situation_scheduler_component.can_remove_component: continue data = situation_scheduler_component.object_based_situations_schedule resolver = SingleObjectResolver(obj) if not data.tests is None: if not data.tests.run_tests(resolver): continue self._object_based_situations_schedule[ data.tag] = ObjectBasedSituationData( data.situation_schedule, data.schedule_immediate, data.consider_off_lot_objects) @staticmethod def _cleanup_object(obj): situation_scheduler_component = obj.get_component( SITUATION_SCHEDULER_COMPONENT) if situation_scheduler_component is None: return if situation_scheduler_component.can_remove_component: obj.remove_component(SITUATION_SCHEDULER_COMPONENT) else: situation_scheduler_component.destroy_scheduler_and_situations() def _cleanup_affected_objects(self): object_tags = self._object_based_situations_schedule.keys() object_manager = services.object_manager() for obj in object_manager.get_valid_objects_gen(): if not obj.has_any_tag(object_tags): self._cleanup_object(obj) def _setup_affected_objects(self): object_manager = services.object_manager() for (object_tag, data) in self._object_based_situations_schedule.items(): object_cap = self._get_current_affected_object_cap( data.situation_schedule) tagged_objects = [] if data.consider_off_lot_objects: tagged_objects = list( object_manager.get_objects_with_tag_gen(object_tag)) else: tagged_objects = [ obj for obj in object_manager.get_objects_with_tag_gen( object_tag) if obj.is_on_active_lot() ] if not tagged_objects: continue if object_tag not in self.affected_objects_map: self.affected_objects_map[object_tag] = WeakSet() affected_objects = self.affected_objects_map[object_tag] while len(affected_objects) < object_cap: while tagged_objects: obj_to_add = tagged_objects.pop() if obj_to_add in affected_objects: continue scheduler = data.situation_schedule( start_callback=self._start_situations, schedule_immediate=data.schedule_immediate, extra_data=obj_to_add) if obj_to_add.has_component(SITUATION_SCHEDULER_COMPONENT): obj_to_add.set_situation_scheduler(scheduler) else: obj_to_add.add_dynamic_component( SITUATION_SCHEDULER_COMPONENT, scheduler=scheduler) affected_objects.add(obj_to_add) while len(affected_objects) > object_cap: while affected_objects: obj_to_remove = affected_objects.pop() self._cleanup_object(obj_to_remove) def _start_situations(self, scheduler, alarm_data, obj): self._setup_affected_objects() if not scheduler.extra_data.has_component( SITUATION_SCHEDULER_COMPONENT): return if hasattr(alarm_data.entry, 'weighted_situations'): (situation, params ) = WeightedSituationsWeeklySchedule.get_situation_and_params( alarm_data.entry) else: situation = alarm_data.entry.situation params = {} if situation is None: return obj.create_situation(situation, **params) def _get_current_affected_object_cap(self, schedule): current_time = services.time_service().sim_now (best_time, alarm_data) = schedule().time_until_next_scheduled_event( current_time, schedule_immediate=True) if best_time is None or best_time > TimeSpan.ZERO: current_affected_object_cap = 0 else: current_affected_object_cap = alarm_data[ 0].entry.affected_object_cap return current_affected_object_cap
class JungleOpenStreetDirector(OpenStreetDirectorBase): DEFAULT_LOCK = LockAllWithSimIdExceptionData(lock_priority=LockPriority.PLAYER_LOCK, lock_sides=LockSide.LOCK_FRONT, should_persist=True, except_actor=False, except_household=False) PATH_LOCKED = 0 PATH_UNAVAILABLE = 1 PATH_UNLOCKED = 2 MIN_CLEAR_COMMODITY = 0 TEMPLE_STATE_NEEDS_RESET = 0 TEMPLE_STATE_RESET = 1 TEMPLE_STATE_IN_PROGRESS = 2 TEMPLE_STATE_COMPLETE = 3 TREASURE_CHEST_CLOSED = 0 TREASURE_CHEST_OPEN = 1 TEMPLE_PATH_OBSTACLE = TunableTag(description='\n The tag for the path obstacle that leads to the Temple. This will be\n used to gain a reference to it when the temple resets.\n ', filter_prefixes=('Func',)) TEMPLE_PATH_OBSTACLE_LOCK_STATE = TunableStateValueReference(description='\n Indicates the temple is locked. This will be used to lock the\n Path Obstacle.\n ', pack_safe=True) TEMPLE_PATH_OBSTACLE_UNLOCK_STATE = TunableStateValueReference(description='\n The unlock state for the path obstacles. Set when we load a brand new\n vacation in the jungle.\n ', pack_safe=True) TEMPLE_VENUE_TUNING = TunablePackSafeReference(description='\n The venue for the temple zone.\n ', manager=services.get_instance_manager(sims4.resources.Types.VENUE)) TEMPLE_LOCK_COMMODITY = TunablePackSafeReference(description='\n The commodity that controls the temple lock.\n ', manager=services.get_instance_manager(sims4.resources.Types.STATISTIC), class_restrictions='Commodity') INSTANCE_TUNABLES = {'path_obstacle_data': TunableMapping(description='\n Tuned data for the path obstacles in the open street. \n \n This includes which conditional layer the path obstacle is attached\n to and what state that layer is in when the obstacle is locked.\n ', key_name='obstacle_tag_id', key_type=TunableTag(description='\n A tag for a specific path obstacle object that we might want\n to mark blocked or access_PermanentlyBlocked. \n ', filter_prefixes=('Func',)), value_name='obstacle_data', value_type=TunableTuple(description='\n All of the data associated with the path obstacle.\n ', always_available=Tunable(description='\n If True then this particular path obstacle is always \n available to be cleared and traveled through.\n \n If False then this path obstacle is subject to randomly\n being available or unavailable depending on the travel\n group.\n ', tunable_type=bool, default=False), layers=TunableList(description='\n A list of conditional layers and the status the layer starts\n in (visible/hidden) that are associated with this path\n obstacle.\n ', tunable=TunableTuple(description='\n Data about which conditional layer the obstacle is associated\n with and what state it is in.\n ', conditional_layer=TunableReference(description='\n A reference to the Conditional Layer found in the open streets.\n ', manager=services.get_instance_manager(sims4.resources.Types.CONDITIONAL_LAYER)), visible=Tunable(description='\n Whether or not the conditional layer is show/hidden when\n the corresponding path obstacle is locked.\n \n Checked signifies that the layer is visible when the\n obstacle is locked.\n \n Unchecked signifies that the layer is hidden when the \n obstacle is locked.\n ', tunable_type=bool, default=True), immediate=Tunable(description='\n If checked then the layer will load immediately. If\n not checked then the layer will load over time.\n ', tunable_type=bool, default=False))))), 'num_of_paths_available': TunableRange(description='\n The number of paths that are available when a vacation group \n arrives in the jungle for the first time.\n ', tunable_type=int, minimum=0, default=1), 'clear_path_interaction': TunableInteractionOfInterest(description='\n A reference to the interaction that a Sim runs in order to clear\n the path obstacle so they can use the portal.\n '), 'permanently_blocked_state': TunableStateValueReference(description='\n The state the blocked path obstacles should be set to if they \n cannot be cleared.\n '), 'path_unlocked_state': TunableStateValueReference(description='\n The state the blocked path obstacles should be set to if they are \n unlocked.\n '), 'path_clearing_commodity': TunableReference(description='\n The commodity that has to reach 100 in order for a path to be\n considered clear.\n ', manager=services.get_instance_manager(sims4.resources.Types.STATISTIC)), 'treasure_chest_tag': TunableTag(description='\n The tag used to identify a treasure chest.\n '), 'treasure_chest_open_state': TunableStateValueReference(description='\n The state that a treasure chest is in when it has already been \n opened.\n '), 'treasure_chest_closed_state': TunableStateValueReference(description='\n The state that a treasure chest is in when it is still closed.\n ')} def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._obstacle_status = {} self._path_obstacles = {} self._treasure_chest_status = {} travel_group_manager = services.travel_group_manager() household = services.active_household() travel_group = travel_group_manager.get_travel_group_by_household(household) if travel_group is None: logger.error("Trying to initialize the Jungle Open Street Director but there doesn't appear to be a travel group for the current household.") self._current_travel_group_id = None else: self._current_travel_group_id = travel_group.id services.get_event_manager().register_single_event(self, TestEvent.InteractionComplete) self._current_temple_id = None self._temple_state = self.TEMPLE_STATE_NEEDS_RESET self._last_time_saved = None def on_startup(self): super().on_startup() object_manager = services.object_manager() self._path_obstacles = self._get_path_obstacles() if self._current_travel_group_id in self._obstacle_status: path_obstacle_data = self._obstacle_status[self._current_travel_group_id] for (tag, status, progress) in path_obstacle_data: obstacles = object_manager.get_objects_matching_tags((tag,)) for obstacle in obstacles: if tag == self.TEMPLE_PATH_OBSTACLE: if not obstacle.state_value_active(self.TEMPLE_PATH_OBSTACLE_UNLOCK_STATE): self._update_temple_lock_commodity() if obstacle.state_value_active(self.TEMPLE_PATH_OBSTACLE_UNLOCK_STATE): status = JungleOpenStreetDirector.PATH_LOCKED else: status = JungleOpenStreetDirector.PATH_UNAVAILABLE progress = 0 if status == JungleOpenStreetDirector.PATH_LOCKED: self._lock_path_obstacle(obstacle, tag) elif status == JungleOpenStreetDirector.PATH_UNAVAILABLE: self._permanently_lock_path_obstacle(obstacle, tag) elif status == JungleOpenStreetDirector.PATH_UNLOCKED: self._unlock_path_obstacle(obstacle, tag) else: logger.error('Trying to setup an object that has a tag status that is not known. {}', status) obstacle.set_stat_value(self.path_clearing_commodity, progress) if self._current_travel_group_id in self._treasure_chest_status: treasure_chest_data = self._treasure_chest_status[self._current_travel_group_id] for (obj_id, status) in treasure_chest_data: chest = object_manager.get(obj_id) if chest is None: continue if status == JungleOpenStreetDirector.TREASURE_CHEST_OPEN: chest.set_state(self.treasure_chest_open_state.state, self.treasure_chest_open_state) else: chest.set_state(self.treasure_chest_closed_state.state, self.treasure_chest_closed_state) else: always_available_paths = [] possible_paths = [] for (obj, tag) in self._path_obstacles.items(): if self.path_obstacle_data[tag].always_available: always_available_paths.append((obj, tag)) else: possible_paths.append((obj, tag)) available_paths = random.sample(possible_paths, min(len(possible_paths), self.num_of_paths_available)) unavailable_paths = [path for path in possible_paths if path not in available_paths] for (path_obstacle, tag) in itertools.chain(always_available_paths, available_paths): self._lock_path_obstacle(path_obstacle, tag, reset_commodity=True) for (path_obstacle, tag) in unavailable_paths: self._permanently_lock_path_obstacle(path_obstacle, tag) for chest in object_manager.get_objects_matching_tags((self.treasure_chest_tag,)): chest.set_state(self.treasure_chest_closed_state.state, self.treasure_chest_closed_state) travel_group_manager = services.travel_group_manager() travel_groups = travel_group_manager.get_travel_group_ids_in_region() for group_id in travel_groups: if group_id == self._current_travel_group_id: continue group = travel_group_manager.get(group_id) if group.played: break else: self._setup_for_first_travel_group() if self._temple_needs_reset(): self.reset_temple() def _update_temple_lock_commodity(self): obstacle = self._get_temple_entrance_obstacle() lock_tracker = obstacle.get_tracker(JungleOpenStreetDirector.TEMPLE_LOCK_COMMODITY) lock_stat = lock_tracker.get_statistic(JungleOpenStreetDirector.TEMPLE_LOCK_COMMODITY) lock_stat.update_commodity_to_time(self._last_time_saved) lock_state = JungleOpenStreetDirector.TEMPLE_PATH_OBSTACLE_UNLOCK_STATE.state obstacle.state_component.set_state_from_stat(lock_state, lock_stat) def _temple_needs_reset(self): if self._temple_state == self.TEMPLE_STATE_NEEDS_RESET: return True elif self._temple_state == self.TEMPLE_STATE_COMPLETE: sim_info_manager = services.sim_info_manager() temple_zones = tuple(services.venue_service().get_zones_for_venue_type_gen(self.TEMPLE_VENUE_TUNING)) if len(temple_zones) != 1: logger.error('Found either 0 or more than 1 zone that is set as a temple venue. There can be only one!') temple_zone_id = next(iter(temple_zones)) if not any(sim.zone_id == temple_zone_id and sim.is_played_sim for sim in sim_info_manager.get_all()): return True return False def _setup_for_first_travel_group(self): self._current_temple_id = None self._temple_state = self.TEMPLE_STATE_NEEDS_RESET self._unlock_temple_obstacle() def on_shutdown(self): super().on_shutdown() services.get_event_manager().unregister_single_event(self, TestEvent.InteractionComplete) self._path_obstacles.clear() def _get_path_obstacles(self): object_manager = services.object_manager() path_obstacles = {} for obstacle_tag in self.path_obstacle_data: obstacles = object_manager.get_objects_matching_tags((obstacle_tag,)) for obstacle in obstacles: path_obstacles[obstacle] = obstacle_tag return path_obstacles def _lock_path_obstacle(self, path_obstacle, obstacle_tag, reset_commodity=False): path_obstacle.add_lock_data(JungleOpenStreetDirector.DEFAULT_LOCK) self._setup_corresponding_layers(obstacle_tag) if reset_commodity: path_obstacle.set_stat_value(self.path_clearing_commodity, JungleOpenStreetDirector.MIN_CLEAR_COMMODITY) def _permanently_lock_path_obstacle(self, path_obstacle, obstacle_tag): path_obstacle.add_lock_data(JungleOpenStreetDirector.DEFAULT_LOCK) path_obstacle.set_state(self.permanently_blocked_state.state, self.permanently_blocked_state) self._setup_corresponding_layers(obstacle_tag) def _unlock_path_obstacle(self, path_obstacle, obstacle_tag): path_obstacle.set_state(self.path_unlocked_state.state, self.path_unlocked_state) self._setup_corresponding_layers(obstacle_tag, unlock=True) def _setup_corresponding_layers(self, path_obstacle_tag, unlock=False): obstacle_datas = self.path_obstacle_data[path_obstacle_tag] for obstacle_data in obstacle_datas.layers: if unlock == obstacle_data.visible: self.remove_layer_objects(obstacle_data.conditional_layer) elif obstacle_data.immediate: self.load_layer_immediately(obstacle_data.conditional_layer) else: self.load_layer_gradually(obstacle_data.conditional_layer) @classproperty def priority(cls): return OpenStreetDirectorPriority.DEFAULT def handle_event(self, sim_info, event, resolver, **kwargs): if resolver(self.clear_path_interaction): obstacle = resolver.interaction.target statistic = obstacle.get_stat_instance(self.path_clearing_commodity) if statistic is not None: statistic_value = statistic.get_value() if statistic_value >= statistic.max_value: obstacle.set_state(self.path_unlocked_state.state, self.path_unlocked_state) self._setup_corresponding_layers(self._path_obstacles[obstacle], unlock=True) def _save_custom_open_street_director(self, street_director_proto, writer): group_ids = [] tags = [] tag_status = [] clear_progress = [] for (group_id, path_obstacle_data) in self._obstacle_status.items(): if group_id == self._current_travel_group_id: continue for (tag, status, progress) in path_obstacle_data: group_ids.append(group_id) tags.append(tag) tag_status.append(status) clear_progress.append(progress) if self._current_travel_group_id is None: return for (path_obstacle, tag) in self._path_obstacles.items(): group_ids.append(self._current_travel_group_id) tags.append(tag) tag_status.append(self._get_tag_status(path_obstacle)) clear_progress.append(path_obstacle.get_stat_value(self.path_clearing_commodity)) writer.write_uint64s(GROUP_TOKEN, group_ids) writer.write_uint64s(TAG_TOKEN, tags) writer.write_uint64s(TAG_STATUS_TOKEN, tag_status) writer.write_floats(CLEAR_PROGRESS_TOKEN, clear_progress) writer.write_uint64(CURRENT_TEMPLE_ID, self._current_temple_id) writer.write_uint32(TEMPLE_STATE, self._temple_state) writer.write_uint64(LAST_TIME_SAVED, services.time_service().sim_now.absolute_ticks()) self._save_treasure_chest_data(writer) def _get_tag_status(self, path_obstacle): if path_obstacle.state_value_active(self.permanently_blocked_state): return JungleOpenStreetDirector.PATH_UNAVAILABLE if path_obstacle.state_value_active(self.path_unlocked_state): return JungleOpenStreetDirector.PATH_UNLOCKED return JungleOpenStreetDirector.PATH_LOCKED def _load_custom_open_street_director(self, street_director_proto, reader): if reader is None: return travel_group_manager = services.travel_group_manager() group_ids = reader.read_uint64s(GROUP_TOKEN, []) tags = reader.read_uint64s(TAG_TOKEN, []) tag_status = reader.read_uint64s(TAG_STATUS_TOKEN, []) clear_progress = reader.read_floats(CLEAR_PROGRESS_TOKEN, 0) self._current_temple_id = reader.read_uint64(CURRENT_TEMPLE_ID, 0) self._temple_state = reader.read_uint32(TEMPLE_STATE, self.TEMPLE_STATE_NEEDS_RESET) last_time_saved = reader.read_uint64(LAST_TIME_SAVED, 0) for (index, group_id) in enumerate(group_ids): if not travel_group_manager.get(group_id): continue if group_id not in self._obstacle_status: self._obstacle_status[group_id] = [] path_obstacles = self._obstacle_status[group_id] path_obstacles.append((tags[index], tag_status[index], clear_progress[index])) self._last_time_saved = DateAndTime(last_time_saved) self._load_treasure_chest_data(reader) @property def current_temple_id(self): return self._current_temple_id def get_next_temple_id(self): if self._temple_state == self.TEMPLE_STATE_RESET: return self._current_temple_id def reset_temple(self, new_id=None, force=False): if self._temple_state == self.TEMPLE_STATE_COMPLETE and not force: self._lock_temple_obstacle() self._update_temple_lock_commodity() self._current_temple_id = self._get_new_temple_id(new_id=new_id) self._temple_state = self.TEMPLE_STATE_RESET self._update_temple_id_for_client() def set_temple_in_progress(self): self._temple_state = self.TEMPLE_STATE_IN_PROGRESS def set_temple_complete(self): self._temple_state = self.TEMPLE_STATE_COMPLETE def _set_temple_obstacle_state(self, state_value): obstacle = self._get_temple_entrance_obstacle() if obstacle is not None: obstacle.set_state(state_value.state, state_value) def _get_temple_entrance_obstacle(self): obstacle = services.object_manager().get_objects_matching_tags((self.TEMPLE_PATH_OBSTACLE,)) if len(obstacle) != 1: logger.error('There should only be one Temple Entrance Path Obstacle. Found {} instead.', len(obstacle), owner='trevor') return return next(iter(obstacle)) def _lock_temple_obstacle(self): self._set_temple_obstacle_state(self.TEMPLE_PATH_OBSTACLE_LOCK_STATE) def _unlock_temple_obstacle(self): self._set_temple_obstacle_state(self.TEMPLE_PATH_OBSTACLE_UNLOCK_STATE) def _get_new_temple_id(self, new_id=None): temples = list(TempleTuning.TEMPLES.keys()) if new_id is not None and new_id in temples and new_id != self._current_temple_id: return new_id if self._current_temple_id is not None: temples.remove(self._current_temple_id) return random.choice(temples) def _update_temple_id_for_client(self): for proto in services.get_persistence_service().zone_proto_buffs_gen(): if proto.lot_description_id == TempleTuning.TEMPLE_LOT_DESCRIPTION: proto.pending_house_desc_id = self._current_temple_id def _save_treasure_chest_data(self, writer): group_ids = [] obj_ids = [] status_ids = [] for (group_id, treasure_chest_data) in self._treasure_chest_status.items(): if group_id == self._current_travel_group_id: continue for (obj_id, curr_status) in treasure_chest_data: group_ids.append(group_id) obj_ids.append(obj_id) status_ids.append(curr_status) if self._current_travel_group_id is None: return for chest in services.object_manager().get_objects_matching_tags((self.treasure_chest_tag,)): if chest.is_on_active_lot(): continue group_ids.append(self._current_travel_group_id) obj_ids.append(chest.id) status_ids.append(self._get_treasure_chest_status(chest)) writer.write_uint64s(TREASURE_CHEST_GROUP, group_ids) writer.write_uint64s(TREASURE_CHEST_ID, obj_ids) writer.write_uint64s(TREASURE_CHEST_STATUS, status_ids) def _get_treasure_chest_status(self, chest): if chest.state_value_active(self.treasure_chest_open_state): return JungleOpenStreetDirector.TREASURE_CHEST_OPEN return JungleOpenStreetDirector.TREASURE_CHEST_CLOSED def _load_treasure_chest_data(self, reader): if reader is None: return travel_group_manager = services.travel_group_manager() group_ids = reader.read_uint64s(TREASURE_CHEST_GROUP, []) obj_ids = reader.read_uint64s(TREASURE_CHEST_ID, []) status = reader.read_uint64s(TREASURE_CHEST_STATUS, []) for (index, group_id) in enumerate(group_ids): if not travel_group_manager.get(group_id): continue if group_id not in self._treasure_chest_status: self._treasure_chest_status[group_id] = [] treasure_chest = self._treasure_chest_status[group_id] treasure_chest.append((obj_ids[index], status[index]))
class FavoritesTest(HasTunableSingletonFactory, AutoFactoryInit, event_testing.test_base.BaseTest): FACTORY_TUNABLES = { 'subject': TunableEnumEntry( description= "\n The subject who's favorite we're testing against.\n ", tunable_type=ParticipantTypeSim, default=ParticipantTypeSim.Actor), 'target': TunableEnumEntry( description= '\n The potential favorite object to test against.\n ', tunable_type=ParticipantTypeObject, default=ParticipantTypeObject.Object), 'favorite_type': TunableVariant( description= "\n The type of favorite that we are testing.\n \n Preferred Object: Test whether the object is a sim's preferred object\n to use for a specific func tag.\n Favorite Stack: Test whether the object is in one of the sim's favorite stacks\n in their inventory.\n ", preferred_object=TunableTuple( description= "\n Test whether the object is a sim's preferred object for\n a specified tag.\n ", tag=TunableTag( description= '\n The tag that represents this type of favorite.\n ', filter_prefixes=('Func', )), instance_must_match=Tunable( description= '\n If checked, the object instance must match the instance\n of the favorite object for this test to pass (barring the\n case where this test is negated). If unchecked, either the\n instance or definition may match for this test to pass.\n ', tunable_type=bool, default=True)), locked_args={'favorite_stack': None}, default='preferred_object'), 'negate': Tunable( description= '\n If checked, the result of this test will be negated. Error cases,\n like subject or target not being found or the subject not having a\n favorites tracker, will always fail.\n ', tunable_type=bool, default=False) } def get_expected_args(self): return {'subject': self.subject, 'target': self.target} def __call__(self, subject=None, target=None): if not (subject and target): logger.error( 'Subject or Target not found while running a Favorites Test') return TestResult(False, 'Subject or Target was not found.', tooltip=self.tooltip) if len(subject) > 1: logger.warn( 'FavoritesTest is being called with more than one participant for subject. Only the first participant will be used.' ) if len(target) > 1: logger.warn( 'FavoritesTest is being called with more than one participant for target. Only the first participant will be used.' ) sim = subject[0] obj = target[0] favorites_tracker = sim.sim_info.favorites_tracker if favorites_tracker is None: logger.error( "Trying to get a favorites tracker for Sim {} but they don't have one.", sim) return TestResult(False, 'Sim {} has no favorites tracker.', sim, tooltip=self.tooltip) if self.favorite_type is not None: if favorites_tracker.is_favorite( self.favorite_type.tag, obj, self.favorite_type.instance_must_match): if self.negate: return TestResult( False, 'Found favorite for Sim. Test is negated.', tooltip=self.tooltip) return TestResult.TRUE if self.negate: return TestResult.TRUE return TestResult(False, 'Object {} is not the favorite for Sim {}', obj, sim, tooltip=self.tooltip) if favorites_tracker.is_favorite_stack(obj): if self.negate: return TestResult( False, 'Obj is part of a favorite stack for Sim. Test is negated.', tooltip=self.tooltip) return TestResult.TRUE if self.negate: return TestResult.TRUE else: return TestResult( False, 'Object {} is not part of a favorite stack for Sim {}', obj, sim, tooltip=self.tooltip)
class InfectedSituation(SubSituationOwnerMixin, SituationComplexCommon): INSTANCE_TUNABLES = {'infected_state': _InfectedState.TunableFactory(display_name='Infected State', tuning_group=SituationComplexCommon.SITUATION_STATE_GROUP), 'possessed_start_time': TunableTimeOfDay(description='\n Time of day Sims become possessed.\n ', tuning_group=GroupNames.SITUATION), 'possessed_duration_hours': TestedSum.TunableFactory(description='\n How long the possession lasts.\n ', tuning_group=GroupNames.SITUATION), 'possessed_situation': Situation.TunableReference(description='\n Possessed situation to place Sim in.\n ', class_restrictions=('PossessedSituation',), tuning_group=GroupNames.SITUATION), 'default_job_and_role': TunableSituationJobAndRoleState(description='\n The job/role the infected Sim will be in.\n ', tuning_group=GroupNames.SITUATION), 'possessed_buff_tag': TunableTag(description='\n Tag for buffs that can add the Possessed Mood through the Infection\n System. Possessed status is refreshed when these buffs are added\n or removed.\n ', filter_prefixes=('Buff',)), 'possessed_buff_no_animate_tag': TunableTag(description='\n Possession buffs with this tag will not play the start possession\n animation.\n ', filter_prefixes=('Buff',)), 'possession_time_buff': TunableBuffReference(description='\n The buff to add to the Sim when it is the possessed start time.\n ')} REMOVE_INSTANCE_TUNABLES = Situation.NON_USER_FACING_REMOVE_INSTANCE_TUNABLES def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._start_possession_alarm = None self._end_possession_alarm = None self._possession_sources = [] @classmethod def _states(cls): return (SituationStateData.from_auto_factory(0, cls.infected_state),) @classmethod def _get_tuned_job_and_default_role_state_tuples(cls): return ((cls.default_job_and_role.job, cls.default_job_and_role.role_state),) @classmethod def default_job(cls): pass @classproperty def situation_serialization_option(cls): return SituationSerializationOption.DONT def start_situation(self): super().start_situation() self._change_state(self.infected_state()) sim_info = self._get_sim_info() for buff in sim_info.Buffs.get_all_buffs_with_tag(self.possessed_buff_tag): self._request_possession(buff.buff_type, animate_possession_override=False) now = services.time_service().sim_now time_span_day = create_time_span(days=1) start_day_time = self._get_possessed_start_day_time() time_span = now.time_till_next_day_time(start_day_time) self._start_possession_alarm = alarms.add_alarm(self, time_span, self._on_possession_start, repeating_time_span=time_span_day, repeating=True) end_day_time = self._get_possessed_end_day_time() sim_info.Buffs.on_buff_added.register(self._on_buff_added) sim_info.Buffs.on_buff_removed.register(self._on_buff_removed) in_possession_window = now.time_between_day_times(start_day_time, end_day_time) if in_possession_window: elapsed_time = create_time_span(days=1) - now.time_till_next_day_time(start_day_time) self._trigger_possession_time(elapsed_time=elapsed_time) def on_remove(self): sim_info = self._get_sim_info() sim_info.Buffs.on_buff_added.unregister(self._on_buff_added) sim_info.Buffs.on_buff_removed.unregister(self._on_buff_removed) super().on_remove() @property def sim_id(self): return self._guest_list.host_sim_id def _get_sim_info(self): sim_info = services.sim_info_manager().get(self.sim_id) return sim_info def _get_possessed_start_day_time(self): return self.possessed_start_time def _get_possessed_end_day_time(self): sim_info = self._get_sim_info() if sim_info is None: logger.error('Missing SimInfo for infected sim') return start_day_time = self._get_possessed_start_day_time() resolver = SingleSimResolver(sim_info) hours = self.possessed_duration_hours.get_modified_value(resolver) end_day_time = start_day_time + create_time_span(hours=hours) return end_day_time def _on_possession_start(self, _): self._trigger_possession_time() def _trigger_possession_time(self, elapsed_time=None): sim_info = self._get_sim_info() if sim_info is None: logger.error('Missing SimInfo for infected sim') return possession_buff = self.possession_time_buff sim_info.add_buff_from_op(possession_buff.buff_type, possession_buff.buff_reason) buff_commodity = sim_info.get_statistic(possession_buff.buff_type.commodity, add=False) if buff_commodity: resolver = SingleSimResolver(sim_info) hours = self.possessed_duration_hours.get_modified_value(resolver) buff_time = hours*60 if elapsed_time is not None: buff_time -= elapsed_time.in_minutes() buff_commodity.set_value(buff_time) def _on_sub_situation_end(self, sub_situation_id): if services.current_zone().is_zone_shutting_down: return if self._possession_sources: sim_info = self._get_sim_info() for source in tuple(self._possession_sources): sim_info.remove_buff_by_type(source) def _start_possession_situation(self, animate_possession_override=None): guest_list = SituationGuestList(invite_only=True, host_sim_id=self.sim_id) animate_possession = services.current_zone().is_zone_running if animate_possession: if animate_possession_override is not None: animate_possession = animate_possession_override self._create_sub_situation(self.possessed_situation, guest_list=guest_list, user_facing=False, animate_possession=animate_possession) def _on_possession_sources_changed(self): sub_situations = self._get_sub_situations() if sub_situations: sub_situations[0].on_possession_sources_changed() def _request_possession(self, source, animate_possession_override=None): if source in self._possession_sources: logger.error('Redundant source: {}', source) return self._possession_sources.append(source) if not self._sub_situation_ids: self._start_possession_situation(animate_possession_override=animate_possession_override) self._on_possession_sources_changed() def _remove_possession_request(self, source): if source not in self._possession_sources: logger.error('Missing source: {}', source) return self._possession_sources.remove(source) self._on_possession_sources_changed() def _on_buff_added(self, buff_type, owner_sim_id): if self.possessed_buff_tag not in buff_type.tags: return animate = None if self.possessed_buff_no_animate_tag in buff_type.tags: animate = False self._request_possession(buff_type, animate_possession_override=animate) def _on_buff_removed(self, buff_type, owner_sim_id): if self.possessed_buff_tag not in buff_type.tags: return self._remove_possession_request(buff_type) def get_possession_source(self): sim_info = self._get_sim_info() if sim_info is None: return (None, None) buff_component = sim_info.Buffs longest_source = None buff_duration = None for source in self._possession_sources: buff = buff_component.get_buff_by_type(source) if buff is None: continue buff_commodity = buff.get_commodity_instance() if buff_commodity is None: longest_source = buff buff_duration = None break buff_value = buff_commodity.get_value() if not buff_duration is None: if buff_value > buff_duration: buff_duration = buff_value longest_source = buff buff_duration = buff_value longest_source = buff return (longest_source, buff_duration)
class ZoneModifier(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager( sims4.resources.Types.ZONE_MODIFIER)): INSTANCE_TUNABLES = { 'zone_modifier_locked': Tunable( description= '\n Whether this is a locked trait that cannot be assigned/removed\n through build/buy.\n ', tunable_type=bool, default=False, export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'enter_lot_loot': TunableSet( description= '\n Loot applied to Sims when they enter or spawn in on the lot while\n this zone modifier is active.\n \n NOTE: The corresponding exit loot is not guaranteed to be given.\n For example, if the Sim walks onto the lot, player switches to a\n different zone, then summons that Sim, that Sim will bypass\n getting the exit loot.\n A common use case for exit lot loot is to remove buffs granted\n by this zone_mod. This case is already covered as buffs are \n automatically removed if they are non-persistable (have no associated commodity)\n ', tunable=LootActions.TunableReference(pack_safe=True), tuning_group=GroupNames.LOOT), 'exit_lot_loot': TunableSet( description= '\n Loot applied to Sims when they exit or spawn off of the lot while\n this zone modifier is active.\n \n NOTE: This loot is not guaranteed to be given after the enter loot.\n For example, if the Sim walks onto the lot, player switches to a\n different zone, then summons that Sim, that Sim will bypass\n getting the exit loot.\n A common use case for exit lot loot is to remove buffs granted\n by this zone_mod. This case is already covered as buffs are \n automatically removed if they are non-persistable (have no associated commodity)\n ', tunable=LootActions.TunableReference(pack_safe=True), tuning_group=GroupNames.LOOT), 'interaction_triggers': TunableList( description= '\n A mapping of interactions to possible loots that can be applied\n when an on-lot Sim executes them if this zone modifier is set.\n ', tunable=ZoneInteractionTriggers.TunableFactory()), 'schedule': ZoneModifierWeeklySchedule.TunableFactory( description= '\n Schedule to be activated for this particular zone modifier.\n ' ), 'household_actions': TunableList( description= '\n Actions to apply to the household that owns this lot when this zone\n modifier is set.\n ', tunable=ZoneModifierHouseholdActionVariants( description= '\n The action to apply to the household.\n ' )), 'object_tag_to_actions': TunableMapping( description= '\n Mapping of object tag to zone modifier from object actions. Objects \n in this tuning can be buy objects, build objects (column, window, pool),\n and materials (floor tiles, roof tiles, wallpaper).\n \n This is primarily intended for architectural elements such as wallpaper, \n roof materials, windows will give effect to utilities and eco footprint.\n \n NOTE: The actions will only be applied if user enables the \n "Architecture Affects Eco Living" option under Game Options.\n ', key_type=TunableTag( description= '\n The object tag that will be used to do actions.\n ' ), value_type=TunableList( description= '\n The list of action to apply.\n ', tunable=ZoneModifierFromObjectsActionVariants())), 'prohibited_situations': OptionalTunable( description= '\n Optionally define if this zone should prevent certain situations\n from running or getting scheduled.\n ', tunable=SituationIdentityTest.TunableFactory( description= '\n Prevent a situation from running if it is one of the specified \n situations or if it contains one of the specified tags.\n ' )), 'venue_requirements': TunableVariant( description= '\n Whether or not we use a blacklist or white list for the venue\n requirements on this zone modifier.\n ', allowed_venue_types=TunableSet( description= '\n A list of venue types that this Zone Modifier can be placed on.\n All other venue types are not allowed.\n ', tunable=TunableReference( description= '\n A venue type that this Zone Modifier can be placed on.\n ', manager=services.get_instance_manager( sims4.resources.Types.VENUE), pack_safe=True)), prohibited_venue_types=TunableSet( description= '\n A list of venue types that this Zone Modifier cannot be placed on.\n ', tunable=TunableReference( description= '\n A venue type that this Zone Modifier cannot be placed on.\n ', manager=services.get_instance_manager( sims4.resources.Types.VENUE), pack_safe=True)), export_modes=ExportModes.All), 'conflicting_zone_modifiers': TunableSet( description= '\n Conflicting zone modifiers for this zone modifier. If the lot has any of the\n specified zone modifiers, then it is not allowed to be equipped with this\n one.\n ', tunable=TunableReference(manager=services.get_instance_manager( sims4.resources.Types.ZONE_MODIFIER), pack_safe=True), export_modes=ExportModes.All), 'additional_situations': SituationCurve.TunableFactory( description= "\n An additional schedule of situations that can be added in addition\n a situation scheduler's source tuning.\n ", get_create_params={'user_facing': False}), 'zone_wide_loot': ZoneModifierUpdateAction.TunableFactory( description= '\n Loots applied when spawning into a zone with \n this zone modifier. This loot is also applied to all sims, \n objects, etc. in the zone when this zone modifier is added to a lot.\n ', tuning_group=GroupNames.LOOT), 'cleanup_loot': ZoneModifierUpdateAction.TunableFactory( description= '\n Loots applied when this zone modifier is removed.\n ', tuning_group=GroupNames.LOOT), 'on_add_loot': ZoneModifierUpdateAction.TunableFactory( description= '\n Loots applied when this zone modifier is added.\n ', tuning_group=GroupNames.LOOT), 'spin_up_lot_loot': ZoneModifierUpdateAction.TunableFactory( description= '\n Loots applied when the zone spins up.\n ', tuning_group=GroupNames.LOOT), 'utility_supply_surplus_loot': TunableMapping( description= '\n Loots applied when utility supply statistic change\n from deficit to surplus.\n ', key_type=TunableEnumEntry( description= '\n The utility that we want to listen for supply change.\n ', tunable_type=Utilities, default=Utilities.POWER), value_type=ZoneModifierUpdateAction.TunableFactory( description= '\n Loots to apply.\n '), tuning_group=GroupNames.LOOT), 'utility_supply_deficit_loot': TunableMapping( description= '\n Loots applied when utility supply statistic change\n from surplus to deficit.\n ', key_type=TunableEnumEntry( description= '\n The utility that we want to listen for supply change.\n ', tunable_type=Utilities, default=Utilities.POWER), value_type=ZoneModifierUpdateAction.TunableFactory( description= '\n Loots to apply.\n '), tuning_group=GroupNames.LOOT), 'ignore_route_events_during_zone_spin_up': Tunable( description= "\n Don't handle sim route events during zone spin up. Useful for preventing\n unwanted loot from being applied when enter_lot_loot runs situation blacklist tests.\n If we require sims to retrieve loot on zone spin up, we can tune spin_up_lot_loot. \n ", tunable_type=bool, default=False), 'hide_screen_slam': Tunable( description= '\n If checked, this zone modifier will not show the usual screen slam\n when first applied.\n ', tunable_type=bool, default=False, tuning_group=GroupNames.UI) } _obj_tag_id_to_count = None @classproperty def obj_tag_id_to_count(cls): return cls._obj_tag_id_to_count @classmethod def on_start_actions(cls): cls.register_interaction_triggers() @classmethod def on_spin_up_actions(cls, is_build_eco_effects_enabled): sim_spawner_service = services.sim_spawner_service() if not sim_spawner_service.is_registered_sim_spawned_callback( cls.zone_wide_loot.apply_to_sim): sim_spawner_service.register_sim_spawned_callback( cls.zone_wide_loot.apply_to_sim) cls.spin_up_lot_loot.apply_all_actions() cls.zone_wide_loot.apply_all_actions() cls.apply_object_actions(is_build_eco_effects_enabled) @classmethod def on_add_actions(cls, is_build_eco_effects_enabled): sim_spawner_service = services.sim_spawner_service() if not sim_spawner_service.is_registered_sim_spawned_callback( cls.zone_wide_loot.apply_to_sim): sim_spawner_service.register_sim_spawned_callback( cls.zone_wide_loot.apply_to_sim) cls.register_interaction_triggers() cls.start_household_actions() cls.on_add_loot.apply_all_actions() cls.zone_wide_loot.apply_all_actions() cls.apply_object_actions(is_build_eco_effects_enabled) @classmethod def on_stop_actions(cls): services.sim_spawner_service().unregister_sim_spawned_callback( cls.zone_wide_loot.apply_to_sim) cls.unregister_interaction_triggers() cls.stop_household_actions() cls.revert_object_actions() @classmethod def on_remove_actions(cls): services.sim_spawner_service().unregister_sim_spawned_callback( cls.zone_wide_loot.apply_to_sim) cls.unregister_interaction_triggers() cls.stop_household_actions() cls.cleanup_loot.apply_all_actions() cls.revert_object_actions() @classmethod def on_utility_supply_surplus(cls, utility): if utility in cls.utility_supply_surplus_loot: cls.utility_supply_surplus_loot[utility].apply_all_actions() @classmethod def on_utility_supply_deficit(cls, utility): if utility in cls.utility_supply_deficit_loot: cls.utility_supply_deficit_loot[utility].apply_all_actions() @classmethod def handle_event(cls, sim_info, event, resolver): if event not in InteractionTestEvents: return sim = sim_info.get_sim_instance() if sim is None or not sim.is_on_active_lot(): return for trigger in cls.interaction_triggers: trigger.handle_interaction_event(sim_info, event, resolver) @classmethod def start_household_actions(cls): if not cls.household_actions: return household_id = services.owning_household_id_of_active_lot() if household_id is not None: for household_action in cls.household_actions: household_action.start_action(household_id) @classmethod def stop_household_actions(cls): if not cls.household_actions: return household_id = services.owning_household_id_of_active_lot() if household_id is not None: for household_action in cls.household_actions: household_action.stop_action(household_id) @classmethod def _on_build_objects_environment_score_update(cls): household = services.active_household() if household is None: return for sim in household.instanced_sims_gen( allow_hidden_flags=ALL_HIDDEN_REASONS): sim.on_build_objects_environment_score_update() @classmethod def apply_object_actions(cls, is_build_eco_effects_enabled): if not is_build_eco_effects_enabled: return if not cls.object_tag_to_actions: return object_tags = list(cls.object_tag_to_actions.keys()) curr_obj_tag_id_to_count = services.active_lot( ).get_object_count_by_tags(object_tags) if cls._obj_tag_id_to_count is None: delta_obj_tag_id_to_count = curr_obj_tag_id_to_count else: delta_obj_tag_id_to_count = { key: curr_obj_tag_id_to_count[key] - cls._obj_tag_id_to_count[key] for key in curr_obj_tag_id_to_count } zone = services.current_zone() for (obj_tag_id, obj_count) in delta_obj_tag_id_to_count.items(): if obj_count != 0: for action in cls.object_tag_to_actions[Tag(obj_tag_id)]: success = action.apply(obj_count) if not success: continue if action.action_type == ZoneModifierFromObjectsActionType.STATISTIC_CHANGE: zone.zone_architectural_stat_effects[ action.stat.guid64] += action.get_value(obj_count) cls._on_build_objects_environment_score_update() cls._obj_tag_id_to_count = curr_obj_tag_id_to_count @classmethod def revert_object_actions(cls): if not cls._obj_tag_id_to_count: return zone = services.current_zone() for (obj_tag_id, obj_count) in cls._obj_tag_id_to_count.items(): if obj_count != 0: for action in cls.object_tag_to_actions[Tag(obj_tag_id)]: success = action.revert(obj_count) if not success: continue if action.action_type == ZoneModifierFromObjectsActionType.STATISTIC_CHANGE: zone.zone_architectural_stat_effects[ action.stat.guid64] -= action.get_value(obj_count) cls._on_build_objects_environment_score_update() cls._obj_tag_id_to_count = None @classmethod def register_interaction_triggers(cls): services.get_event_manager().register_tests(cls, cls._get_trigger_tests()) @classmethod def unregister_interaction_triggers(cls): services.get_event_manager().unregister_tests(cls, cls._get_trigger_tests()) @classmethod def _get_trigger_tests(cls): tests = list() for trigger in cls.interaction_triggers: tests.extend(trigger.get_trigger_tests()) return tests @classmethod def is_situation_prohibited(cls, situation_type): if cls.prohibited_situations is None: return False return cls.prohibited_situations(situation=situation_type)
class CASMenuItem(metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(Types.CAS_MENU_ITEM)): class RemoveActions(enum.Int): DISALLOW_REMOVAL = 0 REMOVE_PART = 1 REMOVE_PAINT_LAYER = 2 INSTANCE_TUNABLES = { 'icons': CASMenuItemIconSet( description= '\n Default icons to use for the menu item.\n ', tuning_group=GroupNames.UI, export_modes=ExportModes.ClientBinary), 'icon_overrides': TunableList( description= '\n List of possible overrides for the icons for this menu item.\n Each override will be evaluated in the order listed, the last\n override to satisfy its criteria will be used.\n ', tunable=TunableTuple( override_criteria=CASContextCriterionList( description= '\n Criteria to determine when this set of icons should be used.\n ' ), icons=CASMenuItemIconSet( description= '\n Icons to use instead of the default icons.\n Note that you must specify all the desired icons here, even\n if they differ from the defaults. For example, if you wish\n to only override the selected icon, you still need to specify\n the default icon here as well.\n ' ), export_class_name='CASMenuItemIconOverride'), export_modes=ExportModes.ClientBinary, tuning_group=GroupNames.UI), 'name': TunableLocalizedStringFactory( description='\n Item name\n ', export_modes=ExportModes.ClientBinary, tuning_group=GroupNames.UI), 'actions': TunableTuple( description= '\n Actions to perform when this menu item is selected.\n ', change_menu_state=OptionalTunable( TunableTuple( description= '\n MenuState to change into. This value must be supported by the\n code, so this will most likely be provided by an engineer.\n To be deprecated. It is part of the old system.\n ', menu_type=TunableEnumEntry( tunable_type=CASMenuState.MenuType, default=CASMenuState.MenuType.NONE), menu_mode=TunableEnumEntry( tunable_type=CASMenuState.MenuMode, default=CASMenuState.MenuMode.NONE), menu_section=TunableEnumEntry( tunable_type=CASMenuState.MenuSection, default=CASMenuState.MenuSection.NONE), menu_item=TunableEnumEntry( tunable_type=CASMenuState.MenuItem, default=CASMenuState.MenuItem.NONE), export_class_name='CASMenuState')), display_ui_widget=OptionalTunable( Tunable( description= '\n Name of a UI widget that should be shown when this item is selected.\n ', tunable_type=str, default='')), export_class_name='CASMenuItemActions', export_modes=ExportModes.ClientBinary), 'remove_action': TunableEnumEntry( description= '\n What action to perform when the remove button is clicked, if present.\n ', tunable_type=RemoveActions, default=RemoveActions.REMOVE_PART, export_modes=ExportModes.ClientBinary), 'audio_component_name': Tunable( description= '\n Optional name to associate with this item for the UI audio system.\n ', tunable_type=str, default='', allow_empty=True, tuning_group=GroupNames.UI, export_modes=ExportModes.ClientBinary), 'disable_reasons': TunableList( description= '\n List of possible conditions under which this menu item should appear disabled.\n ', tunable=TunableTuple( criteria=CASContextCriterionList( description= '\n Criteria which defines when this menu item should be disabled for\n the given reason.\n ' ), reason=TunableLocalizedStringFactory( description= '\n The reason for disabling this menu item.\n ' ), export_class_name='CASMenuItemDisableReason'), tuning_group=GroupNames.UI, export_modes=ExportModes.ClientBinary), 'sim_click_activations': TunableList( description= '\n Defines a list of areas on the sim that when clicked should cause\n this menu item to become selected.\n ', tunable=TunableTuple( criteria=CASContextCriteria( description= '\n Criteria to determine if this click source should be enabled.\n ' ), click_source=TunableVariant( description= '\n The possible areas on which a sim might be clicked.\n \n Note: Clothing is only a valid option for pet sims.\n ', locked_args={'clothing': True}, body_types=TunableEnumSet( description= '\n Areas corresponding to sim BodyTypes.\n ', enum_type=BodyType, enum_default=BodyType.NONE), sim_regions=TunableEnumSet( description= '\n Areas corresponding to SimRegions.\n ', enum_type=SimRegion, enum_default=SimRegion.INVALID)), export_class_name='CASMenuItemSimClickActivation'), export_modes=ExportModes.ClientBinary), 'part_tags': TunableSet( description= '\n Tags to use for what parts to include in the displayed catalog.\n (09/26/2017 - Currently only one tag is supported but will be expanded to multiple later.)\n ', tunable=TunableTag(), maxlength=1, export_modes=ExportModes.ClientBinary) }
class FavoriteObjectSituationMixin: INSTANCE_TUNABLES = {'_favorite_objects': TunableList(description='\n A list of favorites objects to give to Sims when they enter this\n situation. These favorites will be removed from the Sim when the \n situation ends.\n ', tunable=TunableTuple(description='\n Favorite data to add to the Sim.\n ', favorite_tag=TunableTag(description='\n The tag for this favorite object.\n ', filter_prefixes=('func',)), potential_favorites=TunableSet(description='\n A set of potential objects. One of these will be chosen at\n random.\n ', tunable=TunableReference(description='\n The definition of the favorite object.\n ', manager=services.definition_manager()), minlength=1)), tuning_group=GroupNames.SITUATION)} def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._favorite_types_added = defaultdict(set) def _on_set_sim_job(self, sim, job_type): super()._on_set_sim_job(sim, job_type) favorites_tracker = sim.sim_info.favorites_tracker if favorites_tracker is None: return for favorite_data in self._favorite_objects: favorite_tag = favorite_data.favorite_tag if favorites_tracker.has_favorite(favorite_tag): continue self._favorite_types_added[sim.id].add(favorite_tag) favorite_object = random.choice(list(favorite_data.potential_favorites)) favorites_tracker.set_favorite(favorite_tag, obj_def_id=favorite_object.id) def _on_remove_sim_from_situation(self, sim): favorites = self._favorite_types_added.get(sim.id, None) if favorites: favorites_tracker = sim.sim_info.favorites_tracker for favorite in favorites: favorites_tracker.clear_favorite_type(favorite) super()._on_remove_sim_from_situation(sim)
class CASTuning: CAS_BRANDED_TAG_DATA = TunableList( description= '\n A list of CAS tag to data used to show a branded logo on the item\n ', tunable=TunableTuple( description= '\n Tuning for branded logo to use.\n ', tag=TunableTag( description= '\n Tag to use for the brand to be displayed\n ' ), icon=TunableIcon( description= '\n Icon to be displayed on the item\n ' ), background_type=TunableEnumEntry( description= '\n Background to be used for it\n ', tunable_type=CASBrandedLogoBackground, default=CASBrandedLogoBackground.LIGHT), export_class_name='CasBrandedTagEntry'), export_modes=ExportModes.ClientBinary) CAS_SPECIES_PAINT_POSES = TunableMapping( description= '\n A mapping of species type to data that is required for the paint pose ui\n ', key_type=TunableEnumEntry( description= '\n The species type that this entry applies to.\n ', tunable_type=SpeciesExtended, default=SpeciesExtended.HUMAN, invalid_enums=(SpeciesExtended.INVALID, )), value_type=TunableList( description= '\n A list of CasPaintPostTuples\n ', tunable=TunableTuple( description= '\n Data required for each UI Paint pose button.\n ', icon=TunableIcon( description= '\n Icon to be displayed on the button for the pose\n ', tuning_group=GroupNames.UI), icon_selected=TunableIcon( description= '\n Icon to be displayed on the button when the pose button is selected\n ', tuning_group=GroupNames.UI), pose=TunableEnumEntry( description= '\n The pose to play when the button is pressed\n ', tunable_type=CASPaintPose, default=CASPaintPose.NONE), export_class_name='CasPaintPoseTuple')), export_modes=ExportModes.ClientBinary, tuple_name='CasPaintPoseKeyValue') CAS_VOICES_DATA = TunableMapping( description= '\n A mapping of species type to data required for the personality panel ui.\n ', key_type=TunableEnumEntry( description= '\n The species type that this entry applies to.\n ', tunable_type=SpeciesExtended, default=SpeciesExtended.HUMAN, invalid_enums=(SpeciesExtended.INVALID, )), value_type=TunableMapping( description= '\n A mapping of age type to data required for displaying voices in the ui.\n ', key_type=TunableEnumEntry( description= '\n The age that this entry applies to.\n ', tunable_type=Age, default=Age.ADULT), value_type=TunableList( description= '\n a list of voice data for this species at this age.\n ', tunable=TunableTuple( description= '\n data required to display this voice in the ui.\n ', icon=TunableIcon( description= '\n Icon to be displayed as voice button.\n ', tuning_group=GroupNames.UI), icon_selected=TunableIcon( description= '\n Icon to be displayed as voice button when it is selected.\n ', tuning_group=GroupNames.UI), tooltip=TunableLocalizedString( description= '\n Localized name of this voice.\n ' ), export_class_name='CasVoicesDataTuple')), tuple_name='CasVoicesAgeKeyValue'), export_modes=ExportModes.ClientBinary, tuple_name='CasVoicesSpeciesKeyValue') CAS_RANDOMIZE_FILTERS = TunableMapping( description= '\n An Ordered list of randomization menu items that will appear in the randomization panel ui in CAS. \n The list is filtered by the criteria on each menu item.\n ', key_type=Tunable( description= '\n An integer value used to set the specific order of the menu items\n in the ui. The lower numbers are displayed first in the ui.\n ', tunable_type=int, default=0), value_type=TunableTuple( description= '\n A randomization menu item and its inclusion (and/or exclusion) criteria.\n ', criteria=CASContextCriterionList( description= '\n Use this menu item if all of the specified criteria are met.\n ' ), flags=TunableList( description= '\n A list of randomization flags for this item.\n ', tunable=TunableEnumEntry( description= '\n A randomization flag.\n ', tunable_type=CASRandomizeFlag, default=CASRandomizeFlag.RANDOMIZE_BY_MENUSTATE)), name=TunableLocalizedString( description= '\n The name of this menu item displayed in the ui.\n ' ), required_flags=TunableList( description= '\n A list of randomization flags that are required to be enabled \n in order for this menu item to be enabled. \n ', tunable=TunableEnumEntry( description= '\n A randomization flag.\n ', tunable_type=CASRandomizeFlag, default=CASRandomizeFlag.RANDOMIZE_BY_MENUSTATE)), export_class_name='CasRandomizeItemTuple'), tuple_name='CasRandomizeItemsKeyValue', export_modes=(ExportModes.ClientBinary, )) CAS_COPY_FILTERS = TunableList( description= '\n An Ordered list of copy menu items that will appear in the randomization panel ui in CAS. \n The list is filtered by the criteria on each menu item.\n ', tunable=TunableTuple( description= '\n A copy menu item and its inclusion (and/or exclusion) criteria.\n ', criteria=CASContextCriterionList( description= '\n Use this menu item if all of the specified criteria are met.\n ' ), flags=TunableList( description= '\n A list of copy flags for this item.\n ', tunable=TunableEnumEntry( description= '\n A copy flag.\n ', tunable_type=CASRandomizeFlag, default=CASRandomizeFlag.RANDOMIZE_BY_MENUSTATE)), name=TunableLocalizedString( description= '\n The name of this menu item displayed in the ui.\n ' ), required_flags=TunableList( description= '\n A list of copy flags that are required to be enabled \n in order for this menu item to be enabled. \n ', tunable=TunableEnumEntry( description= '\n A copy flag.\n ', tunable_type=CASRandomizeFlag, default=CASRandomizeFlag.RANDOMIZE_BY_MENUSTATE)), export_class_name='CasCopyItemEntry'), export_modes=(ExportModes.ClientBinary, )) CAS_ADD_SIM_MENU_DATA = TunableMapping( description= '\n An ordered mapping of menu data used for the Add Sim portion of CAS.\n ', key_name='index', key_type=Tunable( description= '\n The order in which these entries should be added. 1 is first, 2 is\n second, etc.\n ', tunable_type=int, default=0), value_name='data', value_type=TunableTuple( description= '\n Data associated with an add Sim button in CAS.\n ', criteria=CASContextCriterionList( description= '\n Only add this menu item if the criteria are met.\n ' ), parent_index=Tunable( description= '\n The index of the parent entry if this is a child to another\n entry in the list. 0 if this entry has no parent.\n ', tunable_type=int, default=0), tooltip=TunableLocalizedString( description= '\n The tooltip when hovering over this entry.\n ', allow_none=True), icon=TunableResourceKey( description= '\n The icon for this entry.\n ', allow_none=True, pack_safe=True), icon_selected=TunableResourceKey( description= '\n The icon when this entry is selected.\n ', allow_none=True, pack_safe=True), audio_name=Tunable( description= '\n The audio to play when this entry is selected.\n ', tunable_type=str, default='', allow_empty=True), flair_name=Tunable( description= '\n Flair to apply to this entry (for instance, god rays).\n ', tunable_type=str, default='', allow_empty=True), tutorial_control_enum=TunableEnumEntry( description= '\n The enum used for tutorial controls. UI_INVALID should be\n used if this entry has no tutorial control.\n ', tunable_type=TutorialTipUiElement, default=TutorialTipUiElement.UI_INVALID), action=TunableEnumEntry( description= '\n The action to take when clicking this entry.\n ', tunable_type=CASAddSimAction, default=CASAddSimAction.ACTION_NONE), species=TunableEnumEntry( description= "\n The species for this entry. Species.INVALID indicates no\n preference or it's not relevant to this menu entry.\n ", tunable_type=SpeciesExtended, default=SpeciesExtended.INVALID), occult_type=TunableEnumFlags( description= '\n The occult type for this entry, if any.\n ', enum_type=OccultType, allow_no_flags=True), limit_genetics_species=TunableEnumSet( description= '\n Species in this list will only be allowed through if the action\n for this entry is GENETICS. This is very likely only going to be\n used for pet genetics.\n ', enum_type=SpeciesExtended, enum_default=SpeciesExtended.INVALID, allow_empty_set=True), export_class_name='CasAddSimMenuData'), tuple_name='CasAddSimMenuDataKeyValue', export_modes=(ExportModes.ClientBinary, ))
class OutfitChangeTags(HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = {'outfit_change_data': TunableList(description='\n List of data corresponding at possible outfits and tests that will\n generate change outfit affordances.\n ', tunable=TunableTuple(description='\n Outfits and tests that will generate the change outfit\n interactions.\n ', outfit_tag=TunableTag(description='\n Outfit tag that will generate the outfit change \n interactions.\n ', filter_prefixes=('Uniform', 'OutfitCategory', 'Style', 'Situation')), outfit_tests=TunableTestSet(description='\n Tests the Sim should pass to be able to change into the\n outfit.\n ')))} def outfit_affordances_gen(self, sim, target, affordance, **kwargs): resolver = SingleSimResolver(sim.sim_info) for outfit_data in self.outfit_change_data: if outfit_data.outfit_tests.run_tests(resolver): yield AffordanceObjectPair(affordance, target, affordance, None, pie_menu_cateogory=affordance.category, outfit_tags=(outfit_data.outfit_tag,), **kwargs) def get_outfit_tags(self): outfit_tags = set() for outfit_data in self.outfit_change_data: outfit_tags.add(outfit_data.outfit_tag) return outfit_tags def get_outfit_for_clothing_change(self, sim_info, outfit_change_category): return (outfit_change_category, 0)
class SpawnOnVehicleActionAffordance(HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = { 'vehicle_obj_tag': TunableTag( description= '\n The tag to use to look up if the sim has a favorite vehicle\n to use for the spawn action.\n ', filter_prefixes=('Func', )) } @TunableFactory.factory_option def list_pack_safe(list_pack_safe=False): tuning_name = 'fallback_vehicle_def' description = "\n The definition of the vehicle to spawn if the sim doesn't have\n a favorite vehicle.\n " if list_pack_safe: return { tuning_name: TunableReference(description=description, manager=services.definition_manager(), pack_safe=True) } return { tuning_name: TunablePackSafeReference(description=description, manager=services.definition_manager()) } def __call__(self, sim): def _abort(vehicle_obj): sim.allow_opacity_change = True sim.fade_in() if vehicle_obj is not None: vehicle_obj.destroy() vehicle = None if sim.sim_info.favorites_tracker is not None: favorites_tracker = sim.sim_info.favorites_tracker definition_manager = services.definition_manager() vehicle_def_id = favorites_tracker.get_favorite_definition_id( self.vehicle_obj_tag) if vehicle_def_id is not None: vehicle_def = definition_manager.get(vehicle_def_id) if vehicle_def is not None: vehicle = objects.system.create_object(vehicle_def) if vehicle is None: if self.fallback_vehicle_def is None: _abort(vehicle) return True vehicle = create_object(self.fallback_vehicle_def) if vehicle is None: _abort(vehicle) return True vehicle.set_household_owner_id(sim.household_id) starting_location = placement.create_starting_location( position=sim.position) fgl_context = placement.create_fgl_context_for_object( starting_location, vehicle) (position, orientation) = placement.find_good_location(fgl_context) if position is None or orientation is None: _abort(vehicle) return True vehicle.transform = sims4.math.Transform(position, orientation) result = vehicle.vehicle_component.push_drive_affordance( sim, priority=Priority.Critical) if result is None: _abort(vehicle) return True if result.interaction is None: logger.warn( "Vehicle's push drive affordance {} resulted in a None interaction. Result: {}.", vehicle.vehicle_component.drive_affordance, result, owner='jmorrow') _abort(vehicle) return True sim.fade_in() vehicle.claim() for situation in services.get_zone_situation_manager().get_all(): if sim in situation.all_sims_in_situation_gen(): situation.manage_vehicle(vehicle) break return True