class StoryProgressionActionFame(_StoryProgressionAction): FACTORY_TUNABLES = { 'time_of_day': TunableTuple( description= '\n Only run this action when it is between a certain time of day.\n ', start_time=TunableTimeOfDay(default_hour=2), end_time=TunableTimeOfDay(default_hour=6)) } def should_process(self, options): current_time = services.time_service().sim_now if not current_time.time_between_day_times(self.time_of_day.start_time, self.time_of_day.end_time): return False return True def process_action(self, story_progression_flags): if FameTunables.FAME_RANKED_STATISTIC is None: return played_famous = 0 non_played_famous = 0 non_played_fame_level = Counter() for sim_info in services.sim_info_manager().get_all(): if sim_info.lod == SimInfoLODLevel.MINIMUM: continue fame_stat = sim_info.get_statistic( FameTunables.FAME_RANKED_STATISTIC) if fame_stat.rank_level >= 1: if sim_info.is_player_sim: played_famous += 1 else: non_played_famous += 1 non_played_fame_level[fame_stat.rank_level] += 1 with telemetry_helper.begin_hook(fame_telemetry_writer, TELEMETRY_HOOK_FAME) as hook: hook.write_int(TELEMETRY_FIELD_FAME_PLAYED, played_famous) hook.write_int(TELEMETRY_FIELD_FAME_NON_PLAYED, non_played_famous) hook.write_int(TELEMETRY_FIELD_FAME_ONE_STAR_NON_PLAYED, non_played_fame_level[1]) hook.write_int(TELEMETRY_FIELD_FAME_TWO_STAR_NON_PLAYED, non_played_fame_level[2]) hook.write_int(TELEMETRY_FIELD_FAME_THREE_STAR_NON_PLAYED, non_played_fame_level[3]) hook.write_int(TELEMETRY_FIELD_FAME_FOUR_STAR_NON_PLAYED, non_played_fame_level[4]) hook.write_int(TELEMETRY_FIELD_FAME_FIVE_STAR_NON_PLAYED, non_played_fame_level[5])
class ScheduleEntry(AutoFactoryInit): __qualname__ = 'ScheduleEntry' FACTORY_TUNABLES = { 'description': '\n A map of days of the week to start time and duration.\n ', 'days_available': TunableDayAvailability(), 'start_time': TunableTimeOfDay(default_hour=9), 'duration': Tunable(description='Duration of this work session in hours.', tunable_type=float, default=1.0), 'random_start': Tunable( bool, False, description= '\n If True, This schedule will have a random start time in the tuned window\n each time.\n ', needs_tuning=True) } def __init__(self, **kwargs): super().__init__(**kwargs) self._start_and_end_times = set() for (day, day_enabled) in self.days_available.items(): while day_enabled: days_as_time_span = date_and_time.create_time_span(days=day) start_time = self.start_time + days_as_time_span end_time = start_time + date_and_time.create_time_span( hours=self.duration) self._start_and_end_times.add((start_time, end_time)) def get_start_and_end_times(self): return self._start_and_end_times
class ZoneDirectorEventListener(AspirationBasic): INSTANCE_TUNABLES = { 'valid_times': TunableList( description= '\n The valid times that this event listener can be completed.\n ', tunable=TunableTuple( description= '\n A period time that this event listener can be completed.\n ', start_time=TunableTimeOfDay( description= '\n The start of this period of time that this event listener\n can be completed.\n ', default_hour=9), end_time=TunableTimeOfDay( description= '\n The end time of this period of time that this event\n listener can be completed.\n ', default_hour=17))) } @classmethod def _verify_tuning_callback(cls): for objective in cls.objectives: if not objective.resettable: logger.error('Objective {} tuned in {} is not resettable.', objective, cls) @constproperty def aspiration_type(): return AspriationType.ZONE_DIRECTOR @classmethod def handle_event(cls, sim_info, event, resolver): if sim_info is None: return if sim_info.aspiration_tracker is None: return now = services.time_service().sim_now if not any( now.time_between_day_times(time_period.start_time, time_period.end_time) for time_period in cls.valid_times): return sim_info.aspiration_tracker.handle_event(cls, event, resolver)
def __init__(self, *args, **kwargs): super().__init__( *args, schedule=TunableList(tunable=TunableTuple( description= "\n Define a Sim's sleep pattern by applying buffs at\n certain times before their scheduled work time. If Sim's\n don't have a job, define an arbitrary time and define\n buffs relative to that.\n ", time_from_work_start=Tunable( description= '\n The time relative to the start work time that the buff\n should be added. For example, if you want the Sim to\n gain this static commodity 10 hours before work, set\n this value to 10.\n ', tunable_type=float, default=0), buff=TunableBuffReference( description= '\n Buff that gets added to the Sim.\n ', allow_none=True))), default_work_time=TunableTimeOfDay( description= "\n The default time that the Sim assumes he needs to be at work\n if he doesn't have a career. This is only used for sleep.\n ", default_hour=9), **kwargs)
class TimeOfDayTrigger(BaseSituationTrigger): FACTORY_TUNABLES = { 'time': TunableTimeOfDay( description= '\n The time of day that this trigger will occur at.\n ' ) } def _duration_complete(self, _): self._has_triggered = True self._effect(self._owner) def _setup(self, reader): now = services.game_clock_service().now() self._owner._create_or_load_alarm_with_timespan( DURATION_ALARM_KEY.format(self._index), now.time_till_next_day_time(self.time), self._duration_complete, reader=reader, should_persist=False)
class SicknessTuning: SICKNESS_TIME_OF_DAY = TunableTimeOfDay( description= '\n Hour of day in which sicknesses will be distributed to Sims.\n ', default_hour=3) SICKNESS_TESTS = TunableTestSet( description= '\n Test sets determining whether or not a given Sim may become sick at all.\n These tests run before we attempt to roll on whether or not \n the Sim can avoid becoming sick. \n (ORs of ANDs)\n ' ) SICKNESS_CHANCE = TestedSum.TunableFactory( description= '\n Chance of any given Sim to become sick. \n \n Chance is out of 100.\n \n When the sum of the base value and values from passed tests are\n greater than 100, the Sim is guaranteed to become sick during a \n sickness distribution pass.\n \n When 0 or below, the Sim will not get sick.\n ' ) PREVIOUS_SICKNESSES_TO_TRACK = TunableRange( description= '\n Number of previous sicknesses to track. Can use this to help promote\n variation of sicknesses a Sim receives over time.', tunable_type=int, minimum=0, default=1) EXAM_TYPES_TAGS = TunableTags( description= '\n Tags that represent the different types of objects that are used\n to run exams.\n ', filter_prefixes=('interaction', ))
class BarSpecialNightSituation(SituationComplexCommon): INSTANCE_TUNABLES = {'end_time': TunableTimeOfDay(description='\n The time that this situation will end.\n '), 'special_night_patron': TunableSituationJobAndRoleState(description='\n The job and role of the special night patron.\n '), 'notification': TunableUiDialogNotificationSnippet(description='\n The notification to display when this object reward is granted\n to the Sim. There is one additional token provided: a string\n representing a bulleted list of all individual rewards granted.\n '), 'starting_entitlement': OptionalTunable(description='\n If enabled, this situation is locked by an entitlement. Otherwise,\n this situation is available to all players.\n ', tunable=TunableEnumEntry(description='\n Pack required for this event to start.\n ', tunable_type=Pack, default=Pack.BASE_GAME)), 'valid_regions': TunableWhiteBlackList(description='\n A white/black list of regions in which this schedule entry is valid.\n For instance, some bar nights might not be valid in the Jungle bar.\n ', tunable=Region.TunableReference(pack_safe=True))} REMOVE_INSTANCE_TUNABLES = Situation.NON_USER_FACING_REMOVE_INSTANCE_TUNABLES @classmethod def _states(cls): return (SituationStateData(1, _BarSpecialNightSituationState),) @classmethod def _get_tuned_job_and_default_role_state_tuples(cls): return [(cls.special_night_patron.job, cls.special_night_patron.role_state)] @classmethod def default_job(cls): pass @classmethod def situation_meets_starting_requirements(cls, **kwargs): if not cls.valid_regions.test_item(services.current_region()): return False if cls.starting_entitlement is None: return True return is_available_pack(cls.starting_entitlement) def _get_duration(self): time_now = services.time_service().sim_now return time_now.time_till_next_day_time(self.end_time).in_minutes() def start_situation(self): super().start_situation() self._change_state(_BarSpecialNightSituationState()) dialog = self.notification(services.active_sim_info()) dialog.show_dialog() def _issue_requests(self): request = BouncerRequestFactory(self, callback_data=_RequestUserData(role_state_type=self.special_night_patron.role_state), job_type=self.special_night_patron.job, request_priority=BouncerRequestPriority.BACKGROUND_LOW, user_facing=self.is_user_facing, exclusivity=self.exclusivity) self.manager.bouncer.submit_request(request)
class ScheduleEntry(HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = {'days_available': TunableDayAvailability(), 'start_time': TunableTimeOfDay(default_hour=9), 'duration': Tunable(description='\n Duration of this work session in hours.\n ', tunable_type=float, default=1.0), 'random_start': Tunable(description='\n If checked, this schedule will have a random start time in the tuned\n window each time.\n ', tunable_type=bool, default=False), 'schedule_shift_type': TunableEnumEntry(description='\n Shift Type for the schedule, this will be used for validations.\n ', tunable_type=CareerShiftType, default=CareerShiftType.ALL_DAY)} @TunableFactory.factory_option def schedule_entry_data(tuning_name='schedule_entry_tuning', tuning_type=None, additional_tuning_name='additional_tuning', additional_tuning_type=None): value = {} if tuning_type is not None: value[tuning_name] = tuning_type if additional_tuning_type is not None: value[additional_tuning_name] = additional_tuning_type return value def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._start_and_end_times = set() for (day, day_enabled) in self.days_available.items(): if day_enabled: days_as_time_span = date_and_time.create_time_span(days=day) start_time = self.start_time + days_as_time_span end_time = start_time + date_and_time.create_time_span(hours=self.duration) self._start_and_end_times.add((start_time, end_time)) def get_start_and_end_times(self): return self._start_and_end_times
class HouseholdManager(objects.object_manager.DistributableObjectManager): __qualname__ = 'HouseholdManager' PHONE_CALL_INFO = TunableTuple(ask_to_come_over=AskToComeOverPhoneCall.TunableFactory(), chat=ChatPhoneCall.TunableFactory(), invite_over=InviteOverPhoneCall.TunableFactory(), minimum_time_between_calls=TunableSimMinute(description='\n The minimum time between calls. When scheduling the call alarm\n A number between minimum and maximum will be chosen.\n ', default=60, minimum=1), maximum_time_between_calls=TunableSimMinute(description='\n The maximum time between calls. When scheduling the call alarm\n A number between minimum and maximum will be chosen.\n ', default=120, minimum=1), availible_time_of_day=TunableTuple(start_time=TunableTimeOfDay(description='\n The start time that the player can receive phone calls\n '), end_time=TunableTimeOfDay(description='\n The end time that the player can receive phone calls.\n '), description='\n The start and end times that determine the time that the player can\n receive phone calls.\n '), description='\n Data related to sims calling up your sims.\n ') NPC_HOSTED_EVENT_SCHEDULER = situations.npc_hosted_situations.TunableNPCHostedSituationSchedule() def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._npc_hosted_situation_scheduler = None self._save_slot_data = None self._phone_call_alarm_handle = None self._phone_call_element = None self._pending_household_funds = collections.defaultdict(list) def create_household(self, account): new_household = sims.household.Household(account) self.add(new_household) return new_household def increment_household_object_count(self, household_id): if household_id is None: return house = self.get(household_id) if house is not None: build_buy.update_household_object_count(house.id, house.owned_object_count) def decrement_household_object_count(self, household_id): if household_id is None: return house = self.get(household_id) if house is not None: build_buy.update_household_object_count(house.id, house.owned_object_count) def load_households(self): for household_proto in services.get_persistence_service().all_household_proto_gen(): household_id = household_proto.household_id household = self.get(household_id) while household is None: self._load_household_from_household_proto(household_proto) for household_id in self._pending_household_funds.keys(): logger.error('Household {} has pending funds leftover from BB after all households were loaded.', household_id, owner='camilogarcia') self._pending_household_funds = None def load_household(self, household_id): return self._load_household(household_id) def _load_household(self, household_id): household = self.get(household_id) if household is not None: for sim_info in household.sim_info_gen(): while sim_info.zone_id != sims4.zone_utils.get_zone_id(False): householdProto = services.get_persistence_service().get_household_proto_buff(household_id) if householdProto is None: logger.error('unable to find household with household id {}'.household_id) return found_sim = False if householdProto.sims.ids: for sim_id in householdProto.sims.ids: while sim_id == sim_info.sim_id: found_sim = True break if found_sim: sim_proto = services.get_persistence_service().get_sim_proto_buff(sim_id) sim_info.load_sim_info(sim_proto) return household logger.info('Starting to load household id = {0}', household_id) household_proto = services.get_persistence_service().get_household_proto_buff(household_id) if household_proto is None: sims4.log.error('Persistence', 'Household proto could not be found id = {0}', household_id) return household = self._load_household_from_household_proto(household_proto) return household def _load_household_from_household_proto(self, household_proto): account = services.account_service().get_account_by_id(household_proto.account_id, try_load_account=True) if account is None: sims4.log.error('Persistence', "Household account doesn't exist in account ids. Creating temp account", owner='yshan') account = server.account.Account(household_proto.account_id, 'TempPersonaName') household = sims.household.Household(account) household.load_data(household_proto) logger.info('Household loaded. name:{:20} id:{:10} #sim_infos:{:2}', household.name, household.id, len(household)) self.add(household) household.initialize_sim_infos() if household is services.client_manager().get_first_client().household: for sim_info in household.sim_info_gen(): for other_info in household.sim_info_gen(): while sim_info is not other_info: family_relationship = sim_info.relationship_tracker._find_relationship(other_info.id, create=False) if family_relationship is not None and family_relationship.has_bit(global_relationship_tuning.RelationshipGlobalTuning.NEIGHBOR_RELATIONSHIP_BIT): family_relationship.remove_bit(global_relationship_tuning.RelationshipGlobalTuning.NEIGHBOR_RELATIONSHIP_BIT) pending_funds_reasons = self._pending_household_funds.get(household.id) if pending_funds_reasons is not None: del self._pending_household_funds[household.id] for (fund, reason) in pending_funds_reasons: household.funds.add(fund, reason, None) return household def is_household_stored_in_any_neighborhood_proto(self, household_id): for neighborhood_proto in services.get_persistence_service().get_neighborhoods_proto_buf_gen(): while any(household_id == household_account_proto.household_id for household_account_proto in neighborhood_proto.npc_households): return True return False def get_by_sim_id(self, sim_id): for house in self._objects.values(): while house.sim_in_household(sim_id): return house def get_household_autonomy_mode(self, sim): household = sim.household if household is not None: return household.get_autonomy_mode() def get_household_autonomy_state(self, sim): household = sim.household if household is not None: household_setting = household.autonomy_setting if household_setting: if household_setting.state is not autonomy.settings.AutonomyState.UNDEFINED: return household_setting.state def prune_household(self, household_id): household = self.get(household_id) if household is not None: if household.get_household_type() != sims.household.HouseholdType.GAME_CREATED: logger.warn('Trying to prune a non-game created household:{}', household.id, owner='msantander') return sim_info_manager = services.sim_info_manager() for sim_info in tuple(household): household.remove_sim_info(sim_info, destroy_if_empty_gameplay_household=True) sim_info_manager.remove_permanently(sim_info) services.get_persistence_service().del_sim_proto_buff(sim_info.id) def save(self, **kwargs): households = self.get_all() for household in households: household.save_data() def on_all_households_and_sim_infos_loaded(self, client): self._npc_hosted_situation_scheduler = HouseholdManager.NPC_HOSTED_EVENT_SCHEDULER() self._schedule_phone_call_alarm() for household in self.get_all(): household.on_all_households_and_sim_infos_loaded() def on_client_disconnect(self, client): for household in self.get_all(): household.on_client_disconnect() if self._phone_call_alarm_handle is not None: alarms.cancel_alarm(self._phone_call_alarm_handle) self._phone_call_alarm_handle = None if self._phone_call_element is not None: self._phone_call_element.trigger_hard_stop() def _schedule_phone_call_alarm(self): delay = random.randint(HouseholdManager.PHONE_CALL_INFO.minimum_time_between_calls, HouseholdManager.PHONE_CALL_INFO.maximum_time_between_calls) time_delay = interval_in_sim_minutes(delay) current_time = services.time_service().sim_now alarm_end_time = current_time + time_delay if alarm_end_time.time_between_day_times(HouseholdManager.PHONE_CALL_INFO.availible_time_of_day.start_time, HouseholdManager.PHONE_CALL_INFO.availible_time_of_day.end_time): time_till_alarm_end_time = alarm_end_time - current_time self._phone_call_alarm_handle = alarms.add_alarm(self, time_till_alarm_end_time, self._trigger_phone_call_callback) return if current_time.time_between_day_times(HouseholdManager.PHONE_CALL_INFO.availible_time_of_day.start_time, HouseholdManager.PHONE_CALL_INFO.availible_time_of_day.end_time): time_till_next_end_time = current_time.time_till_next_day_time(HouseholdManager.PHONE_CALL_INFO.availible_time_of_day.end_time) else: time_till_next_end_time = TimeSpan.ZERO time_delay -= time_till_next_end_time time_available_per_day = HouseholdManager.PHONE_CALL_INFO.availible_time_of_day.end_time - HouseholdManager.PHONE_CALL_INFO.availible_time_of_day.start_time cycles_left = time_delay.in_ticks()//time_available_per_day.in_ticks() time_till_next_start_time = current_time.time_till_next_day_time(HouseholdManager.PHONE_CALL_INFO.availible_time_of_day.start_time) time_till_alarm_end_time = time_till_next_start_time + interval_in_sim_days(cycles_left) time_delay -= time_available_per_day*cycles_left time_till_alarm_end_time += time_delay self._phone_call_alarm_handle = alarms.add_alarm(self, time_till_alarm_end_time, self._trigger_phone_call_callback) def _trigger_phone_call_callback(self, _): if self._phone_call_element is None: self._phone_call_element = elements.GeneratorElement(self._trigger_phone_call_gen) services.time_service().sim_timeline.schedule(self._phone_call_element) self._schedule_phone_call_alarm() def _trigger_phone_call_gen(self, timeline): client = services.client_manager().get_first_client() if client is None: return client_household = client.household if client_household is None: return sims_to_check = [sim for sim in client_household.instanced_sims_gen()] random.shuffle(sims_to_check) for sim in sims_to_check: call_types = [] ask_to_come_over_phone_call = HouseholdManager.PHONE_CALL_INFO.ask_to_come_over(sim) call_types.append((ask_to_come_over_phone_call.weight, ask_to_come_over_phone_call)) chat_phone_call = HouseholdManager.PHONE_CALL_INFO.chat(sim) call_types.append((chat_phone_call.weight, chat_phone_call)) invite_over_phone_call = HouseholdManager.PHONE_CALL_INFO.invite_over(sim) call_types.append((invite_over_phone_call.weight, invite_over_phone_call)) while call_types: call_type = pop_weighted(call_types) if call_type.try_and_setup(): call_type.execute() self._phone_call_element = None return yield element_utils.run_child(timeline, element_utils.sleep_until_next_tick_element()) self._phone_call_element = None def debug_trigger_ask_to_come_over_phone_call(self, sim): phone_call = HouseholdManager.PHONE_CALL_INFO.ask_to_come_over(sim) if not phone_call.try_and_setup(): return False phone_call.execute() return True def debug_trigger_chat_phone_call(self, sim): phone_call = HouseholdManager.PHONE_CALL_INFO.chat(sim) if not phone_call.try_and_setup(): return False phone_call.execute() return True def debug_trigger_invite_over_phone_call(self, sim): phone_call = HouseholdManager.PHONE_CALL_INFO.invite_over(sim) if not phone_call.try_and_setup(): return False phone_call.execute() return True @staticmethod def get_active_sim_home_zone_id(): client = services.client_manager().get_first_client() if client is not None: active_sim = client.active_sim if active_sim is not None: household = active_sim.household if household is not None: return household.home_zone_id def try_add_pending_household_funds(self, household_id, funds, reason): if self._pending_household_funds is None: return False self._pending_household_funds[household_id].append((funds, reason)) return True
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 Retirement(CareerKnowledgeMixin): CAREER_TRACK_RETIRED = TunableReference( description= '\n A carer track for retired Sims. This is used for "Ask about Career"\n notifications.\n ', manager=services.get_instance_manager( sims4.resources.Types.CAREER_TRACK)) DAILY_HOURS_WORKED_FALLBACK = TunableRange( description= '\n If a Sim retires from a career that has no fixed schedule, use this\n number to compute average hours worked per day.\n ', tunable_type=float, minimum=1, maximum=24, default=5) DAILY_PAY_TIME = TunableTimeOfDay( description= '\n The time of day the retirement payout will be given.\n ', default_hour=7) DAILY_PAY_MULTIPLIER = TunableMultiplier.TunableFactory( description= '\n Multiplier on the average daily pay of the retired career the Sim will\n get every day.\n ' ) DAILY_PAY_NOTIFICATION = _get_notification_tunable_factory( description= '\n Message when a Sim receives a retirement payout.\n ') RETIREMENT_NOTIFICATION = _get_notification_tunable_factory( description='\n Message when a Sim retires.\n ') __slots__ = ('_sim_info', '_career_uid', '_alarm_handle') def __init__(self, sim_info, retired_career_uid): self._sim_info = sim_info self._career_uid = retired_career_uid self._alarm_handle = None @property def current_track_tuning(self): return self.CAREER_TRACK_RETIRED @property def career_uid(self): return self._career_uid def start(self, send_retirement_notification=False): self._add_alarm() self._distribute() if send_retirement_notification: self.send_dialog(Retirement.RETIREMENT_NOTIFICATION) def stop(self): self._clear_alarm() def _add_alarm(self): now = services.time_service().sim_now time_span = now.time_till_next_day_time(Retirement.DAILY_PAY_TIME) if time_span == TimeSpan.ZERO: time_span = time_span + TimeSpan(date_and_time.sim_ticks_per_day()) self._alarm_handle = alarms.add_alarm(self._sim_info, time_span, self._alarm_callback, repeating=False, use_sleep_time=False) def _clear_alarm(self): if self._alarm_handle is not None: alarms.cancel_alarm(self._alarm_handle) self._alarm_handle = None def _alarm_callback(self, alarm_handle): self._add_alarm() self.pay_retirement() def pay_retirement(self): pay = self._get_daily_pay() self._sim_info.household.funds.add( pay, protocolbuffers.Consts_pb2.TELEMETRY_MONEY_CAREER, self._sim_info.get_sim_instance( allow_hidden_flags=ALL_HIDDEN_REASONS)) self.send_dialog(Retirement.DAILY_PAY_NOTIFICATION, pay) def get_career_text_tokens(self): career_level = self._get_career_level_tuning() career_track = self._get_career_track_tuning() return (career_level.get_title(self._sim_info), career_track.get_career_name(self._sim_info), None) def _get_career_history(self): return self._sim_info.career_tracker.career_history[self._career_uid] def _get_career_track_tuning(self): history = self._get_career_history() return history.career_track def _get_career_level_tuning(self): history = self._get_career_history() track = self._get_career_track_tuning() return track.career_levels[history.level] def _get_daily_pay(self): career_history = self._get_career_history() resolver = SingleSimResolver(self._sim_info) multiplier = Retirement.DAILY_PAY_MULTIPLIER.get_multiplier(resolver) adjusted_pay = int(career_history.daily_pay * multiplier) return adjusted_pay def send_dialog(self, notification, *additional_tokens, icon_override=None, on_response=None): if self._sim_info.is_npc: return resolver = SingleSimResolver(self._sim_info) dialog = notification(self._sim_info, resolver=resolver) if dialog is not None: track = self._get_career_track_tuning() level = self._get_career_level_tuning() job = level.get_title(self._sim_info) career = track.get_career_name(self._sim_info) tokens = (job, career) + additional_tokens icon_override = IconInfoData( icon_resource=track.icon ) if icon_override is None else icon_override dialog.show_dialog(additional_tokens=tokens, icon_override=icon_override, secondary_icon_override=IconInfoData( obj_instance=self._sim_info), on_response=on_response) def _distribute(self): op = protocolbuffers.DistributorOps_pb2.SetCareers() with ProtocolBufferRollback(op.careers) as career_op: career_history = self._get_career_history() career_op.career_uid = self._career_uid career_op.career_level = career_history.level career_op.career_track = career_history.career_track.guid64 career_op.user_career_level = career_history.user_level career_op.is_retired = True distributor = Distributor.instance() if distributor is not None: distributor.add_op( self._sim_info, GenericProtocolBufferOp(Operation.SET_CAREER, career_op))
class StoryProgressionActionMaxPopulation(_StoryProgressionAction): FACTORY_TUNABLES = {'sim_info_cap_per_lod': TunableMapping(description="\n The mapping of SimInfoLODLevel value to an interval of sim info cap\n integer values.\n \n NOTE: The ACTIVE lod can't be tuned here because it's being tracked\n via the Maximum Size tuning in Household module tuning.\n ", key_type=TunableEnumEntry(description='\n The SimInfoLODLevel value.\n ', tunable_type=SimInfoLODLevel, default=SimInfoLODLevel.FULL, invalid_enums=(SimInfoLODLevel.ACTIVE,)), value_type=TunableRange(description='\n The number of sim infos allowed to be present before culling\n is triggered for this SimInfoLODLevel.\n ', tunable_type=int, default=210, minimum=0)), 'time_of_day': TunableTuple(description='\n Only run this action when it is between a certain time of day.\n ', start_time=TunableTimeOfDay(default_hour=2), end_time=TunableTimeOfDay(default_hour=6)), 'culling_buffer_percentage': TunablePercent(description='\n When sim infos are culled due to the number of sim infos exceeding\n the cap, this is how much below the cap the number of sim infos\n will be (as a percentage of the cap) after the culling, roughly.\n The margin of error is due to the fact that we cull at the household\n level, so the number of sims culled can be a bit more than this value\n if the last household culled contains more sims than needed to reach\n the goal. (i.e. we never cull partial households)\n ', default=20), 'homeless_played_demotion_time': OptionalTunable(description='\n If enabled, played Sims that have been homeless for at least this\n many days will be drops from FULL to BASE_SIMULATABLE lod.\n ', tunable=TunableRange(tunable_type=int, default=10, minimum=0))} def __init__(self, **kwargs): super().__init__(**kwargs) self._played_family_tree_distances = {} self._precull_telemetry_data = Counter() self._precull_telemetry_lod_counts_str = '' self._telemetry_id = 0 self._total_sim_cap = Household.MAXIMUM_SIZE self._total_sim_cap += sum(self.sim_info_cap_per_lod.values()) import sims.sim_info_manager sims.sim_info_manager.SimInfoManager.SIM_INFO_CAP = self._total_sim_cap sims.sim_info_manager.SIM_INFO_CAP_PER_LOD = self.sim_info_cap_per_lod def should_process(self, options): current_time = services.time_service().sim_now if not current_time.time_between_day_times(self.time_of_day.start_time, self.time_of_day.end_time): return False return True def process_action(self, story_progression_flags): try: self._pre_cull() self._process_full() self._process_interacted() self._process_base() self._process_background() self._process_minimum() self._post_cull(story_progression_flags) finally: self._cleanup() def _get_cap_level(self, sim_info_lod): cap_override = services.sim_info_manager().get_sim_info_cap_override_per_lod(sim_info_lod) if cap_override is not None: return cap_override elif sim_info_lod in self.sim_info_cap_per_lod: return self.sim_info_cap_per_lod[sim_info_lod] return 0 def _pre_cull(self): self._played_family_tree_distances = self._get_played_family_tree_distances() self._telemetry_id = telemetry_id_generator() self._precull_telemetry_data['scap'] = self._total_sim_cap (player_households, player_sims, households, sims, lod_counts) = self._get_census() self._precull_telemetry_data['thob'] = households self._precull_telemetry_data['tsib'] = sims self._precull_telemetry_data['phob'] = player_households self._precull_telemetry_data['psib'] = player_sims self._precull_telemetry_lod_counts_str = self._get_lod_counts_str_for_telemetry(lod_counts) for sim_info in services.sim_info_manager().get_all(): sim_info.report_telemetry('pre-culling') self._trigger_creation_source_telemetry() def _trigger_creation_source_telemetry(self): payload = '' counter = 0 def dump_hook(): hook_name = 'CS{:0>2}'.format(counter) with telemetry_helper.begin_hook(writer, hook_name) as hook: hook.write_int('clid', self._telemetry_id) hook.write_string('crsr', payload) sources = get_sim_info_creation_sources() for (source, count) in sources.most_common(): if counter >= TELEMETRY_CREATION_SOURCE_HOOK_COUNT: break delta = '{}${}'.format(source, count) if len(payload) + len(delta) <= TELEMETRY_CREATION_SOURCE_BUFFER_LENGTH: payload = '{}+{}'.format(payload, delta) else: dump_hook() payload = delta counter += 1 dump_hook() def _process_full(self): if gsi_handlers.sim_info_culling_handler.is_archive_enabled(): gsi_archive = CullingArchive('Full Pass') gsi_archive.census_before = self._get_gsi_culling_census() else: gsi_archive = None cap = self._get_cap_level(SimInfoLODLevel.FULL) sim_infos = services.sim_info_manager().get_sim_infos_with_lod(SimInfoLODLevel.FULL) now = services.time_service().sim_now mandatory_drops = set() scores = {} for sim_info in sim_infos: if sim_info.is_instanced(allow_hidden_flags=ALL_HIDDEN_REASONS): if gsi_archive is not None: gsi_archive.add_sim_info_cullability(sim_info, info='immune -- instanced') if not sim_info.is_player_sim: if gsi_archive is not None: gsi_archive.add_sim_info_cullability(sim_info, score=0, info='mandatory drop -- non-player') mandatory_drops.add(sim_info) elif sim_info.household.home_zone_id != 0: if gsi_archive is not None: gsi_archive.add_sim_info_cullability(sim_info, info='immune -- player and not homeless') if self.homeless_played_demotion_time is not None: days_homeless = (now - sim_info.household.home_zone_move_in_time).in_days() if days_homeless < self.homeless_played_demotion_time: if gsi_archive is not None: gsi_archive.add_sim_info_cullability(sim_info, info='immune -- not homeless long enough') if gsi_archive is not None: gsi_archive.add_sim_info_cullability(sim_info, score=days_homeless, info='homeless for too long') scores[sim_info] = days_homeless else: if gsi_archive is not None: gsi_archive.add_sim_info_cullability(sim_info, score=days_homeless, info='homeless for too long') scores[sim_info] = days_homeless if gsi_archive is not None: gsi_archive.add_sim_info_cullability(sim_info, info='immune -- no pressure to drop') elif gsi_archive is not None: gsi_archive.add_sim_info_cullability(sim_info, info='immune -- no pressure to drop') elif self.homeless_played_demotion_time is not None: days_homeless = (now - sim_info.household.home_zone_move_in_time).in_days() if days_homeless < self.homeless_played_demotion_time: if gsi_archive is not None: gsi_archive.add_sim_info_cullability(sim_info, info='immune -- not homeless long enough') if gsi_archive is not None: gsi_archive.add_sim_info_cullability(sim_info, score=days_homeless, info='homeless for too long') scores[sim_info] = days_homeless else: if gsi_archive is not None: gsi_archive.add_sim_info_cullability(sim_info, score=days_homeless, info='homeless for too long') scores[sim_info] = days_homeless if gsi_archive is not None: gsi_archive.add_sim_info_cullability(sim_info, info='immune -- no pressure to drop') elif gsi_archive is not None: gsi_archive.add_sim_info_cullability(sim_info, info='immune -- no pressure to drop') elif not sim_info.is_player_sim: if gsi_archive is not None: gsi_archive.add_sim_info_cullability(sim_info, score=0, info='mandatory drop -- non-player') mandatory_drops.add(sim_info) elif sim_info.household.home_zone_id != 0: if gsi_archive is not None: gsi_archive.add_sim_info_cullability(sim_info, info='immune -- player and not homeless') if self.homeless_played_demotion_time is not None: days_homeless = (now - sim_info.household.home_zone_move_in_time).in_days() if days_homeless < self.homeless_played_demotion_time: if gsi_archive is not None: gsi_archive.add_sim_info_cullability(sim_info, info='immune -- not homeless long enough') if gsi_archive is not None: gsi_archive.add_sim_info_cullability(sim_info, score=days_homeless, info='homeless for too long') scores[sim_info] = days_homeless else: if gsi_archive is not None: gsi_archive.add_sim_info_cullability(sim_info, score=days_homeless, info='homeless for too long') scores[sim_info] = days_homeless if gsi_archive is not None: gsi_archive.add_sim_info_cullability(sim_info, info='immune -- no pressure to drop') elif gsi_archive is not None: gsi_archive.add_sim_info_cullability(sim_info, info='immune -- no pressure to drop') elif self.homeless_played_demotion_time is not None: days_homeless = (now - sim_info.household.home_zone_move_in_time).in_days() if days_homeless < self.homeless_played_demotion_time: if gsi_archive is not None: gsi_archive.add_sim_info_cullability(sim_info, info='immune -- not homeless long enough') if gsi_archive is not None: gsi_archive.add_sim_info_cullability(sim_info, score=days_homeless, info='homeless for too long') scores[sim_info] = days_homeless else: if gsi_archive is not None: gsi_archive.add_sim_info_cullability(sim_info, score=days_homeless, info='homeless for too long') scores[sim_info] = days_homeless if gsi_archive is not None: gsi_archive.add_sim_info_cullability(sim_info, info='immune -- no pressure to drop') elif gsi_archive is not None: gsi_archive.add_sim_info_cullability(sim_info, info='immune -- no pressure to drop') num_to_cull = self._get_num_to_cull(len(sim_infos) - len(mandatory_drops), cap) sorted_sims = sorted(scores.keys(), key=lambda x: scores[x], reverse=True) culling_service = services.get_culling_service() for sim_info in itertools.chain(mandatory_drops, sorted_sims[:num_to_cull]): if culling_service.has_sim_interacted_with_active_sim(sim_info.sim_id): new_lod = SimInfoLODLevel.INTERACTED else: new_lod = SimInfoLODLevel.BASE if sim_info.request_lod(new_lod): if gsi_archive is not None: gsi_archive.add_sim_info_action(sim_info, action='drop to {}'.format(new_lod)) elif gsi_archive is not None: gsi_archive.add_sim_info_action(sim_info, action='failed to drop to {}'.format(new_lod)) if gsi_archive is not None: gsi_archive.census_after = self._get_gsi_culling_census() gsi_archive.apply() def _process_interacted(self): if gsi_handlers.sim_info_culling_handler.is_archive_enabled(): gsi_archive = CullingArchive('Interacted Pass') gsi_archive.census_before = self._get_gsi_culling_census() else: gsi_archive = None culling_service = services.get_culling_service() cap = self._get_cap_level(SimInfoLODLevel.INTERACTED) sim_info_manager = services.sim_info_manager() interacted_sim_ids_in_priority_order = culling_service.get_interacted_sim_ids_in_priority_order() interacted_count = 0 for sim_id in interacted_sim_ids_in_priority_order: sim_info = sim_info_manager.get(sim_id) if sim_info is None or sim_info.lod != SimInfoLODLevel.INTERACTED: culling_service.remove_sim_from_interacted_sims(sim_id) else: interacted_count += 1 if cap < interacted_count: if gsi_archive is not None: gsi_archive.add_sim_info_cullability(sim_info, score=interacted_sim_ids_in_priority_order.index(sim_id), info='last interaction too old') if sim_info.request_lod(SimInfoLODLevel.BASE): culling_service.remove_sim_from_interacted_sims(sim_id) interacted_count -= 1 if gsi_archive is not None: gsi_archive.add_sim_info_action(sim_info, action='drop to BASE') elif gsi_archive is not None: gsi_archive.add_sim_info_action(sim_info, action='failed to drop to INTERACTED') elif gsi_archive is not None: gsi_archive.add_sim_info_cullability(sim_info, score=interacted_sim_ids_in_priority_order.index(sim_id), info='no pressure to drop') if gsi_archive is not None: gsi_archive.census_after = self._get_gsi_culling_census() gsi_archive.apply() def _process_base(self): def demote_from_base(sim_info, gsi_archive): if sim_info.request_lod(SimInfoLODLevel.BACKGROUND): if gsi_archive is not None: gsi_archive.add_sim_info_action(sim_info, action='drop to BACKGROUND') elif gsi_archive is not None: gsi_archive.add_sim_info_action(sim_info, action='failed to drop to BACKGROUND') self._process_low(SimInfoLODLevel.BASE, 'Base Pass', demote_from_base) def _process_background(self): culling_service = services.get_culling_service() if gsi_handlers.sim_info_culling_handler.is_archive_enabled(): gsi_archive = CullingArchive('Background Pass') gsi_archive.census_before = self._get_gsi_culling_census() else: gsi_archive = None background_cap = self._get_cap_level(SimInfoLODLevel.BACKGROUND) sim_info_manager = services.sim_info_manager() sim_infos = sim_info_manager.get_sim_infos_with_lod(SimInfoLODLevel.BACKGROUND) households = frozenset(sim_info.household for sim_info in sim_infos) num_infos_above_background_lod = sim_info_manager.get_num_sim_infos_with_criteria(lambda sim_info: sim_info.lod > SimInfoLODLevel.BACKGROUND) full_and_active_cap = self._get_cap_level(SimInfoLODLevel.FULL) + Household.MAXIMUM_SIZE cap_overage = num_infos_above_background_lod - full_and_active_cap cap = max(background_cap - cap_overage, 0) if cap_overage > 0 else background_cap sim_info_immunity_reasons = {} sim_info_scores = {} for sim_info in sim_infos: immunity_reasons = sim_info.get_culling_immunity_reasons() if immunity_reasons: sim_info_immunity_reasons[sim_info] = immunity_reasons else: sim_info_scores[sim_info] = culling_service.get_culling_score_for_sim_info(sim_info) household_scores = {} immune_households = set() for household in households: if any(sim_info.lod != SimInfoLODLevel.BACKGROUND or sim_info in sim_info_immunity_reasons for sim_info in household): immune_households.add(household) else: score = max(sim_info_scores[sim_info].score for sim_info in household) household_scores[household] = score if gsi_archive is not None: for (sim_info, immunity_reasons) in sim_info_immunity_reasons.items(): gsi_archive.add_sim_info_cullability(sim_info, info='immune: {}'.format(', '.join(reason.gsi_reason for reason in immunity_reasons))) for (sim_info, score) in sim_info_scores.items(): gsi_archive.add_sim_info_cullability(sim_info, score=score.score, rel_score=score.rel_score, inst_score=score.inst_score, importance_score=score.importance_score) def get_sim_cullability(sim_info): if sim_info.lod > SimInfoLODLevel.BACKGROUND: return 'LOD is not BACKGROUND' if sim_info in sim_info_immunity_reasons: return ', '.join(reason.gsi_reason for reason in sim_info_immunity_reasons[sim_info]) elif sim_info in sim_info_scores: return str(sim_info_scores[sim_info].score) return '' for household in immune_households: member_cullabilities = ', '.join('{} ({})'.format(sim_info.full_name, get_sim_cullability(sim_info)) for sim_info in household) gsi_archive.add_household_cullability(household, info='immune: {}'.format(member_cullabilities)) for (household, score) in household_scores.items(): member_cullabilities = ', '.join('{} ({})'.format(sim_info.full_name, get_sim_cullability(sim_info)) for sim_info in household) gsi_archive.add_household_cullability(household, score=score, info=member_cullabilities) self._precull_telemetry_data['imho'] = len(immune_households) self._precull_telemetry_data['imsi'] = len(sim_info_immunity_reasons) self._precull_telemetry_data['imsc'] = sum(len(h) for h in immune_households) self._precull_telemetry_data.update(reason.telemetry_hook for reason in itertools.chain.from_iterable(sim_info_immunity_reasons.values())) for reason in CullingReasons.ALL_CULLING_REASONS: if reason not in self._precull_telemetry_data: self._precull_telemetry_data[reason.telemetry_hook] = 0 culling_service = services.get_culling_service() sorted_households = sorted(household_scores, key=household_scores.get) num_to_cull = self._get_num_to_cull(len(sim_infos), cap) while sorted_households: while num_to_cull > 0: household = sorted_households.pop(0) num_to_cull -= len(household) culling_service.cull_household(household, is_important_fn=self._has_player_sim_in_family_tree, gsi_archive=gsi_archive) for sim_info in sim_info_manager.get_all(): if sim_info.household is None: logger.error('Found sim info {} without household during sim culling.', sim_info) if gsi_archive is not None: gsi_archive.census_after = self._get_gsi_culling_census() gsi_archive.apply() def _process_minimum(self): def demote_from_minimum(sim_info, gsi_archive): if gsi_archive is not None: gsi_archive.add_sim_info_action(sim_info, action='cull') sim_info.remove_permanently() self._process_low(SimInfoLODLevel.MINIMUM, 'Minimum Pass', demote_from_minimum) def _process_low(self, current_lod, debug_pass_name, demote_fn): if gsi_handlers.sim_info_culling_handler.is_archive_enabled(): gsi_archive = CullingArchive(debug_pass_name) gsi_archive.census_before = self._get_gsi_culling_census() else: gsi_archive = None cap = self._get_cap_level(current_lod) sim_info_manager = services.sim_info_manager() min_lod_sim_infos = sim_info_manager.get_sim_infos_with_lod(current_lod) num_min_lod_sim_infos = len(min_lod_sim_infos) sorted_sim_infos = sorted(min_lod_sim_infos, key=lambda x: self._played_family_tree_distances[x.id], reverse=True) if gsi_archive is not None: for sim_info in min_lod_sim_infos: gsi_archive.add_sim_info_cullability(sim_info, score=self._played_family_tree_distances[sim_info.id]) num_to_cull = self._get_num_to_cull(num_min_lod_sim_infos, cap) for sim_info in sorted_sim_infos[:num_to_cull]: demote_fn(sim_info, gsi_archive) if gsi_archive is not None: gsi_archive.census_after = self._get_gsi_culling_census() gsi_archive.apply() def _post_cull(self, story_progression_flags): with telemetry_helper.begin_hook(writer, TELEMETRY_HOOK_CULL_SIMINFO_BEFORE) as hook: hook.write_int('clid', self._telemetry_id) hook.write_string('rson', self._get_trigger_reason(story_progression_flags)) for (key, value) in self._precull_telemetry_data.items(): hook.write_int(key, value) with telemetry_helper.begin_hook(writer, TELEMETRY_HOOK_CULL_SIMINFO_BEFORE2) as hook: hook.write_int('clid', self._telemetry_id) hook.write_string('lodb', self._precull_telemetry_lod_counts_str) (player_households, player_sims, households, sims, lod_counts) = self._get_census() with telemetry_helper.begin_hook(writer, TELEMETRY_HOOK_CULL_SIMINFO_AFTER) as hook: hook.write_int('clid', self._telemetry_id) hook.write_string('rson', self._get_trigger_reason(story_progression_flags)) hook.write_int('scap', self._total_sim_cap) hook.write_int('thoa', households) hook.write_int('tsia', sims) hook.write_string('loda', self._get_lod_counts_str_for_telemetry(lod_counts)) hook.write_int('phoa', player_households) hook.write_int('psia', player_sims) def _cleanup(self): self._played_family_tree_distances.clear() self._precull_telemetry_data.clear() def _get_played_family_tree_distances(self): with genealogy_caching(): sim_info_manager = services.sim_info_manager() played_sim_infos = frozenset(sim_info for sim_info in sim_info_manager.get_all() if sim_info.is_player_sim) def get_sim_ids_with_played_spouses(): return set(sim_info.spouse_sim_id for sim_info in played_sim_infos if sim_info.spouse_sim_id is not None if sim_info.spouse_sim_id in sim_info_manager) def get_sim_ids_with_played_siblings(): sim_ids_with_played_siblings = set() visited_ids = set() for sim_info in played_sim_infos: if sim_info.id in visited_ids: continue siblings = set(sim_info.genealogy.get_siblings_sim_infos_gen()) siblings.add(sim_info) visited_ids.update(sibling.id for sibling in siblings) played_siblings = set(sibling for sibling in siblings if sibling.is_player_sim) if len(played_siblings) == 1: sim_ids_with_played_siblings.update(sibling.id for sibling in siblings if sibling not in played_siblings) elif len(played_siblings) > 1: sim_ids_with_played_siblings.update(sibling.id for sibling in siblings) return sim_ids_with_played_siblings def get_played_relative_distances(up=False): distances = {} step = 0 next_crawl_set = set(played_sim_infos) while next_crawl_set: step += 1 crawl_set = next_crawl_set next_crawl_set = set() def relatives_gen(sim_info): if up: yield from sim_info.genealogy.get_child_sim_infos_gen() else: yield from sim_info.genealogy.get_parent_sim_infos_gen() for relative in itertools.chain.from_iterable(relatives_gen(sim_info) for sim_info in crawl_set): if relative.id in distances: continue distances[relative.id] = step if relative not in played_sim_infos: next_crawl_set.add(relative) return distances zero_distance_sim_ids = get_sim_ids_with_played_spouses() | get_sim_ids_with_played_siblings() ancestor_map = get_played_relative_distances(up=True) descendant_map = get_played_relative_distances(up=False) def get_score(sim_info): sim_id = sim_info.id if sim_id in zero_distance_sim_ids: return 0 return min(ancestor_map.get(sim_id, MAX_INT32), descendant_map.get(sim_id, MAX_INT32)) distances = {sim_info.id: get_score(sim_info) for sim_info in sim_info_manager.get_all()} return distances def _has_player_sim_in_family_tree(self, sim_info): if sim_info.id not in self._played_family_tree_distances: logger.error('Getting played family tree distance for an unknown Sim Info {}', sim_info) return False return self._played_family_tree_distances[sim_info.id] < MAX_INT32 def _get_distance_to_nearest_player_sim_in_family_tree(self, sim_info): if sim_info.id not in self._played_family_tree_distances: logger.error('Getting played family tree distance for an unknown Sim Info {}', sim_info) return MAX_INT32 return self._played_family_tree_distances[sim_info.id] def _get_num_to_cull(self, pop_count, pop_cap): if pop_cap < 0: logger.error('Invalid pop_cap provided to _get_num_to_cull: {}', pop_cap) if pop_count > pop_cap: target_pop = pop_cap*(1 - self.culling_buffer_percentage) return int(pop_count - target_pop) return 0 def _get_census(self): player_households = sum(1 for household in services.household_manager().get_all() if household.is_player_household) player_sims = sum(1 for sim_info in services.sim_info_manager().get_all() if sim_info.is_player_sim) households = len(services.household_manager()) sims = len(services.sim_info_manager()) lod_counts = {lod: services.sim_info_manager().get_num_sim_infos_with_lod(lod) for lod in SimInfoLODLevel} return (player_households, player_sims, households, sims, lod_counts) def _get_lod_counts_str_for_telemetry(self, lod_counts): return '+'.join('{}~{}'.format(lod.name, num) for (lod, num) in lod_counts.items()) def _get_gsi_culling_census(self): (player_households, player_sims, households, sims, lod_counts) = self._get_census() return CullingCensus(player_households, player_sims, households, sims, lod_counts) @classmethod def _get_trigger_reason(cls, flags): reason = 'REGULAR_PROGRESSION' if flags & StoryProgressionFlags.SIM_INFO_FIREMETER != 0: reason = 'FIREMETER' return reason
class TutorialTip( metaclass=sims4.tuning.instances.HashedTunedInstanceMetaclass, manager=services.get_instance_manager( sims4.resources.Types.TUTORIAL_TIP)): INSTANCE_TUNABLES = { 'required_tip_groups': TunableList( description= '\n The Tip Groups that must be complete for this tip to be valid.\n ', tunable=TunableReference(manager=services.get_instance_manager( sims4.resources.Types.TUTORIAL_TIP), class_restrictions='TutorialTipGroup'), tuning_group=GROUP_NAME_DISPLAY_CRITERIA, export_modes=ExportModes.ClientBinary), 'required_ui_list': TunableList( description= '\n The UI elements that are required to be present in order for this\n tutorial tip to be valid.\n ', tunable=TunableEnumEntry(tunable_type=TutorialTipUiElement, default=TutorialTipUiElement.UI_INVALID), tuning_group=GROUP_NAME_DISPLAY_CRITERIA, export_modes=ExportModes.ClientBinary), 'required_ui_hidden_list': TunableList( description= '\n The UI elements that are required to NOT be present in order for this\n tutorial tip to be valid.\n ', tunable=TunableEnumEntry(tunable_type=TutorialTipUiElement, default=TutorialTipUiElement.UI_INVALID), tuning_group=GROUP_NAME_DISPLAY_CRITERIA, export_modes=ExportModes.ClientBinary), 'required_game_state': TunableEnumEntry( description= '\n The state the game must be in for this tutorial tip to be valid.\n ', tunable_type=TutorialTipGameState, default=TutorialTipGameState.GAMESTATE_NONE, tuning_group=GROUP_NAME_DISPLAY_CRITERIA, export_modes=ExportModes.ClientBinary), 'required_tips_not_satisfied': TunableList( description= '\n This is a list of tips that must be un-satisfied in order for this\n tip to activate. If any tip in this list is satisfied, this tip will\n not activate.\n ', tunable=TunableReference(manager=services.get_instance_manager( sims4.resources.Types.TUTORIAL_TIP), class_restrictions='TutorialTip'), tuning_group=GROUP_NAME_DISPLAY_CRITERIA, export_modes=ExportModes.ClientBinary), 'platform_filter': TunableEnumEntry( description= '\n The platforms on which this tutorial tip is shown.\n ', tunable_type=tutorials.tutorial.TutorialPlatformFilter, default=tutorials.tutorial.TutorialPlatformFilter.ALL_PLATFORMS, tuning_group=GROUP_NAME_DISPLAY_CRITERIA, export_modes=ExportModes.ClientBinary), 'required_tutorial_mode': TunableEnumEntry( description= '\n What mode this tutorial tip should be restricted to.\n STANDARD allows this tip to be in the original / standard tutorial mode.\n FTUE allows this tip to be in the FTUE tutorial mode.\n DISABLED means this tip is valid in any mode.\n ', tunable_type=TutorialMode, default=TutorialMode.STANDARD, tuning_group=GROUP_NAME_DISPLAY_CRITERIA, export_modes=ExportModes.ClientBinary), 'display': TunableTutorialTipDisplay( description= '\n This display information for this tutorial tip.\n ', tuning_group=GROUP_NAME_ACTIONS, export_modes=ExportModes.ClientBinary), 'display_narration': OptionalTunable( description= '\n Optionally play narration voice-over and display subtitles.\n ', tunable=TunableTuple( voiceover_audio=TunableResourceKey( description= '\n Narration audio to play.\n ', default=None, allow_none=True, resource_types=(sims4.resources.Types.PROPX, )), voiceover_audio_ps4=TunableResourceKey( description= '\n Narration audio to play specific to PS4.\n ', default=None, allow_none=True, resource_types=(sims4.resources.Types.PROPX, )), voiceover_audio_xb1=TunableResourceKey( description= '\n Narration audio to play specific to XB1.\n ', default=None, allow_none=True, resource_types=(sims4.resources.Types.PROPX, )), subtitle_text=TunableLocalizedString( description= '\n Subtitles to display while audio narration is playing.\n ' ), subtitle_display_location=TunableVariant( description= '\n What area on the screen the subtitles should appear.\n Top - Use the generic top-of-screen position.\n Bottom - Use the generic bottom-of-screen position.\n Custom - Specify a custom position in terms of % vertically.\n ', location=TunableEnumEntry( description= '\n Semantic location (UX-defined) for where the subtitles should appear.\n ', tunable_type=TutorialTipSubtitleDisplayLocation, default=TutorialTipSubtitleDisplayLocation.BOTTOM), custom=TunablePercent( description= '\n Vertical position for the subtitles, expressed as a\n percentage of the height of the screen.\n ', default=90), default='location'), satisfy_when_voiceover_finished=Tunable( description= '\n If set, the tutorial tip will be marked as satisfied when the\n voiceover completes or is interrupted.\n ', tunable_type=bool, default=False), delay_satisfaction_until_voiceover_finished=Tunable( description= '\n If set, the tutorial tip will not be marked satisfied until after\n the voiceover completes, preventing the voiceover from being\n interrupted by external satisfaction.\n ', tunable_type=bool, default=False), keep_subtitle_visible_until_satisfaction=Tunable( description= '\n If set, the subtitle will remain visible until the tutorial tip is\n marked as satisfied, even though the voiceover may have finished.\n ', tunable_type=bool, default=False), export_class_name='TutorialTipNarrationDisplay'), tuning_group=GROUP_NAME_ACTIONS, export_modes=ExportModes.ClientBinary), 'activation_ui_message': TunableTutorialTipUiMessage( description= '\n Sends a message to the UI when this tip is activated.\n ', tuning_group=GROUP_NAME_ACTIONS, export_modes=ExportModes.ClientBinary), 'deactivation_ui_message': TunableTutorialTipUiMessage( description= '\n Sends a message to the UI when this tip is deactivated.\n ', tuning_group=GROUP_NAME_ACTIONS, export_modes=ExportModes.ClientBinary), 'buffs': TunableList( description= '\n Buffs that will be applied at the start of this tutorial tip.\n ', tunable=TunableBuffReference(), tuning_group=GROUP_NAME_ACTIONS), 'buffs_removed_on_deactivate': Tunable( description= '\n If enabled, this tip will remove those buffs on deactivate.\n ', tunable_type=bool, default=False, tuning_group=GROUP_NAME_ACTIONS), 'commodities_to_solve': TunableSet( description= "\n A set of commodities we will attempt to solve. This will result in\n the Sim's interaction queue being filled with various interactions.\n ", tunable=TunableReference(services.statistic_manager()), tuning_group=GROUP_NAME_ACTIONS), 'gameplay_loots': OptionalTunable( description= '\n Loots that will be given at the start of this tip.\n Actor is is the sim specified by Sim Actor.\n Target is the sim specified by Sim Target.\n ', tunable=TunableList( tunable=TunableReference(manager=services.get_instance_manager( sims4.resources.Types.ACTION), class_restrictions=('LootActions', ), pack_safe=True)), tuning_group=GROUP_NAME_ACTIONS), 'restricted_affordances': OptionalTunable( description= '\n If enabled, use the filter to determine which affordances are allowed.\n ', tunable=TunableTuple( visible_affordances=TunableAffordanceFilterSnippet( description= '\n The filter of affordances that are visible.\n ' ), tooltip=OptionalTunable( description= '\n Tooltip when interaction is disabled by tutorial restrictions\n If not specified, will use the default in the tutorial service\n tuning.\n ', tunable=sims4.localization.TunableLocalizedStringFactory( )), enabled_affordances=TunableAffordanceFilterSnippet( description= '\n The filter of visible affordances that are enabled.\n ' )), tuning_group=GROUP_NAME_ACTIONS), 'call_to_actions': OptionalTunable( description= '\n Call to actions that should persist for the duration of this tip.\n ', tunable=TunableList( tunable=TunableReference(manager=services.get_instance_manager( sims4.resources.Types.CALL_TO_ACTION), pack_safe=True)), tuning_group=GROUP_NAME_ACTIONS), 'end_drama_node': Tunable( description= '\n If enabled, this tip will end the tutorial drama node.\n ', tunable_type=bool, default=False, tuning_group=GROUP_NAME_ACTIONS), 'sim_actor': TunableEnumEntry( description= "\n The entity who will be the actor sim for loot, and will\n receive the items that aren't specified via loots.\n \n If there is no Tutorial Drama Node active, actor will be active\n sim\n ", tunable_type=TutorialTipActorOption, default=TutorialTipActorOption.ACTIVE_SIM, tuning_group=GROUP_NAME_ACTIONS), 'sim_target': TunableEnumEntry( description= '\n The entity who will be the target sim for loot\n \n If there is no Tutorial Drama Node active, target sim will be active\n sim.\n ', tunable_type=TutorialTipActorOption, default=TutorialTipActorOption.ACTIVE_SIM, tuning_group=GROUP_NAME_ACTIONS), 'add_target_to_actor_household': Tunable( description= '\n If enabled, target sim will be added to active sim household.\n ', tunable_type=bool, default=False, tuning_group=GROUP_NAME_ACTIONS), 'make_housemate_unselectable': Tunable( description= '\n If enabled, housemate will be unselectable for the duration of the\n tooltip.\n ', tunable_type=bool, default=False, tuning_group=GROUP_NAME_ACTIONS), 'timeout_satisfies': Tunable( description= '\n If enabled, this tip is satisfied when the timeout is reached.\n If disabled, this tip will not satisfy when the timeout is reached.\n ', tunable_type=bool, default=False, tuning_group=GROUP_NAME_SATISFY, export_modes=ExportModes.ClientBinary), 'gameplay_test': OptionalTunable( description= '\n Tests that, if passed, will satisfy this tutorial tip.\n Only one test needs to pass to satisfy. These are intended for tips\n where the satisfy message should be tested and sent at a later time.\n ', tunable=tutorials.tutorial.TunableTutorialTestVariant(), tuning_group=GROUP_NAME_SATISFY, export_modes=ExportModes.All), 'sim_tested': TunableEnumEntry( description= '\n The entity who must fulfill the test events.\n \n If there is no Tutorial Drama Node, player sim and housemate sim will be active\n sim.\n ', tunable_type=TutorialTipTestSpecificityOption, default=TutorialTipTestSpecificityOption.UNSPECIFIED, tuning_group=GROUP_NAME_SATISFY), 'time_of_day': OptionalTunable( description= '\n If specified, tutorialtip will be satisfied once the time passes \n the specified time.\n ', tunable=TunableTimeOfDay(), tuning_group=GROUP_NAME_SATISFY), 'gameplay_immediate_test': OptionalTunable( description= '\n Tests that, if passed, will satisfy this tutorial tip.\n Only one test needs to pass to satisfy. These are intended for tips\n where the satisfy message should be tested and sent back immediately.\n ', tunable=tutorials.tutorial.TunableTutorialTestVariant(), tuning_group=GROUP_NAME_SATISFY, export_modes=ExportModes.All), 'satisfy_on_active_sim_change': Tunable( description= '\n If enabled, this tip is satisfied when the active sim changes\n ', tunable_type=bool, default=False, tuning_group=GROUP_NAME_SATISFY, export_modes=ExportModes.All), 'satisfy_on_activate': Tunable( description= "\n If enabled, this tip is satisfied immediately when all of it's\n preconditions have been met.\n ", tunable_type=bool, default=False, tuning_group=GROUP_NAME_SATISFY, export_modes=ExportModes.ClientBinary), 'tutorial_group_to_complete_on_skip': TunableReference( description= '\n The tutorial group who will have all tutorial tips within it\n completed when the button to skip all is pressed from this tip.\n ', manager=services.get_instance_manager( sims4.resources.Types.TUTORIAL_TIP), class_restrictions='TutorialTipGroup', export_modes=ExportModes.ClientBinary) } def __init__(self): raise NotImplementedError @classmethod def activate(cls): tutorial_service = services.get_tutorial_service() client = services.client_manager().get_first_client() actor_sim_info = client.active_sim.sim_info target_sim_info = actor_sim_info housemate_sim_info = None tutorial_drama_node = None drama_scheduler = services.drama_scheduler_service() if drama_scheduler is not None: drama_nodes = drama_scheduler.get_running_nodes_by_drama_node_type( DramaNodeType.TUTORIAL) if drama_nodes: tutorial_drama_node = drama_nodes[0] housemate_sim_info = tutorial_drama_node.get_housemate_sim_info( ) player_sim_info = tutorial_drama_node.get_player_sim_info() if cls.sim_actor == TutorialTipActorOption.PLAYER_SIM: actor_sim_info = player_sim_info elif cls.sim_actor == TutorialTipActorOption.HOUSEMATE_SIM: actor_sim_info = housemate_sim_info if cls.sim_target == TutorialTipActorOption.PLAYER_SIM: target_sim_info = player_sim_info elif cls.sim_target == TutorialTipActorOption.HOUSEMATE_SIM: target_sim_info = housemate_sim_info if cls.gameplay_immediate_test is not None: resolver = event_testing.resolver.SingleSimResolver(actor_sim_info) if resolver(cls.gameplay_immediate_test): cls.satisfy() else: return for buff_ref in cls.buffs: actor_sim_info.add_buff_from_op(buff_ref.buff_type, buff_reason=buff_ref.buff_reason) if cls.gameplay_test is not None: services.get_event_manager().register_tests( cls, [cls.gameplay_test]) if cls.satisfy_on_active_sim_change: client = services.client_manager().get_first_client() if client is not None: client.register_active_sim_changed(cls._on_active_sim_change) if cls.commodities_to_solve: actor_sim = actor_sim_info.get_sim_instance() if actor_sim is not None: context = InteractionContext( actor_sim, InteractionContext.SOURCE_SCRIPT_WITH_USER_INTENT, priority.Priority.High, bucket=InteractionBucketType.DEFAULT) for commodity in cls.commodities_to_solve: if not actor_sim.queue.can_queue_visible_interaction(): break autonomy_request = autonomy.autonomy_request.AutonomyRequest( actor_sim, autonomy_mode=autonomy.autonomy_modes.FullAutonomy, commodity_list=(commodity, ), context=context, consider_scores_of_zero=True, posture_behavior=AutonomyPostureBehavior. IGNORE_SI_STATE, distance_estimation_behavior= AutonomyDistanceEstimationBehavior. ALLOW_UNREACHABLE_LOCATIONS, allow_opportunity_cost=False, autonomy_mode_label_override='Tutorial') selected_interaction = services.autonomy_service( ).find_best_action(autonomy_request) AffordanceObjectPair.execute_interaction( selected_interaction) if cls.gameplay_loots: resolver = DoubleSimResolver(actor_sim_info, target_sim_info) for loot_action in cls.gameplay_loots: loot_action.apply_to_resolver(resolver) if cls.restricted_affordances is not None and tutorial_service is not None: tutorial_service.set_restricted_affordances( cls.restricted_affordances.visible_affordances, cls.restricted_affordances.tooltip, cls.restricted_affordances.enabled_affordances) if cls.call_to_actions is not None: call_to_action_service = services.call_to_action_service() for call_to_action_fact in cls.call_to_actions: call_to_action_service.begin(call_to_action_fact, None) if cls.add_target_to_actor_household: household_manager = services.household_manager() household_manager.switch_sim_household(target_sim_info) if cls.make_housemate_unselectable and tutorial_service is not None: tutorial_service.set_unselectable_sim(housemate_sim_info) if cls.end_drama_node and tutorial_drama_node is not None: tutorial_drama_node.end() if cls.time_of_day is not None and tutorial_service is not None: tutorial_service.add_tutorial_alarm(cls, lambda _: cls.satisfy(), cls.time_of_day) @classmethod def _on_active_sim_change(cls, old_sim, new_sim): cls.satisfy() @classmethod def handle_event(cls, sim_info, event, resolver): if cls.gameplay_test is not None and resolver(cls.gameplay_test): if cls.sim_tested != TutorialTipTestSpecificityOption.UNSPECIFIED: client = services.client_manager().get_first_client() test_sim_info = client.active_sim.sim_info drama_scheduler = services.drama_scheduler_service() if drama_scheduler is not None: drama_nodes = drama_scheduler.get_running_nodes_by_drama_node_type( DramaNodeType.TUTORIAL) if drama_nodes: drama_node = drama_nodes[0] if cls.sim_tested == TutorialTipTestSpecificityOption.PLAYER_SIM: test_sim_info = drama_node.get_player_sim_info() elif cls.sim_tested == TutorialTipTestSpecificityOption.HOUSEMATE_SIM: test_sim_info = drama_node.get_housemate_sim_info() if test_sim_info is not sim_info: return cls.satisfy() @classmethod def satisfy(cls): op = distributor.ops.SetTutorialTipSatisfy(cls.guid64) distributor_instance = Distributor.instance() distributor_instance.add_op_with_no_owner(op) @classmethod def deactivate(cls): tutorial_service = services.get_tutorial_service() client = services.client_manager().get_first_client() if cls.gameplay_test is not None: services.get_event_manager().unregister_tests( cls, (cls.gameplay_test, )) if cls.satisfy_on_active_sim_change and client is not None: client.unregister_active_sim_changed(cls._on_active_sim_change) if cls.restricted_affordances is not None and tutorial_service is not None: tutorial_service.clear_restricted_affordances() if cls.call_to_actions is not None: call_to_action_service = services.call_to_action_service() for call_to_action_fact in cls.call_to_actions: call_to_action_service.end(call_to_action_fact) if cls.buffs_removed_on_deactivate: actor_sim_info = None if client is not None: actor_sim_info = client.active_sim.sim_info drama_scheduler = services.drama_scheduler_service() if drama_scheduler is not None: drama_nodes = drama_scheduler.get_running_nodes_by_drama_node_type( DramaNodeType.TUTORIAL) if drama_nodes: tutorial_drama_node = drama_nodes[0] if cls.sim_actor == TutorialTipActorOption.PLAYER_SIM: actor_sim_info = tutorial_drama_node.get_player_sim_info( ) elif cls.sim_actor == TutorialTipActorOption.HOUSEMATE_SIM: actor_sim_info = tutorial_drama_node.get_housemate_sim_info( ) if actor_sim_info is not None: for buff_ref in cls.buffs: actor_sim_info.remove_buff_by_type(buff_ref.buff_type) if cls.time_of_day is not None and tutorial_service is not None: tutorial_service.remove_tutorial_alarm(cls) if cls.make_housemate_unselectable and tutorial_service is not None: tutorial_service.set_unselectable_sim(None)
class Retirement: __qualname__ = 'Retirement' DAILY_PAY_TIME = TunableTimeOfDay( description= '\n The time of day the retirement payout will be given.\n ', default_hour=7) DAILY_PAY_MULTIPLIER = TunableMultiplier.TunableFactory( description= '\n Multiplier on the average daily pay of the retired career the Sim will\n get every day.\n ' ) DAILY_PAY_NOTIFICATION = _get_notification_tunable_factory( description= '\n Message when a Sim receives a retirement payout.\n ') RETIREMENT_NOTIFICATION = _get_notification_tunable_factory( description='\n Message when a Sim retires.\n ') __slots__ = ('_sim_info', '_career_uid', '_alarm_handle') def __init__(self, sim_info, retired_career_uid): self._sim_info = sim_info self._career_uid = retired_career_uid self._alarm_handle = None @property def career_uid(self): return self._career_uid def start(self, send_retirement_notification=False): self._add_alarm() self._distribute() if send_retirement_notification: self.send_dialog(Retirement.RETIREMENT_NOTIFICATION) def stop(self): self._clear_alarm() def _add_alarm(self): now = services.time_service().sim_now time_span = now.time_till_next_day_time(Retirement.DAILY_PAY_TIME) self._alarm_handle = alarms.add_alarm(self._sim_info, time_span, self._alarm_callback, repeating=False, use_sleep_time=False) def _clear_alarm(self): if self._alarm_handle is not None: alarms.cancel_alarm(self._alarm_handle) self._alarm_handle = None def _alarm_callback(self, alarm_handle): self._add_alarm() pay = self._get_daily_pay() self._sim_info.household.funds.add( pay, protocolbuffers.Consts_pb2.TELEMETRY_MONEY_CAREER, self._sim_info.get_sim_instance( allow_hidden_flags=ALL_HIDDEN_REASONS)) self.send_dialog(Retirement.DAILY_PAY_NOTIFICATION, pay) def _get_career_history(self): return self._sim_info.career_tracker.career_history[self._career_uid] def _get_career_track_tuning(self): history = self._get_career_history() career_track_manager = services.get_instance_manager( sims4.resources.Types.CAREER_TRACK) track = career_track_manager.get(history.track_uid) return track def _get_career_level_tuning(self): history = self._get_career_history() track = self._get_career_track_tuning() level = track.career_levels[history.career_level] return level def _get_daily_pay(self): level = self._get_career_level_tuning() ticks = 0 for (start_ticks, end_ticks) in level.work_schedule( init_only=True).get_schedule_times(): ticks += end_ticks - start_ticks hours = date_and_time.ticks_to_time_unit(ticks, date_and_time.TimeUnit.HOURS, True) pay_rate = level.simoleons_per_hour for trait_bonus in level.simolean_trait_bonus: while self._sim_info.trait_tracker.has_trait(trait_bonus.trait): pay_rate += pay_rate * (trait_bonus.bonus * 0.01) pay_rate = int(pay_rate) week_pay = hours * pay_rate day_pay = week_pay / 7 resolver = event_testing.resolver.SingleSimResolver(self._sim_info) multiplier = Retirement.DAILY_PAY_MULTIPLIER.get_multiplier(resolver) adjusted_pay = int(day_pay * multiplier) return adjusted_pay def send_dialog(self, notification, *additional_tokens, on_response=None): if self._sim_info.is_npc: return resolver = event_testing.resolver.SingleSimResolver(self._sim_info) dialog = notification(self._sim_info, resolver=resolver) if dialog is not None: track = self._get_career_track_tuning() level = self._get_career_level_tuning() job = level.title(self._sim_info) career = track.career_name(self._sim_info) tokens = (job, career) + additional_tokens dialog.show_dialog(additional_tokens=tokens, icon_override=(track.icon, None), secondary_icon_override=(None, self._sim_info), on_response=on_response) def _distribute(self): op = protocolbuffers.DistributorOps_pb2.SetCareers() with ProtocolBufferRollback(op.careers) as career_op: history = self._get_career_history() career_op.career_uid = self._career_uid career_op.career_level = history.career_level career_op.career_track = history.track_uid career_op.user_career_level = history.user_level career_op.is_retired = True distributor = Distributor.instance() if distributor is not None: distributor.add_op( self._sim_info, GenericProtocolBufferOp(Operation.SET_CAREER, career_op))
class StoryProgressionRelationshipCulling(_StoryProgressionAction): PLAYED_NPC_TO_PLAYED_NPC_MAX_RELATIONSHIPS = Tunable( description= '\n The max number of relationships that a played NPC is allowed to have\n with other NPCs. This is for sims that have been played in the past,\n but are not part of the active household now, and only operates on\n relationships with other NPCs.\n ', tunable_type=int, default=20) CULLING_BUFFER_AMOUNT = Tunable( description= '\n When relationships are culled from an NPC relationship tracker due to \n the number of relationships exceeding the cap, this is how much below\n the cap the number of relationships will be after the culling.\n ', tunable_type=int, default=5) PLAYED_NPC_REL_DEPTH_CULLING_THRESHOLD = Tunable( description= '\n The relationship depth below which an NPC relationship will be culled.\n This is for sims that have been played in the past, but are not part of\n the active household now, and only operates on relationships with other NPCs.\n ', tunable_type=int, default=30) FACTORY_TUNABLES = { 'time_of_day': TunableTuple( description= '\n Only run this action when it is between a certain time of day.\n ', start_time=TunableTimeOfDay(default_hour=2), end_time=TunableTimeOfDay(default_hour=5)) } def should_process(self, options): current_time = services.time_service().sim_now if not current_time.time_between_day_times(self.time_of_day.start_time, self.time_of_day.end_time): return False return True def process_action(self, story_progression_flags): self.trigger_npc_relationship_culling() @classmethod def _trigger_relationship_telemetry(cls, hook_name, culling_event_id): metrics = performance.performance_commands.get_relationship_metrics() with telemetry_helper.begin_hook(writer, hook_name) as hook: hook.write_int('clid', culling_event_id) hook.write_int('rels', metrics.rels) hook.write_int('ract', metrics.rels_active) hook.write_int('rpla', metrics.rels_played) hook.write_int('runp', metrics.rels_unplayed) hook.write_int('rbow', metrics.rel_bits_one_way) hook.write_int('rbbi', metrics.rel_bits_bi) hook.write_float('avmr', metrics.avg_meaningful_rels) @classmethod def _add_relationship_data_to_list(cls, output_list, sim_info, rel_id, active_household_id, culled_status, reason=''): target_sim_info = services.sim_info_manager().get(rel_id) total_depth = sim_info.relationship_tracker.get_relationship_depth( rel_id) rel_bits = sim_info.relationship_tracker.get_depth_sorted_rel_bit_list( rel_id) formated_rel_bits = list() for rel_bit in rel_bits: bit_depth = rel_bit.depth rel_bit_string = str(rel_bit) rel_bit_string = rel_bit_string.replace( "<class 'sims4.tuning.instances.", '') rel_bit_string = rel_bit_string.replace('>', '') rel_bit_string = rel_bit_string.replace("'", '') rel_bit_string += ' ({})'.format(bit_depth) formated_rel_bits.append(rel_bit_string) output_list.append( RelationshipGSIData(sim_info, target_sim_info, total_depth, formated_rel_bits, culled_status, reason)) @classmethod def trigger_npc_relationship_culling(cls): culling_event_id = int(random.random() * 1000) cls._trigger_relationship_telemetry(TELEMETRY_HOOK_CULL_REL_BEFORE, culling_event_id) cls._do_npc_relationship_culling() cls._trigger_relationship_telemetry(TELEMETRY_HOOK_CULL_REL_AFTER, culling_event_id) @classmethod def _do_npc_relationship_culling(cls): CULLED = 'Culled' NOT_CULLED = 'Not Culled' UNPLAYED_TO_UNPLAYED = 'Unplayed_to_Unplayed' BELOW_DEPTH = 'Below_Depth_Threshold' MAX_CAP = 'Over_Max_Cap' active_sim_ids = frozenset(sim_info.id for sim_info in services.active_household()) active_household_id = services.active_household_id() gsi_enabled = gsi_handlers.relationship_culling_handlers.archiver.enabled if gsi_enabled: relationship_data = list() culled_relationship_data = list() total_culled_count = 0 for sim_info in sorted(services.sim_info_manager().values(), key=operator.attrgetter('is_player_sim', 'is_played_sim')): if sim_info.household.id == active_household_id or sim_info.is_instanced( allow_hidden_flags=ALL_HIDDEN_REASONS): if gsi_enabled: for rel_id in sim_info.relationship_tracker.target_sim_gen( ): cls._add_relationship_data_to_list( relationship_data, sim_info, rel_id, active_household_id, NOT_CULLED) is_player_sim = sim_info.is_player_sim num_to_cull = 0 ids_to_cull_with_reasons = set() if is_player_sim: sorted_relationships = sorted( sim_info.relationship_tracker, key=lambda rel: rel.get_relationship_depth( sim_info.sim_id)) num_over_cap = len( sorted_relationships ) - cls.PLAYED_NPC_TO_PLAYED_NPC_MAX_RELATIONSHIPS if num_over_cap > 0: num_to_cull = num_over_cap + cls.CULLING_BUFFER_AMOUNT for rel in sorted_relationships: if not rel.get_other_sim_id( sim_info.sim_id) in active_sim_ids: if not rel.can_cull_relationship( consider_convergence=False): continue if rel.get_relationship_depth( sim_info.sim_id ) < cls.PLAYED_NPC_REL_DEPTH_CULLING_THRESHOLD: ids_to_cull_with_reasons.add( (rel.get_other_sim_id(sim_info.sim_id), BELOW_DEPTH)) num_to_cull -= 1 elif num_to_cull > 0: ids_to_cull_with_reasons.add( (rel.get_other_sim_id(sim_info.sim_id), MAX_CAP)) num_to_cull -= 1 else: break else: for rel in sim_info.relationship_tracker: if rel.get_other_sim_id( sim_info.sim_id) in active_sim_ids: continue target_sim_info = rel.get_other_sim_info( sim_info.sim_id) if target_sim_info is not None and target_sim_info.is_player_sim: continue if not rel.can_cull_relationship( consider_convergence=False): continue ids_to_cull_with_reasons.add( (rel.get_other_sim_id(sim_info.sim_id), UNPLAYED_TO_UNPLAYED)) if num_to_cull > 0: logger.warn( 'Relationship Culling could not find enough valid relationships to cull to bring the total number below the cap. Cap exceeded by: {}, Sim {}', num_to_cull, sim_info) for (rel_id, reason) in ids_to_cull_with_reasons: if gsi_enabled: cls._add_relationship_data_to_list( culled_relationship_data, sim_info, rel_id, active_household_id, CULLED, reason=reason) total_culled_count += 1 sim_info.relationship_tracker.destroy_relationship( rel_id) if gsi_enabled: for rel_id in sim_info.relationship_tracker.target_sim_gen( ): cls._add_relationship_data_to_list( relationship_data, sim_info, rel_id, active_household_id, NOT_CULLED) else: is_player_sim = sim_info.is_player_sim num_to_cull = 0 ids_to_cull_with_reasons = set() if is_player_sim: sorted_relationships = sorted( sim_info.relationship_tracker, key=lambda rel: rel.get_relationship_depth(sim_info. sim_id)) num_over_cap = len( sorted_relationships ) - cls.PLAYED_NPC_TO_PLAYED_NPC_MAX_RELATIONSHIPS if num_over_cap > 0: num_to_cull = num_over_cap + cls.CULLING_BUFFER_AMOUNT for rel in sorted_relationships: if not rel.get_other_sim_id( sim_info.sim_id) in active_sim_ids: if not rel.can_cull_relationship( consider_convergence=False): continue if rel.get_relationship_depth( sim_info.sim_id ) < cls.PLAYED_NPC_REL_DEPTH_CULLING_THRESHOLD: ids_to_cull_with_reasons.add( (rel.get_other_sim_id(sim_info.sim_id), BELOW_DEPTH)) num_to_cull -= 1 elif num_to_cull > 0: ids_to_cull_with_reasons.add( (rel.get_other_sim_id(sim_info.sim_id), MAX_CAP)) num_to_cull -= 1 else: break else: for rel in sim_info.relationship_tracker: if rel.get_other_sim_id( sim_info.sim_id) in active_sim_ids: continue target_sim_info = rel.get_other_sim_info( sim_info.sim_id) if target_sim_info is not None and target_sim_info.is_player_sim: continue if not rel.can_cull_relationship( consider_convergence=False): continue ids_to_cull_with_reasons.add( (rel.get_other_sim_id(sim_info.sim_id), UNPLAYED_TO_UNPLAYED)) if num_to_cull > 0: logger.warn( 'Relationship Culling could not find enough valid relationships to cull to bring the total number below the cap. Cap exceeded by: {}, Sim {}', num_to_cull, sim_info) for (rel_id, reason) in ids_to_cull_with_reasons: if gsi_enabled: cls._add_relationship_data_to_list( culled_relationship_data, sim_info, rel_id, active_household_id, CULLED, reason=reason) total_culled_count += 1 sim_info.relationship_tracker.destroy_relationship(rel_id) if gsi_enabled: for rel_id in sim_info.relationship_tracker.target_sim_gen( ): cls._add_relationship_data_to_list( relationship_data, sim_info, rel_id, active_household_id, NOT_CULLED) if gsi_enabled: gsi_handlers.relationship_culling_handlers.archive_relationship_culling( total_culled_count, relationship_data, culled_relationship_data)
class HolidayTradition(HasTunableReference, HolidayTraditionDisplayMixin, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager( sims4.resources.Types.HOLIDAY_TRADITION)): INSTANCE_TUNABLES = { 'situation_goal': TunableReference( description= '\n This is the situation goal that will be offered when this tradition\n is active.\n ', manager=services.get_instance_manager( sims4.resources.Types.SITUATION_GOAL)), 'pre_holiday_buffs': TunableList( description= '\n A list of buffs that will be given out to all of the player Sims\n during the pre-holiday period of each holiday.\n ', tunable=TunableReference( description= '\n A buff that is given to all of the player Sims when it is the\n pre-holiday period.\n ', manager=services.get_instance_manager( sims4.resources.Types.BUFF)), unique_entries=True), 'pre_holiday_buff_reason': OptionalTunable( description= '\n If set, specify a reason why the buff was added.\n ', tunable=TunableLocalizedString( description= '\n The reason the buff was added. This will be displayed in the\n buff tooltip.\n ' )), 'holiday_buffs': TunableList( description= '\n A list of buffs that will be given out to all Sims during each\n holiday.\n ', tunable=TunableReference( description= '\n A buff that is given to all Sims during the holiday.\n ', manager=services.get_instance_manager( sims4.resources.Types.BUFF)), unique_entries=True), 'holiday_buff_reason': OptionalTunable( description= '\n If set, specify a reason why the buff was added.\n ', tunable=TunableLocalizedString( description= '\n The reason the buff was added. This will be displayed in the\n buff tooltip.\n ' )), 'drama_nodes_to_score': TunableList( description= '\n Drama nodes that we will attempt to schedule and score when this\n tradition becomes active.\n ', tunable=TunableReference( description= '\n A drama node that we will put in the scoring pass when this\n tradition becomes active.\n ', manager=services.get_instance_manager( sims4.resources.Types.DRAMA_NODE)), unique_entries=True), 'drama_nodes_to_run': TunableList( description= '\n Drama nodes that will be run when the tradition is activated.\n ', tunable=TunableReference( description= '\n A drama node that we will run when the holiday becomes active.\n ', manager=services.get_instance_manager( sims4.resources.Types.DRAMA_NODE)), unique_entries=True), 'additional_walkbys': SituationCurve.TunableFactory( description= '\n An additional walkby schedule that will be added onto the walkby\n schedule when the tradition is active.\n ', get_create_params={'user_facing': False}), 'preference': TunableList( description= '\n A list of pairs of preference categories and tests. To determine\n what a Sim feels about a tradition each set of tests in this list\n will be run in order. When one of the test sets passes then we\n will set that as the preference. If none of them pass we will\n default to LIKES.\n ', tunable=TunableTuple( description= '\n A pair of preference and test set.\n ', preference=TunableEnumEntry( description= '\n The preference that the Sim will have to this tradition if\n the test set passes.\n ', tunable_type=TraditionPreference, default=TraditionPreference.LIKES), tests=TunablePreferenceTestList( description= '\n A set of tests that need to pass for the Sim to have the\n tuned preference.\n ' ), reason=OptionalTunable( description= '\n If enabled then we will also give this reason as to why the\n preference is the way it is.\n ', tunable=TunableLocalizedString( description= '\n The reason that the Sim has this preference.\n ' )))), 'preference_reward_buff': OptionalTunable( description= '\n If enabled then if the Sim loves this tradition when the holiday is\n completed they will get a special buff if they completed the\n tradition.\n ', tunable=TunableBuffReference( description= '\n The buff given if this Sim loves the tradition and has completed\n it at the end of the holiday.\n ' )), 'selectable': Tunable( description= '\n If checked then this tradition will appear in the tradition\n selection.\n ', tunable_type=bool, default=True, tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'lifecycle_actions': TunableList( description= '\n Actions that occur as a result of the tradition activation/de-activation.\n ', tunable=TraditionActions.TunableFactory()), 'events': TunableList( description= '\n A list of times and things we want to happen at that time.\n ', tunable=TunableTuple( description= '\n A pair of a time of day and event of something that we want\n to occur.\n ', time=TunableTimeOfDay( description= '\n The time of day this event will occur.\n ' ), event=TunableVariant( description= '\n What we want to occur at this time.\n ', modify_items=ModifyAllItems(), start_situation=StartSituation(), default='start_situation'))), 'core_object_tags': TunableTags( description= '\n Tags of all the core objects used in this tradition.\n ', filter_prefixes=('func', ), tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'deco_object_tags': TunableTags( description= '\n Tags of all the deco objects used in this tradition.\n ', filter_prefixes=('func', ), tuning_group=GroupNames.UI, export_modes=ExportModes.All), 'business_cost_multiplier': TunableMapping( description= '\n A mapping between the business type and the cost multiplier that\n we want to use if this tradition is active.\n ', key_type=TunableEnumEntry( description= '\n The type of business that we want to apply this price modifier\n on.\n ', tunable_type=BusinessType, default=BusinessType.INVALID, invalid_enums=(BusinessType.INVALID, )), value_type=TunableRange( description= '\n The value of the multiplier to use.\n ', tunable_type=float, default=1.0, minimum=0.0)) } @classmethod def _verify_tuning_callback(cls): if cls._display_data.instance_display_description is None: logger.error('Tradition {} missing display description', cls) if cls._display_data.instance_display_icon is None: logger.error('Tradition {} missing display icon', cls) if cls._display_data.instance_display_name is None: logger.error('Tradition {} missing display name', cls) def __init__(self): self._state = HolidayState.INITIALIZED self._buffs_added = defaultdict(list) self._event_alarm_handles = {} self._drama_node_processor = None @property def state(self): return self._state @classmethod def get_buiness_multiplier(cls, business_type): return cls.business_cost_multiplier.get(business_type, 1.0) @classmethod def get_sim_preference(cls, sim_info): resolver = SingleSimResolver(sim_info) for possible_preference in cls.preference: if possible_preference.tests.run_tests(resolver): return (possible_preference.preference, possible_preference.reason) return (TraditionPreference.LIKES, None) def on_sim_spawned(self, sim): if self._state == HolidayState.PRE_DAY: if sim.is_npc: return for buff in self.pre_holiday_buffs: buff_handle = sim.add_buff( buff, buff_reason=self.pre_holiday_buff_reason) if buff_handle is not None: self._buffs_added[sim.sim_id].append(buff_handle) elif self._state == HolidayState.RUNNING: for buff in self.holiday_buffs: buff_handle = sim.add_buff( buff, buff_reason=self.holiday_buff_reason) if buff_handle is not None: self._buffs_added[sim.sim_id].append(buff_handle) def activate_pre_holiday(self): if self._state >= HolidayState.PRE_DAY: logger.error( 'Tradition {} is trying to be put into the pre_holiday, but is already in {} which is farther along.', self, self._state) return self._state = HolidayState.PRE_DAY if self.pre_holiday_buffs: services.sim_spawner_service().register_sim_spawned_callback( self.on_sim_spawned) for sim_info in services.active_household().instanced_sims_gen(): for buff in self.pre_holiday_buffs: buff_handle = sim_info.add_buff( buff, buff_reason=self.pre_holiday_buff_reason) if buff_handle is not None: self._buffs_added[sim_info.sim_id].append(buff_handle) def _remove_all_buffs(self): sim_info_manager = services.sim_info_manager() for (sim_id, buff_handles) in self._buffs_added.items(): sim_info = sim_info_manager.get(sim_id) if sim_info is None: continue if sim_info.Buffs is None: continue for buff_handle in buff_handles: sim_info.remove_buff(buff_handle) self._buffs_added.clear() def _deactivate_pre_holiday(self): if self.pre_holiday_buffs: services.sim_spawner_service().unregister_sim_spawned_callback( self.on_sim_spawned) self._remove_all_buffs() def deactivate_pre_holiday(self): if self._state != HolidayState.PRE_DAY: logger.error( 'Tradition {} is trying to deactivate the preday, but it is in the {} state, not that one.', self, self._state) self._state = HolidayState.SHUTDOWN self._deactivate_pre_holiday() def _create_event_alarm(self, key, event): def callback(_): event.event.perform(GlobalResolver()) del self._event_alarm_handles[key] now = services.time_service().sim_now time_to_event = now.time_till_next_day_time(event.time) if key in self._event_alarm_handles: alarms.cancel_alarm(self._event_alarm_handles[key]) self._event_alarm_handles[key] = alarms.add_alarm( self, time_to_event, callback) def _process_scoring_gen(self, timeline): try: yield from services.drama_scheduler_service( ).score_and_schedule_nodes_gen(self.drama_nodes_to_score, 1, timeline=timeline) except GeneratorExit: raise except Exception as exception: logger.exception('Exception while scoring DramaNodes: ', exc=exception, level=sims4.log.LEVEL_ERROR) finally: self._drama_node_processor = None def activate_holiday(self, from_load=False, from_customization=False): if self._state >= HolidayState.RUNNING: logger.error( 'Tradition {} is trying to be put into the Running state, but is already in {} which is farther along.', self, self._state) return self._deactivate_pre_holiday() self._state = HolidayState.RUNNING if self.holiday_buffs: services.sim_spawner_service().register_sim_spawned_callback( self.on_sim_spawned) for sim_info in services.sim_info_manager().instanced_sims_gen(): for buff in self.holiday_buffs: buff_handle = sim_info.add_buff( buff, buff_reason=self.holiday_buff_reason) if buff_handle is not None: self._buffs_added[sim_info.sim_id].append(buff_handle) for (key, event) in enumerate(self.events): self._create_event_alarm(key, event) if not from_load: resolver = GlobalResolver() for actions in self.lifecycle_actions: actions.try_perform( resolver, TraditionActivationEvent.TRADITION_ADD if from_customization else TraditionActivationEvent.HOLIDAY_ACTIVATE) if self.drama_nodes_to_score: sim_timeline = services.time_service().sim_timeline self._drama_node_processor = sim_timeline.schedule( elements.GeneratorElement(self._process_scoring_gen)) drama_scheduler = services.drama_scheduler_service() for drama_node in self.drama_nodes_to_run: drama_scheduler.run_node(drama_node, resolver) def deactivate_holiday(self, from_customization=False): if self._state != HolidayState.RUNNING: logger.error( 'Tradition {} is trying to deactivate the tradition, but it is in the {} state, not that one.', self, self._state) self._state = HolidayState.SHUTDOWN if self.holiday_buffs: services.sim_spawner_service().unregister_sim_spawned_callback( self.on_sim_spawned) self._remove_all_buffs() for alarm in self._event_alarm_handles.values(): alarms.cancel_alarm(alarm) self._event_alarm_handles.clear() resolver = GlobalResolver() for actions in self.lifecycle_actions: actions.try_perform( resolver, TraditionActivationEvent.TRADITION_REMOVE if from_customization else TraditionActivationEvent.HOLIDAY_DEACTIVATE) def get_additional_walkbys(self, predicate=lambda _: True): weighted_situations = self.additional_walkbys.get_weighted_situations( predicate=predicate) if weighted_situations is None: return () return weighted_situations
class AutonomyComponent(Component, component_name=types.AUTONOMY_COMPONENT): __qualname__ = 'AutonomyComponent' STANDARD_STATIC_COMMODITY_SKIP_SET = TunableSet( TunableReference(manager=services.get_instance_manager( Types.STATIC_COMMODITY), description='A static commodity.'), description= 'A set of static commodities. Any affordances that provide these commodities will be skipped in a standard autonomy run.' ) DELAY_UNTIL_RUNNING_AUTONOMY_THE_FIRST_TIME = TunableSimMinute( 5, description= 'The amount of time to wait before running autonomy the first time, in Sim minutes.' ) PREROLL_AUTONOMY_AFFORDANCE_SKIP_SET = TunableSet( TunableReference( manager=services.get_instance_manager(Types.INTERACTION), description= 'The affordances to skip trying to solve on preroll autonomy.'), description= 'A set of affordances that will be skipped when preroll autonomy is run.' ) SLEEP_SCHEDULE = TunableTuple( schedule=TunableList( TunableTuple(time_from_work_start=Tunable( float, 0, description= 'The time relative to the start work time that the buff should be added. For example, if you want the Sim to gain this static commodity 10 hours before work, set this value to 10' ), buff=buffs.tunable.TunableBuffReference( description='Buff that gets added to the sim.'))), default_work_time=TunableTimeOfDay( default_hour=9, description= "The default time that the Sim assumes he needs to be at work if he doesn't have a career. This is only used for sleep." )) MIXERS_TO_CACHE = TunableRange( description= '\n Number of mixers to cache during a subaction request\n ', tunable_type=int, default=3, minimum=1) _STORE_AUTONOMY_REQUEST_HISTORY = False def __init__(self, owner): super().__init__(owner) self._last_user_directed_action_time = DateAndTime(0) self._last_autonomous_action_time = DateAndTime(0) self._last_no_result_time = None self._autonomy_skip_sis = set() self._autonomy_enabled = False self._full_autonomy_alarm_handle = None self._multitasking_roll = UNSET self._role_tracker = RoleStateTracker(owner) self._full_autonomy_request = None self._full_autonomy_element_handle = None self._sleep_buff_handle = None self._sleep_buff_alarms = {} self._sleep_buff_reset = None self._autonomy_settings = autonomy.settings.AutonomySettings() self._cached_mixer_interactions = [] def on_add(self): self.owner.si_state.on_changed.append(self.reset_multitasking_roll) self.owner.si_state.on_changed.append( self.invalidate_mixer_interaction_cache) def on_remove(self): for alarm_handle in self._sleep_buff_alarms.keys(): alarms.cancel_alarm(alarm_handle) if self._full_autonomy_request is not None: self._full_autonomy_request.valid = False self._full_autonomy_request = None if self._sleep_buff_reset is not None: alarms.cancel_alarm(self._sleep_buff_reset) self.owner.si_state.on_changed.remove( self.invalidate_mixer_interaction_cache) self.owner.si_state.on_changed.remove(self.reset_multitasking_roll) self.on_sim_reset(True) self._role_tracker.shutdown() def _on_run_full_autonomy_callback(self, handle): if self._full_autonomy_element_handle is not None: return timeline = services.time_service().sim_timeline self._full_autonomy_element_handle = timeline.schedule( elements.GeneratorElement(self._run_full_autonomy_callback_gen)) def _run_full_autonomy_callback_gen(self, timeline): try: self.set_last_autonomous_action_time(False) autonomy_pushed_interaction = yield self._attempt_full_autonomy_gen( timeline) self._last_autonomy_result_was_none = not autonomy_pushed_interaction except Exception: logger.exception( 'Exception hit while processing FullAutonomy for {}:', self.owner, owner='rez') finally: self._full_autonomy_element_handle = None self._schedule_next_full_autonomy_update() def _attempt_full_autonomy_gen(self, timeline): if self._full_autonomy_request is not None and self._full_autonomy_request.valid: logger.debug( 'Ignoring full autonomy request for {} due to pending request in the queue.', self.owner) return False if self.to_skip_autonomy(): if gsi_handlers.autonomy_handlers.archiver.enabled: gsi_handlers.autonomy_handlers.archive_autonomy_data( self.owner, 'None - Running SIs are preventing autonomy from running: {}' .format(self._autonomy_skip_sis), 'FullAutonomy', None) return False if not self._test_full_autonomy(): return False try: selected_interaction = None try: self._full_autonomy_request = self._create_autonomy_request() selected_interaction = yield services.autonomy_service( ).find_best_action_gen(timeline, self._full_autonomy_request, archive_if_enabled=False) finally: self._full_autonomy_request.valid = False if not self._autonomy_enabled: if gsi_handlers.autonomy_handlers.archiver.enabled: gsi_handlers.autonomy_handlers.archive_autonomy_data( self.owner, 'None - Autonomy Disabled', 'FullAutonomy', None) return False if not self._test_full_autonomy(): if selected_interaction: selected_interaction.invalidate() return False chose_get_comfortable = False if selected_interaction is None: selected_interaction = self.get_comfortable_interaction() chose_get_comfortable = True if gsi_handlers.autonomy_handlers.archiver.enabled: gsi_handlers.autonomy_handlers.archive_autonomy_data( self._full_autonomy_request.sim, selected_interaction, self._full_autonomy_request.autonomy_mode_label, self._full_autonomy_request.gsi_data) if selected_interaction is not None: result = self._push_interaction(selected_interaction) if not result and gsi_handlers.autonomy_handlers.archiver.enabled: gsi_handlers.autonomy_handlers.archive_autonomy_data( self.owner, 'Failed - interaction failed to be pushed {}.'.format( selected_interaction), 'FullAutonomy', None) if result: if gsi_handlers.autonomy_handlers.archiver.enabled: gsi_handlers.autonomy_handlers.archive_autonomy_data( self._full_autonomy_request.sim, selected_interaction, self._full_autonomy_request.autonomy_mode_label, self._full_autonomy_request.gsi_data) if chose_get_comfortable: return False return True return False finally: if selected_interaction is not None: selected_interaction.invalidate() def _test_full_autonomy(self): result = FullAutonomy.test(self.owner) if not result: if gsi_handlers.autonomy_handlers.archiver.enabled: gsi_handlers.autonomy_handlers.archive_autonomy_data( self.owner, result.reason, 'FullAutonomy', None) return False return True @componentmethod def run_test_autonomy_ping(self): autonomy_request = self._create_autonomy_request() selected_interaction = services.autonomy_service().find_best_action( autonomy_request) return selected_interaction @componentmethod def cancel_actively_running_full_autonomy_request(self): if self._full_autonomy_element_handle is not None: self._full_autonomy_element_handle.trigger_hard_stop() self._full_autonomy_element_handle = None @caches.cached def is_object_autonomously_available(self, obj): autonomy_rule = self.owner.get_off_lot_autonomy_rule_type() off_lot_radius = self.owner.get_off_lot_autonomy_radius() sim_is_on_active_lot = self.owner.is_on_active_lot( tolerance=self.owner.get_off_lot_autonomy_tolerance()) return self.get_autonomous_availability_of_object( obj, autonomy_rule, off_lot_radius, sim_is_on_active_lot) def get_autonomous_availability_of_object(self, obj, autonomy_rule, off_lot_radius, sim_is_on_active_lot, reference_object=None): reference_object = self.owner if reference_object is None else reference_object if obj is self.owner: return True object_is_on_active_lot = obj.is_on_active_lot() if object_is_on_active_lot: if autonomy_rule == autonomy.autonomy_modifier.OffLotAutonomyRules.DEFAULT and not sim_is_on_active_lot: return False if autonomy_rule == autonomy.autonomy_modifier.OffLotAutonomyRules.OFF_LOT_ONLY: return False else: if autonomy_rule == autonomy.autonomy_modifier.OffLotAutonomyRules.ON_LOT_ONLY: return False if off_lot_radius == 0: return False if off_lot_radius > 0: delta = obj.position - reference_object.position if delta.magnitude() > off_lot_radius: return False if autonomy_rule != autonomy.autonomy_modifier.OffLotAutonomyRules.UNLIMITED and obj.is_sim and not object_is_on_active_lot: if self.owner.is_on_active_lot( tolerance=self.owner.get_off_lot_autonomy_tolerance()): return False autonomy_service = services.autonomy_service() target_delta = obj.intended_position - obj.position if target_delta.magnitude_squared( ) > autonomy_service.MAX_OPEN_STREET_ROUTE_DISTANCE_FOR_SOCIAL_TARGET_SQUARED: return False distance_from_me = obj.intended_position - self.owner.intended_position if distance_from_me.magnitude_squared( ) > autonomy_service.MAX_OPEN_STREET_ROUTE_DISTANCE_FOR_INITIATING_SOCIAL_SQUARED: return False if self.owner.locked_from_obj_by_privacy(obj): return False return True def get_comfortable_interaction(self): sim = self.owner if not sim.posture.unconstrained: return if sim.get_main_group(): return affordance = interactions.utils.satisfy_constraint_interaction.SatisfyConstraintSuperInteraction aop = AffordanceObjectPair( affordance, None, affordance, None, constraint_to_satisfy=STAND_OR_SIT_CONSTRAINT, route_fail_on_transition_fail=False, name_override='Satisfy[GetComfortable]', allow_posture_changes=True) context = InteractionContext(sim, InteractionContext.SOURCE_GET_COMFORTABLE, Priority.Low, insert_strategy=QueueInsertStrategy.NEXT, must_run_next=True, cancel_if_incompatible_in_queue=True) execute_result = aop.interaction_factory(context) return execute_result.interaction def _create_autonomy_request(self): autonomy_request = AutonomyRequest( self.owner, autonomy_mode=FullAutonomy, skipped_static_commodities=self.STANDARD_STATIC_COMMODITY_SKIP_SET, limited_autonomy_allowed=False) return autonomy_request def _push_interaction(self, selected_interaction): should_log = services.autonomy_service().should_log(self.owner) if AffordanceObjectPair.execute_interaction(selected_interaction): return True if should_log: logger.debug('Autonomy failed to push {}', selected_interaction.affordance) if selected_interaction.target: self.owner.add_lockout(selected_interaction.target, AutonomyMode.LOCKOUT_TIME) return False def _schedule_next_full_autonomy_update(self, delay_in_sim_minutes=None): if not self._autonomy_enabled: return try: if delay_in_sim_minutes is None: delay_in_sim_minutes = self.get_time_until_next_update() logger.assert_log( isinstance(delay_in_sim_minutes, TimeSpan), 'delay_in_sim_minutes is not a TimeSpan object in _schedule_next_full_autonomy_update()', owner='rez') logger.debug('Scheduling next autonomy update for {} for {}', self.owner, delay_in_sim_minutes) self._create_full_autonomy_alarm(delay_in_sim_minutes) except Exception: logger.exception( 'Exception hit while attempting to schedule FullAutonomy for {}:', self.owner) def start_autonomy_alarm(self): self._autonomy_enabled = True self._schedule_next_full_autonomy_update( clock.interval_in_sim_minutes( self.DELAY_UNTIL_RUNNING_AUTONOMY_THE_FIRST_TIME)) def _create_full_autonomy_alarm(self, time_until_trigger): if self._full_autonomy_alarm_handle is not None: self._destroy_full_autonomy_alarm() if time_until_trigger.in_ticks() <= 0: time_until_trigger = TimeSpan(1) self._full_autonomy_alarm_handle = alarms.add_alarm( self, time_until_trigger, self._on_run_full_autonomy_callback, use_sleep_time=False) def _destroy_full_autonomy_alarm(self): if self._full_autonomy_alarm_handle is not None: alarms.cancel_alarm(self._full_autonomy_alarm_handle) self._full_autonomy_alarm_handle = None @componentmethod def get_multitasking_roll(self): if self._multitasking_roll is UNSET: self._multitasking_roll = random.random() return self._multitasking_roll @componentmethod def reset_multitasking_roll(self, interaction=None): if interaction is None or ( interaction.source is InteractionSource.PIE_MENU or interaction.source is InteractionSource.AUTONOMY ) or interaction.source is InteractionSource.SCRIPT: self._multitasking_roll = UNSET @componentmethod def on_sim_reset(self, is_kill): self.invalidate_mixer_interaction_cache(None) if self._full_autonomy_request is not None: self._full_autonomy_request.valid = False if is_kill: self._autonomy_enabled = False self._destroy_full_autonomy_alarm() if self._full_autonomy_element_handle is not None: self._full_autonomy_element_handle.trigger_hard_stop() self._full_autonomy_element_handle = None @componentmethod def run_full_autonomy_next_ping(self): self._last_user_directed_action_time = None self._schedule_next_full_autonomy_update(TimeSpan(1)) @componentmethod def set_last_user_directed_action_time(self, to_reschedule_autonomy=True): now = services.time_service().sim_now logger.debug('Setting user-directed action time for {} to {}', self.owner, now) self._last_user_directed_action_time = now self._last_autonomy_result_was_none = False if to_reschedule_autonomy: self._schedule_next_full_autonomy_update() @componentmethod def set_last_autonomous_action_time(self, to_reschedule_autonomy=True): now = services.time_service().sim_now logger.debug('Setting last autonomous action time for {} to {}', self.owner, now) self._last_autonomous_action_time = now self._last_autonomy_result_was_none = False if to_reschedule_autonomy: self._schedule_next_full_autonomy_update() @componentmethod def set_last_no_result_time(self, to_reschedule_autonomy=True): now = services.time_service().sim_now logger.debug('Setting last no-result time for {} to {}', self.owner, now) self._last_no_result_time = now if to_reschedule_autonomy: self._schedule_next_full_autonomy_update() @componentmethod def skip_autonomy(self, si, to_skip): if si.source == InteractionSource.BODY_CANCEL_AOP or si.source == InteractionSource.CARRY_CANCEL_AOP or si.source == InteractionSource.SOCIAL_ADJUSTMENT: return if to_skip: logger.debug('Skipping autonomy for {} due to {}', self.owner, si) self._autonomy_skip_sis.add(si) else: if si in self._autonomy_skip_sis: self._autonomy_skip_sis.remove(si) logger.debug('Unskipping autonomy for {} due to {}; {} is left.', self.owner, si, self._autonomy_skip_sis) def _get_last_user_directed_action_time(self): return self._last_user_directed_action_time def _get_last_autonomous_action_time(self): return self._last_autonomous_action_time def _get_last_no_result_time(self): return self._last_no_result_time @property def _last_autonomy_result_was_none(self): return self._last_no_result_time is not None @_last_autonomy_result_was_none.setter def _last_autonomy_result_was_none(self, value): if value == True: self.set_last_no_result_time(to_reschedule_autonomy=False) else: self._last_no_result_time = None @componentmethod def to_skip_autonomy(self): return bool(self._autonomy_skip_sis) @componentmethod def clear_all_autonomy_skip_sis(self): self._autonomy_skip_sis.clear() @componentmethod def is_player_active(self): if self._get_last_user_directed_action_time() is None: return False delta = services.time_service( ).sim_now - self._get_last_user_directed_action_time() if delta >= AutonomyMode.get_autonomy_delay_after_user_interaction(): return False return True @componentmethod def get_time_until_next_update(self, mode=FullAutonomy): time_to_run_autonomy = None if self.is_player_active(): time_to_run_autonomy = self._get_last_user_directed_action_time( ) + mode.get_autonomy_delay_after_user_interaction() elif self._last_autonomy_result_was_none: time_to_run_autonomy = self._get_last_no_result_time( ) + mode.get_no_result_delay_time() elif self.owner.si_state.has_visible_si( ignore_pending_complete=True ) or self.owner.transition_controller is not None and self.owner.transition_controller.interaction.visible and not self.owner.transition_controller.interaction.is_finishing or self.owner.queue.visible_len( ) > 0: time_to_run_autonomy = self._get_last_autonomous_action_time( ) + mode.get_autonomous_delay_time() else: time_to_run_autonomy = self._get_last_autonomous_action_time( ) + mode.get_autonomous_update_delay_with_no_primary_sis() delta_time = time_to_run_autonomy - services.time_service().sim_now if delta_time.in_ticks() <= 0: delta_time = TimeSpan(1) return delta_time @componentmethod def run_preroll_autonomy(self, ignored_objects): sim = self.owner sim_info = sim.sim_info current_away_action = sim_info.current_away_action if current_away_action is not None: commodity_list = current_away_action.get_commodity_preroll_list() static_commodity_list = current_away_action.get_static_commodity_preroll_list( ) else: commodity_list = None static_commodity_list = None autonomy_request = AutonomyRequest( self.owner, autonomy_mode=FullAutonomy, commodity_list=commodity_list, static_commodity_list=static_commodity_list, skipped_affordance_list=self.PREROLL_AUTONOMY_AFFORDANCE_SKIP_SET, distance_estimation_behavior=AutonomyDistanceEstimationBehavior. IGNORE_DISTANCE, ignored_object_list=ignored_objects, limited_autonomy_allowed=False, autonomy_mode_label_override='PrerollAutonomy') selected_interaction = services.autonomy_service().find_best_action( autonomy_request) if selected_interaction is None: return (None, None) if self._push_interaction(selected_interaction): return (selected_interaction.affordance, selected_interaction.target) return (None, None) @componentmethod def invalidate_mixer_interaction_cache(self, _): for interaction in self._cached_mixer_interactions: interaction.invalidate() self._cached_mixer_interactions.clear() def _should_run_cached_interaction(self, interaction_to_run): if interaction_to_run is None: return False super_interaction = interaction_to_run.super_interaction if super_interaction is None or super_interaction.is_finishing: return False if super_interaction.phase_index is not None and interaction_to_run.affordance not in super_interaction.all_affordances_gen( phase_index=super_interaction.phase_index): return False if interaction_to_run.is_finishing: return False if self.owner.is_sub_action_locked_out(interaction_to_run.affordance, interaction_to_run.target): return False return interaction_to_run.test() @componentmethod def run_subaction_autonomy(self): if not SubActionAutonomy.test(self.owner): if gsi_handlers.autonomy_handlers.archiver.enabled: gsi_handlers.autonomy_handlers.archive_autonomy_data( self.owner, 'None - Autonomy Disabled', 'SubActionAutonomy', gsi_handlers.autonomy_handlers.EMPTY_ARCHIVE) return EnqueueResult.NONE attempt_to_use_cache = False if gsi_handlers.autonomy_handlers.archiver.enabled: caching_info = [] else: caching_info = None while self._cached_mixer_interactions: attempt_to_use_cache = True interaction_to_run = self._cached_mixer_interactions.pop(0) if self._should_run_cached_interaction(interaction_to_run): enqueue_result = AffordanceObjectPair.execute_interaction( interaction_to_run) if enqueue_result: if gsi_handlers.autonomy_handlers.archiver.enabled: gsi_handlers.autonomy_handlers.archive_autonomy_data( self.owner, 'Using Cache: {}'.format(interaction_to_run), 'SubActionAutonomy', gsi_handlers.autonomy_handlers.EMPTY_ARCHIVE) return enqueue_result if interaction_to_run: interaction_to_run.invalidate() while caching_info is not None: caching_info.append( 'Failed to use cache interaction: {}'.format( interaction_to_run)) continue if caching_info is not None and attempt_to_use_cache: caching_info.append('Cache invalid:Regenerating') self.invalidate_mixer_interaction_cache(None) context = InteractionContext(self.owner, InteractionSource.AUTONOMY, Priority.Low) autonomy_request = AutonomyRequest(self.owner, context=context, consider_scores_of_zero=True, autonomy_mode=SubActionAutonomy) if caching_info is not None: caching_info.append('Caching: Mixers - START') initial_probability_result = None while len(self._cached_mixer_interactions) < self.MIXERS_TO_CACHE: interaction = services.autonomy_service().find_best_action( autonomy_request, consider_all_options=True, archive_if_enabled=False) if interaction is None: break if caching_info is not None: caching_info.append( 'caching interaction: {}'.format(interaction)) if initial_probability_result is None: initial_probability_result = list( autonomy_request.gsi_data['Probability']) self._cached_mixer_interactions.append(interaction) if caching_info is not None: caching_info.append('Caching: Mixers - DONE') if self._cached_mixer_interactions: interaction = self._cached_mixer_interactions.pop(0) if caching_info is not None: caching_info.append('Executing mixer: {}'.format(interaction)) enqueue_result = AffordanceObjectPair.execute_interaction( interaction) if caching_info is not None: autonomy_request.gsi_data['caching_info'] = caching_info autonomy_request.gsi_data[ 'Probability'] = initial_probability_result if enqueue_result: result_info = str(interaction) else: result_info = 'None - failed to execute: {}'.format( interaction) gsi_handlers.autonomy_handlers.archive_autonomy_data( autonomy_request.sim, result_info, autonomy_request.autonomy_mode_label, autonomy_request.gsi_data) return enqueue_result return EnqueueResult.NONE @componentmethod def add_role(self, role_state_type, role_affordance_target=None): for role_state in self._role_tracker: while isinstance(role_state, role_state_type): logger.error( 'Trying to add duplicate role:{}. Returning current instantiated role.', role_state_type) return role_state role_state = role_state_type(self.owner) self._role_tracker.add_role( role_state, role_affordance_target=role_affordance_target) return role_state @componentmethod def remove_role(self, role_state): return self._role_tracker.remove_role(role_state) @componentmethod def remove_role_of_type(self, role_state_type): for role_state_priority in self._role_tracker: for role_state in role_state_priority: while isinstance(role_state, role_state_type): self.remove_role(role_state) return True return False @componentmethod def active_roles(self): return self._role_tracker.active_role_states @componentmethod def reset_role_tracker(self): self._role_tracker.reset() @componentmethod def update_sleep_schedule(self): self._remove_sleep_schedule_buff() for alarm_handle in self._sleep_buff_alarms.keys(): alarms.cancel_alarm(alarm_handle) self._sleep_buff_alarms.clear() time_span_until_wakeup = self.get_time_until_next_wakeup() most_appropriate_buff = None for sleep_schedule_entry in sorted( self.SLEEP_SCHEDULE.schedule, key=lambda entry: entry.time_from_work_start, reverse=True): if time_span_until_wakeup.in_hours( ) <= sleep_schedule_entry.time_from_work_start: most_appropriate_buff = sleep_schedule_entry.buff else: time_until_buff_alarm = time_span_until_wakeup - date_and_time.create_time_span( hours=sleep_schedule_entry.time_from_work_start) alarm_handle = alarms.add_alarm( self, time_until_buff_alarm, self._add_buff_callback, True, date_and_time.create_time_span( hours=date_and_time.HOURS_PER_DAY)) self._sleep_buff_alarms[ alarm_handle] = sleep_schedule_entry.buff if most_appropriate_buff and most_appropriate_buff.buff_type: self._sleep_buff_handle = self.owner.add_buff( most_appropriate_buff.buff_type) if self._sleep_buff_reset: alarms.cancel_alarm(self._sleep_buff_reset) self._sleep_buff_reset = alarms.add_alarm(self, time_span_until_wakeup, self._reset_alarms_callback) @componentmethod def get_time_until_next_wakeup(self, offset_time: TimeSpan = None): now = services.time_service().sim_now time_span_until_wakeup = None sim_careers = self.owner.sim_info.career_tracker.careers if sim_careers: earliest_time = None for career in sim_careers.values(): wakeup_time = career.get_next_wakeup_time() while earliest_time is None or wakeup_time < earliest_time: earliest_time = wakeup_time if earliest_time is not None: time_span_until_wakeup = now.time_till_next_day_time( earliest_time) if time_span_until_wakeup is None: start_time = self._get_default_sleep_schedule_work_time( offset_time) time_span_until_wakeup = start_time - now if time_span_until_wakeup.in_ticks() <= 0: time_span_until_wakeup += TimeSpan( date_and_time.sim_ticks_per_day()) logger.assert_log(time_span_until_wakeup.in_ticks() > 0, 'time_span_until_wakeup occurs in the past.') return time_span_until_wakeup def _add_buff_callback(self, alarm_handle): buff = self._sleep_buff_alarms.get(alarm_handle) if not buff: logger.error( "Couldn't find alarm handle in _sleep_buff_alarms dict for sim:{}.", self.owner, owner='rez') return self._remove_sleep_schedule_buff() if buff and buff.buff_type: self._sleep_buff_handle = self.owner.add_buff(buff.buff_type) def _reset_alarms_callback(self, _): self.update_sleep_schedule() def _remove_sleep_schedule_buff(self): if self._sleep_buff_handle is not None: self.owner.remove_buff(self._sleep_buff_handle) self._sleep_buff_handle = None def _get_default_sleep_schedule_work_time(self, offset_time): now = services.time_service().sim_now if offset_time is not None: now += offset_time work_time = date_and_time.create_date_and_time( days=int(now.absolute_days()), hours=self.SLEEP_SCHEDULE.default_work_time.hour(), minutes=self.SLEEP_SCHEDULE.default_work_time.minute()) if work_time < now: work_time += date_and_time.create_time_span(days=1) return work_time @componentmethod def get_autonomy_state_setting(self) -> autonomy.settings.AutonomyState: return self._get_appropriate_autonomy_setting( autonomy.settings.AutonomyState) @componentmethod def get_autonomy_randomization_setting( self) -> autonomy.settings.AutonomyRandomization: return self._get_appropriate_autonomy_setting( autonomy.settings.AutonomyRandomization) @componentmethod def get_autonomy_settings(self): return self._autonomy_settings def _get_appropriate_autonomy_setting(self, setting_class): autonomy_service = services.autonomy_service() setting = autonomy_service.global_autonomy_settings.get_setting( setting_class) if setting != setting_class.UNDEFINED: return setting if self._role_tracker is not None: setting = self._role_tracker.get_autonomy_state() if setting != setting_class.UNDEFINED: return setting setting = self._autonomy_settings.get_setting(setting_class) if setting != setting_class.UNDEFINED: return setting household = self.owner.household if household: setting = household.autonomy_settings.get_setting(setting_class) if setting != setting_class.UNDEFINED: return setting setting = autonomy_service.default_autonomy_settings.get_setting( setting_class) if setting == setting_class.UNDEFINED: logger.error('Sim {} has an UNDEFINED autonomy setting!', self.owner, owner='rez') return setting def save(self, persistence_master_message): pass def load(self, state_component_message): pass @componentmethod def debug_reset_autonomy_alarm(self): self._schedule_next_full_autonomy_update() @componentmethod def debug_output_autonomy_timers(self, _connection): now = services.time_service().sim_now if self._last_user_directed_action_time is not None: sims4.commands.output( 'Last User-Directed Action: {} ({} ago)'.format( self._last_user_directed_action_time, now - self._last_user_directed_action_time), _connection) else: sims4.commands.output('Last User-Directed Action: None', _connection) if self._last_autonomous_action_time is not None: sims4.commands.output( 'Last Autonomous Action: {} ({} ago)'.format( self._last_autonomous_action_time, now - self._last_autonomous_action_time), _connection) else: sims4.commands.output('Last Autonomous Action: None', _connection) if self._full_autonomy_alarm_handle is not None: sims4.commands.output( 'Full Autonomy: {} from now'.format( self._full_autonomy_alarm_handle.get_remaining_time()), _connection) else: sims4.commands.output('Full Autonomy: None)', _connection) if len(self._autonomy_skip_sis) > 0: sims4.commands.output("Skipping autonomy due to the follow SI's:", _connection) for si in self._autonomy_skip_sis: sims4.commands.output('\t{}'.format(si), _connection) else: sims4.commands.output('Not skipping autonomy', _connection) @componentmethod def debug_get_autonomy_timers_gen(self): now = services.time_service().sim_now if self._full_autonomy_alarm_handle is not None: yield ('Full Autonomy', '{}'.format( self._full_autonomy_alarm_handle.get_remaining_time())) else: yield ('Full Autonomy', 'None') if self._last_user_directed_action_time is not None: yield ('Last User-Directed Action', '{} ({} ago)'.format( self._last_user_directed_action_time, now - self._last_user_directed_action_time)) if self._last_autonomous_action_time: yield ('Last Autonomous Action', '{} ({} ago)'.format( self._last_autonomous_action_time, now - self._last_autonomous_action_time)) if len(self._autonomy_skip_sis) > 0: yield ('Skipping Autonomy?', 'True') else: yield ('Skipping Autonomy?', 'False') @componentmethod def debug_update_autonomy_timer(self, mode): self._schedule_next_full_autonomy_update()
class _AmbientSourceGhost(_AmbientSource): GHOST_SITUATIONS = TunableTestedList( description= '\n A list of possible ghost situations, tested aginst the Sim we want to\n spawn.\n ', tunable_type=TunableReference( description= '\n The ghost situation to spawn.\n ', manager=services.get_instance_manager( sims4.resources.Types.SITUATION), pack_safe=True)) DESIRED_GHOST_COUNT_PER_URNSTONE = TunableCurve( description= '\n This curve describes the maximum number of ghosts we want in the world\n based on the number of valid urnstones in the world. If there are more\n urnstones than the maximum number tuned on the X axis, we will just use\n the final Y value.\n ', x_axis_name='Valid Urnstones', y_axis_name='Desired Ghost Count') WALKBY_ALLOWED_START_TIME = TunableTimeOfDay( description= '\n The time of the day (24hr) when NPC ghosts can start doing walkbys.\n ', default_hour=21) WALKBY_ALLOWED_DURATION = TunableRange( description= "\n The amount of time, in sim hours, past the 'Walkby Start Time' that the\n ghost walkbys can start.\n ", tunable_type=float, default=5, minimum=0, maximum=23) @classproperty def source_type(cls): return AmbientSourceType.SOURCE_GHOST def is_valid(self): return True @classmethod def _is_correct_time(cls): current_time = services.time_service().sim_now start_time = cls.WALKBY_ALLOWED_START_TIME end_time = start_time + clock.interval_in_sim_hours( cls.WALKBY_ALLOWED_DURATION) return current_time.time_between_day_times(start_time, end_time) def get_desired_number_of_sims(self): if not self._is_correct_time(): return 0 urnstones = sims.ghost.Ghost.get_valid_urnstones() if not urnstones: return 0 return self.DESIRED_GHOST_COUNT_PER_URNSTONE.get(len(urnstones)) def start_appropriate_situation(self, time_of_day=None): urnstones = sims.ghost.Ghost.get_valid_urnstones() sim_info = random.choice(urnstones).get_stored_sim_info() resolver = SingleSimResolver(sim_info) for situation_type in self.GHOST_SITUATIONS(resolver=resolver): if self._start_specific_situation(situation_type, sim_info=sim_info): return True return False def _create_standard_ambient_guest_list(self, situation_type, *, sim_info): guest_list = SituationGuestList(invite_only=True) guest_list.add_guest_info( SituationGuestInfo(sim_info.sim_id, situation_type.default_job(), RequestSpawningOption.MUST_SPAWN, BouncerRequestPriority.BACKGROUND_LOW)) return guest_list def get_gsi_description(self): return '(Ghost, {0}, {1})'.format(self.get_desired_number_of_sims(), self.get_current_number_of_sims())
class NPCHostedSituationService(Service): WELCOME_WAGON_TUNING = TunableTuple( description= '\n Tuning dedicated to started the welcome wagon.\n ', situation=TunableReference( description= '\n The welcome wagon situation.\n ', manager=services.get_instance_manager( sims4.resources.Types.SITUATION)), minimum_time_to_start_situation=TunableSimMinute( description= '\n The minimum amount of time since the service started that the\n welcome wagon will begin.\n ', default=60, minimum=0), available_time_of_day=TunableTuple( description= '\n The start and end times that determine the time that the welcome\n wagon can begin. This has nothing to do with the end time of the\n situation. The duration of the situation can last beyond the times\n tuned here.\n ', start_time=TunableTimeOfDay( description= '\n The start time that the welcome wagon can begin.\n ' ), end_time=TunableTimeOfDay( description= '\n The end time that the welcome wagon can begin.\n ' )), welcome_wagon_start_tests=TunableTestSet( description= '\n A test set that we will test against before starting the welcome wagon.\n ' )) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._welcome_wagon_alarm = None self._suspended = False def _start_welcome_wagon(self, _): situation_manager = services.get_zone_situation_manager() if situation_manager.is_user_facing_situation_running(): self._schedule_welcome_wagon() return active_household = services.active_household() for sim_info in active_household.can_live_alone_info_gen(): if sim_info.is_instanced(): break else: self._schedule_welcome_wagon() return resolver = GlobalResolver() if not NPCHostedSituationService.WELCOME_WAGON_TUNING.welcome_wagon_start_tests.run_tests( resolver): active_household.needs_welcome_wagon = False return narrative_service = services.narrative_service() welcome_wagon_situation = narrative_service.get_possible_replacement_situation( NPCHostedSituationService.WELCOME_WAGON_TUNING.situation) if welcome_wagon_situation is NPCHostedSituationService.WELCOME_WAGON_TUNING.situation: region = services.current_region() if region.welcome_wagon_replacement is not None: welcome_wagon_situation = region.welcome_wagon_replacement guest_list = welcome_wagon_situation.get_predefined_guest_list() if guest_list is None: active_household.needs_welcome_wagon = False return game_clock_services = services.game_clock_service() if game_clock_services.clock_speed == ClockSpeedMode.SUPER_SPEED3: game_clock_services.set_clock_speed(ClockSpeedMode.NORMAL) situation_id = situation_manager.create_situation( welcome_wagon_situation, guest_list=guest_list, user_facing=False, scoring_enabled=False) if not (situation_id is None or not welcome_wagon_situation.sets_welcome_wagon_flag): active_household.needs_welcome_wagon = False def on_all_households_and_sim_infos_loaded(self, client): self._schedule_welcome_wagon() def _schedule_welcome_wagon(self): parser = argparse.ArgumentParser() parser.add_argument('--no_welcome_wagon', default=False, action='store_true') (args, unused_args) = parser.parse_known_args() if args.no_welcome_wagon: return active_household = services.active_household() if not active_household.needs_welcome_wagon: return if services.current_zone_id() != active_household.home_zone_id: return venue = services.get_current_venue() if not venue.supports_welcome_wagon: return minimum_time = interval_in_sim_minutes( NPCHostedSituationService.WELCOME_WAGON_TUNING. minimum_time_to_start_situation) now = services.time_service().sim_now possible_time = now + minimum_time if possible_time.time_between_day_times( NPCHostedSituationService.WELCOME_WAGON_TUNING. available_time_of_day.start_time, NPCHostedSituationService. WELCOME_WAGON_TUNING.available_time_of_day.end_time): time_till_welcome_wagon = minimum_time else: time_till_welcome_wagon = now.time_till_next_day_time( NPCHostedSituationService.WELCOME_WAGON_TUNING. available_time_of_day.start_time) self._welcome_wagon_alarm = alarms.add_alarm(self, time_till_welcome_wagon, self._start_welcome_wagon) def suspend_welcome_wagon(self): self._suspended = True self._cancel_alarm() def resume_welcome_wagon(self): self._suspended = False self._schedule_welcome_wagon() def _cancel_alarm(self): if self._welcome_wagon_alarm is not None: alarms.cancel_alarm(self._welcome_wagon_alarm) def stop(self): super().stop() self._cancel_alarm
class HolidayTuning: HOLIDAY_SITUATION = TunablePackSafeReference( description='\n Reference to the holiday situation.\n ', manager=services.get_instance_manager(sims4.resources.Types.SITUATION)) HOLIDAY_JOB = TunablePackSafeReference( description='\n Holiday Situation Job.\n ', manager=services.get_instance_manager( sims4.resources.Types.SITUATION_JOB)) HOLIDAY_DURATION = TunableTimeSpan( description= '\n The number of hours the main holidays and surprise holidays run for.\n ', default_hours=22, locked_args={ 'days': 0, 'minutes': 0 }) HOLIDAY_TIME_OFF_REASON = TunableEnumEntry( description='\n The holiday time off reason.\n ', tunable_type=CareerTimeOffReason, default=CareerTimeOffReason.NO_TIME_OFF) MAIN_HOLIDAY_START_TIME = TunableTimeOfDay( description='\n The start time for main holidays.\n ', default_hour=6) HOLIDAY_NOTIFICATION_INFORMATION = TunableMapping( description= '\n Notification to be shown based on the medal achieved.\n ', key_type=TunableEnumEntry( description= '\n The medal achieved for the situation.\n ', tunable_type=SituationMedal, default=SituationMedal.GOLD), value_type=UiDialogNotification.TunableFactory( description= '\n The notification to be shown.\n 0 - Sim\n 1 - Holiday Name\n ' )) HOLIDAY_SCORING_INFORMATION = TunableList( description= '\n Information related to scoring the holiday situation based on the number\n of traditions a Sim LIKES or LOVES.\n \n Order is important. The first threshold that passes returns the maximum\n score associated with it. Because of this, order the thresholds from\n greater to lesser threshold values.\n \n If no thresholds pass, the situation will have an undefined maximum\n score.\n ', tunable=TunableTuple( description= '\n The number of traditions a Sim cares about that determines the \n maximum score to use and the rewards to be given for the holiday \n situation.\n ', max_score=Tunable( description= '\n The maximum score for the holiday situation. \n ', tunable_type=int, default=0), reward=TunableMapping( description= '\n Reward to be given based on the medal achieved.\n ', key_type=TunableEnumEntry( description= '\n The medal achieved for the situation.\n ', tunable_type=SituationMedal, default=SituationMedal.GOLD), value_type=TunableReference( description= '\n The reward to be given.\n ', manager=services.get_instance_manager( sims4.resources.Types.REWARD), pack_safe=True)), threshold=TunableThreshold( description= '\n The number of traditions that a Sim cares about that determines\n which score and rewards are used.\n ' ))) TRADITION_PREFERENCE_SCORE_MULTIPLIER = TunableMapping( description= '\n Map of tradition preference to score multiplier, that is used to\n modify the score a Sim receives when they complete a holiday tradition\n based on their preferences.\n ', key_type=TunableEnumEntry( description= "\n A Sim's tradition preference.\n ", tunable_type=TraditionPreference, default=TraditionPreference.LIKES), value_type=Tunable( description= '\n The score multiplier for the preference.\n ', tunable_type=float, default=1)) HOLIDAY_DISPLAY_DELAY = TunableTimeSpan( description= '\n The number of hours that elapses from the start of the holiday\n before UI shows the full UI. \n ', default_hours=3, locked_args={ 'days': 0, 'minutes': 0 })
class ServiceNpcHireable(ServiceNpc): INSTANCE_TUNABLES = { 'icon': TunableResourceKey( description= "\n The icon to be displayed in 'Hire a Service' UI\n ", resource_types=CompoundTypes.IMAGE, default=None, tuning_group=GroupNames.UI), 'cost_up_front': TunableRange( description= '\n The up front cost of this NPC per service session (AKA per day)\n in simoleons. This is always charged for the service if the\n service shows up.', tunable_type=int, default=0, minimum=0), 'cost_hourly': TunableRange( description= '\n The cost per hour of this NPC service. This is in addition to the\n cost up front. EX: if you have a service with 50 upfront cost and\n then 25 cost per hour. If the npc works for 1 hour, the total cost\n is 50 + 25 = 75 simoleons.', tunable_type=int, default=50, minimum=0), 'free_service_traits': TunableList( description= '\n If any Sim in the household has one of these traits, the service\n will be free.\n ', tunable=TunableReference(manager=services.trait_manager(), pack_safe=True)), 'bill_source': TunableEnumEntry( description= '\n The bill_source tied to this NPC Service. The cost for the service\n NPC will be applied to that bill_source in total cost of bills.\n Delinquency tests are grouped by bill_source.', tunable_type=AdditionalBillSource, default=AdditionalBillSource.Miscellaneous), '_recurring': OptionalTunable( description= '\n If enabled, when hiring this NPC, you can specify for them to be\n regularly scheduled and come every day or hire them one time.', tunable=TunableTuple( one_time_name=TunableLocalizedString( description= '\n Display name for this Service NPC type when recurring is false.\n Ex: for Maid, non recurring name is: One Time Maid', tuning_group=GroupNames.UI), recurring_name=TunableLocalizedString( description= '\n Display name for this Service NPC type when recurring is true. \n Ex: for Maid, recurring maid is: Scheduled Maid', tuning_group=GroupNames.UI))), '_fake_perform_minutes_per_object': TunableSimMinute( description= "\n If we're pretending this service npc went to your lot, and the fake\n perform tuning is run on the lot, this is the number of minutes we\n pretend it takes for the maid to clean each object.\n ", default=10, minimum=0), '_fake_perform_notification': OptionalTunable( description= '\n The notification to display when you return to your lot if this\n service NPC visited your lot while you were away. The two arguments\n available are the money charged directly to your household funds\n (in argument 0), the money billed to your household (in argument\n 1), and the total cost (in argument 2). So, you can use {0.Money},\n etc. in the notification.\n ', tunable=TunableUiDialogNotificationSnippet()), 'hire_interaction': TunableReference( description= '\n The affordance to push the sim making the call when hiring this\n service npc from a picker dialog from the phone.\n ', manager=services.affordance_manager()), 'bill_time_of_day': OptionalTunable( description= "\n If enabled, service NPC will charge at a specified tunable time.\n Otherwise service NPC will charge by default whenever the situation\n ends (full time service NPC's should be collecting this way).\n ", tunable=TunableTuple( description= '\n Time of day and failure to pay notification when the active\n household fails to pay for the service.\n Delinquent NPC quit notification will trigger whenever\n the NPC quits after bills go delinquent.\n Notification arguments available are money charged directly to \n your household funds (in argument 0), the money billed to your \n household (in argument 1), and the total cost (in argument 2).\n ', time_of_day=TunableTimeOfDay( description= '\n Time of day for butler to collect bills.\n ', default_hour=12), fail_to_pay_notification=TunableUiDialogNotificationSnippet(), delinquent_npc_quit_notification= TunableUiDialogNotificationSnippet()), enabled_name='specify_bill_time', disabled_name='use_situation_end_as_bill') } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @classmethod def try_charge_for_service(cls, household, cost): for sim in household.sim_info_gen(): if sim.trait_tracker.has_any_trait(cls.free_service_traits): cost = 0 break if cost > household.funds.money: billed_amount = cost - household.funds.money paid_amount = household.funds.money else: billed_amount = 0 paid_amount = cost first_instanced_sim = next( household.instanced_sims_gen( allow_hidden_flags=ALL_HIDDEN_REASONS), None) if not household.funds.try_remove( paid_amount, reason=Consts_pb2.TELEMETRY_INTERACTION_COST, sim=first_instanced_sim): billed_amount += paid_amount household.bills_manager.add_additional_bill_cost( cls.bill_source, billed_amount) return (0, billed_amount) if billed_amount > 0: household.bills_manager.add_additional_bill_cost( cls.bill_source, billed_amount) return (paid_amount, billed_amount) @classmethod def get_cost(cls, time_worked_in_hours, include_up_front_cost=True): cost = int(time_worked_in_hours * cls.cost_hourly) if include_up_front_cost: cost += cls.cost_up_front return cost @classmethod def auto_schedule_on_client_connect(cls): return False @classmethod def fake_perform(cls, household): num_modified = super().fake_perform(household) minutes_taken = num_modified * cls._fake_perform_minutes_per_object time_taken = create_time_span(minutes=minutes_taken) total_cost = cls.get_cost(time_taken.in_hours()) if total_cost > 0: (paid_amount, billed_amount) = cls.try_charge_for_service( household, total_cost) else: (paid_amount, billed_amount) = (0, 0) if cls._fake_perform_notification is not None: cls.display_payment_notification(household, paid_amount, billed_amount, cls._fake_perform_notification) return num_modified @classmethod def display_payment_notification(cls, household, paid_amount, billed_amount, notification): household_sim = next( household.instanced_sims_gen( allow_hidden_flags=ALL_HIDDEN_REASONS), None) if household_sim is not None: dialog = notification(household_sim) if dialog is not None: dialog.show_dialog( additional_tokens=(paid_amount, billed_amount, paid_amount + billed_amount))
class Region(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.region_manager()): REGION_DESCRIPTION_TUNING_MAP = TunableMapping( description= '\n A mapping between Catalog region description and tuning instance. This\n way we can find out what region description the current zone belongs to\n at runtime then grab its tuning instance.\n ', key_type=TunableRegionDescription( description= '\n Catalog-side Region Description.\n ', pack_safe=True, export_modes=ExportModes.All), value_type=TunableReference( description= "\n Region Tuning instance. This is retrieved at runtime based on what\n the active zone's region description is.\n ", pack_safe=True, manager=services.region_manager(), export_modes=ExportModes.All), key_name='RegionDescription', value_name='Region', tuple_name='RegionDescriptionMappingTuple', export_modes=ExportModes.All) INSTANCE_TUNABLES = { 'gallery_download_venue_map': TunableMapping( description= '\n A map from gallery venue to instanced venue. We need to be able to\n convert gallery venues into other venues that are only compatible\n with that region.\n ', key_type=TunableReference( description= '\n A venue type that exists in the gallery.\n ', manager=services.venue_manager(), export_modes=ExportModes.All, pack_safe=True), value_type=TunableReference( description= '\n The venue type that the gallery venue will become when it is\n downloaded into this region.\n ', manager=services.venue_manager(), export_modes=ExportModes.All, pack_safe=True), key_name='gallery_venue_type', value_name='region_venue_type', tuple_name='GalleryDownloadVenueMappingTuple', export_modes=ExportModes.All), 'compatible_venues': TunableList( description= '\n A list of venues that are allowed to be set by the player in this\n region.\n ', tunable=TunableReference( description= '\n A venue that the player can set in this region.\n ', manager=services.venue_manager(), export_modes=ExportModes.All, pack_safe=True), export_modes=ExportModes.All), 'tags': TunableList( description= '\n Tags that are used to group regions. Destination Regions will\n likely have individual tags, but Home/Residential Regions will\n share a tag.\n ', tunable=TunableEnumEntry( description= '\n A Tag used to group this region. Destination Regions will\n likely have individual tags, but Home/Residential Regions will\n share a tag.\n ', tunable_type=tag.Tag, default=tag.Tag.INVALID, pack_safe=True)), 'region_buffs': TunableList( description= '\n A list of buffs that are added on Sims while they are instanced in\n this region.\n ', tunable=TunableReference( description= '\n A buff that exists on Sims while they are instanced in this\n region.\n ', manager=services.buff_manager(), pack_safe=True)), 'store_travel_group_placed_objects': Tunable( description= '\n If checked, any placed objects while in a travel group will be returned to household inventory once\n travel group is disbanded.\n ', tunable_type=bool, default=False), 'travel_group_build_disabled_tooltip': TunableLocalizedString( description= '\n The string that will appear in the tooltip of the grayed out build\n mode button if build is being disabled because of a travel group in\n this region.\n ', allow_none=True, export_modes=ExportModes.All), 'sunrise_time': TunableTimeOfDay( description= '\n The time, in Sim-time, the sun rises in this region.\n ', default_hour=6, tuning_group=GroupNames.TIME), 'seasonal_sunrise_time': TunableMapping( description= '\n A mapping between season and sunrise time. If the current season\n is not found then we will default to the tuned sunrise time.\n ', key_type=TunableEnumEntry( description='\n The season.\n ', tunable_type=SeasonType, default=SeasonType.SUMMER), value_type=TunableTimeOfDay( description= '\n The time, in Sim-time, the sun rises in this region, in this\n season.\n ', default_hour=6, tuning_group=GroupNames.TIME)), 'sunset_time': TunableTimeOfDay( description= '\n The time, in Sim-time, the sun sets in this region.\n ', default_hour=20, tuning_group=GroupNames.TIME), 'seasonal_sunset_time': TunableMapping( description= '\n A mapping between season and sunset time. If the current season\n is not found then we will default to the tuned sunset time.\n ', key_type=TunableEnumEntry( description='\n The season.\n ', tunable_type=SeasonType, default=SeasonType.SUMMER), value_type=TunableTimeOfDay( description= '\n The time, in Sim-time, the sun sets in this region, in this\n season.\n ', default_hour=20, tuning_group=GroupNames.TIME)), 'provides_sunlight': Tunable( description= '\n If enabled, this region provides sunlight between the tuned Sunrise\n Time and Sunset Time. This is used for gameplay effect (i.e.\n Vampires).\n ', tunable_type=bool, default=True, tuning_group=GroupNames.TIME), 'weather': TunableMapping( description= '\n Forecasts for this region for the various seasons\n ', key_type=TunableEnumEntry( description='\n The Season.\n ', tunable_type=SeasonType, default=SeasonType.SPRING), value_type=TunableWeatherSeasonalForecastsReference( description= '\n The forecasts for the season by part of season\n ', pack_safe=True)), 'weather_supports_fresh_snow': Tunable( description= '\n If enabled, this region supports fresh snow.\n ', tunable_type=bool, default=True), 'seasonal_parameters': TunableMapping( description='\n ', key_type=TunableEnumEntry( description= '\n The parameter that we wish to change.\n ', tunable_type=SeasonParameters, default=SeasonParameters.LEAF_ACCUMULATION), value_type=TunableList( description= '\n A list of the different seasonal parameter changes that we want to\n send over the course of a year.\n ', tunable=TunableTuple( season=TunableEnumEntry( description= '\n The Season that this change is in.\n ', tunable_type=SeasonType, default=SeasonType.SPRING), time_in_season=TunableRange( description= '\n The time within the season that this will occur.\n ', tunable_type=float, minimum=0.0, maximum=1.0, default=0.0), value=Tunable( description= '\n The value that we will set this parameter to in the\n season\n ', tunable_type=float, default=0.0))), verify_tunable_callback=verify_seasonal_parameters), 'fishing_data': OptionalTunable( description= '\n If enabled, define all of the data for fishing locations in this region.\n Only used if objects are tuned to use region fishing data.\n ', tunable=TunableFishingDataSnippet()), 'welcome_wagon_replacement': OptionalTunable( description= '\n If enabled then we will replace the Welcome Wagon with a new situation.\n \n If the narrative is also set to replace the welcome wagon that will take precedent over this replacement.\n ', tunable=TunableReference( description= '\n The situation we will use to replace the welcome wagon.\n ', manager=services.get_instance_manager( sims4.resources.Types.SITUATION))) } @classmethod def _cls_repr(cls): return "Region: <class '{}.{}'>".format(cls.__module__, cls.__name__) @classmethod def is_region_compatible(cls, region_instance, ignore_tags=False): if region_instance is cls or region_instance is None: return True if ignore_tags: return False for tag in cls.tags: if tag in region_instance.tags: return True return False @classmethod def is_sim_info_compatible(cls, sim_info): other_region = get_region_instance_from_zone_id(sim_info.zone_id) if cls.is_region_compatible(other_region): return True else: travel_group_id = sim_info.travel_group_id if travel_group_id: travel_group = services.travel_group_manager().get( travel_group_id) if travel_group is not None and not travel_group.played: return True return False @classmethod def get_sunrise_time(cls): season_service = services.season_service() if season_service is None: return cls.sunrise_time return cls.seasonal_sunrise_time.get(season_service.season, cls.sunrise_time) @classmethod def get_sunset_time(cls): season_service = services.season_service() if season_service is None: return cls.sunset_time return cls.seasonal_sunset_time.get(season_service.season, cls.sunset_time)
class ApartmentZoneDirectorMixin(SchedulingZoneDirectorMixin): COMMON_AREA_CLEANUP = TunableTuple( description= '\n Tuning to clear out objects from the common area to prevent trash\n and what not from accumulating.\n ', actions=ModifyAllLotItems.TunableFactory( description= '\n Modifications to make to objects on the common area of apartments.\n ' ), time_of_day=TunableTimeOfDay( description= '\n Time of day to run cleanup.\n ', default_hour=4)) NEW_TENANT_CLEANUP = ModifyAllLotItems.TunableFactory( description= '\n Modifications to make to objects when a new tenant moves in.\n Example: We want to fix and reset all apartment problems when new\n tenants move in.\n ' ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._common_area_cleanup_alarm_handle = None @property def forward_ungreeted_front_door_interactions_to_terrain(self): return False def on_startup(self): super().on_startup() now = services.time_service().sim_now time_span = now.time_till_next_day_time( ApartmentZoneDirectorMixin.COMMON_AREA_CLEANUP.time_of_day) repeating_time_span = create_time_span(days=1) handle = add_alarm(self, time_span, lambda _: self._run_common_area_cleanup(), repeating=True, repeating_time_span=repeating_time_span) self._common_area_cleanup_alarm_handle = handle def on_shutdown(self): self._common_area_cleanup_alarm_handle.cancel() self._common_area_cleanup_alarm_handle = None super().on_shutdown() def on_cleanup_zone_objects(self): super().on_cleanup_zone_objects() persistence_service = services.get_persistence_service() plex_service = services.get_plex_service() plex_zone_ids = plex_service.get_plex_zones_in_group( services.current_zone_id()) last_save_ticks = None for zone_id in plex_zone_ids: zone_data = persistence_service.get_zone_proto_buff(zone_id) gameplay_zone_data = zone_data.gameplay_zone_data if not gameplay_zone_data.HasField('game_time'): continue if not last_save_ticks is None: if last_save_ticks < gameplay_zone_data.game_time: last_save_ticks = gameplay_zone_data.game_time last_save_ticks = gameplay_zone_data.game_time if last_save_ticks is not None: last_save_time = DateAndTime(last_save_ticks) next_cleanup_time = last_save_time.time_of_next_day_time( ApartmentZoneDirectorMixin.COMMON_AREA_CLEANUP.time_of_day) if next_cleanup_time < services.time_service().sim_now: self._run_common_area_cleanup() owning_household = services.owning_household_of_active_lot() if owning_household is not None and not owning_household.has_home_zone_been_active( ): self._run_new_tenant_cleanup() def _run_new_tenant_cleanup(self): actions = ApartmentZoneDirectorMixin.NEW_TENANT_CLEANUP() actions.modify_objects() def _run_common_area_cleanup(self): actions = ApartmentZoneDirectorMixin.COMMON_AREA_CLEANUP.actions() plex_service = services.get_plex_service() def object_criteria(obj): return plex_service.get_plex_zone_at_position( obj.position, obj.level) is None actions.modify_objects(object_criteria=object_criteria)
class Season(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager( sims4.resources.Types.SEASON)): INSTANCE_TUNABLES = { 'season_icon': TunableIcon( description="\n The season's icon.\n ", export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'season_name': TunableLocalizedString( description="\n The season's name.\n ", export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'season_length_content': TunableMapping( description= '\n A mapping of season length option to the content contained within.\n ', key_type=TunableEnumEntry(tunable_type=SeasonLength, default=SeasonLength.NORMAL), value_type=SeasonalContent.TunableFactory()), 'screen_slam': OptionalTunable( description= '\n If enabled, trigger this Screen Slam when transitioning to this season.\n ', tunable=TunableTuple( description= '\n The screenslam to trigger, and hour of the day when it should\n appear to the users.\n ', slam=TunableScreenSlamSnippet(), trigger_time=TunableTimeOfDay(default_hour=6))), 'whim_set': OptionalTunable( description= '\n If enabled then this season will offer a whim set to the Sim\n when it is that season.\n ', tunable=TunableReference( description= '\n A whim set that is active when this season is active.\n ', manager=services.get_instance_manager( sims4.resources.Types.ASPIRATION), class_restrictions=('ObjectivelessWhimSet', ))) } def __init__(self, start_time, **kwargs): super().__init__(**kwargs) self._start_time = start_time self._length_option = None self._length_span = None self._content_data = None self._mid_season_begin = None self._absolute_mid = None self._late_season_begin = None self._end_of_season = None def __contains__(self, date_and_time): return self._start_time <= date_and_time < self._end_of_season @property def info(self): holiday_formatted = '\n\t'.join([ '{} on {}'.format(holiday.__name__, day_of_season.date_and_time) for (holiday, day_of_season) in self.get_holiday_dates() ]) return 'Resource: {}\nLength: {}\nStart: {}\n\tMid-Season Period: {}\n\tAbsolute Mid-Season: {}\n\tLate-Season Period: {}\nEnd: {}\nHolidays:\n\t{}'.format( self.__class__, self._length_span, self._start_time, self._mid_season_begin, self._absolute_mid, self._late_season_begin, self._end_of_season, holiday_formatted) @property def start_time(self): return self._start_time @property def length(self): return self._length_span @property def end_time(self): return self._end_of_season @property def midpoint_time(self): return self._absolute_mid def get_date_at_season_progress(self, progress): progress = clamp(0, progress, 1) return self._start_time + self._length_span * progress def get_position(self, date_and_time): return date_and_time - self._start_time def get_segment(self, date_and_time): if not self._verify_in_season(date_and_time): return if date_and_time < self._mid_season_begin: return SeasonSegment.EARLY if date_and_time >= self._late_season_begin: return SeasonSegment.LATE return SeasonSegment.MID def get_progress(self, date_and_time): if not self._verify_in_season(date_and_time): return current_ticks = self.get_position(date_and_time).in_ticks() total_ticks = self._length_span.in_ticks() return current_ticks / total_ticks def get_screen_slam_trigger_time(self): if self.screen_slam is None: return return self._start_time.time_of_next_day_time( self.screen_slam.trigger_time) def _verify_in_season(self, date_and_time): within_season = date_and_time in self if not within_season: seasons_logger.error( 'Provided time {} is not within the current season, which is from {} to {}', date_and_time, self._start_time, self._end_of_season) return within_season def set_length_option(self, length_option): if self._length_option == length_option: return self._length_option = length_option self._length_span = SeasonsTuning.SEASON_LENGTH_OPTIONS[length_option]( ) self._calculate_important_dates() def _calculate_important_dates(self): self._content_data = self.season_length_content[self._length_option] self._mid_season_begin = self._start_time + self._content_data.segments.early_season_length( ) self._absolute_mid = self.get_date_at_season_progress(0.5) self._late_season_begin = self._start_time + ( self._length_span - self._content_data.segments.late_season_length()) self._end_of_season = self._start_time + self._length_span def get_holiday_dates(self): holidays_in_season = [] for (holiday, season_times) in self._content_data.holidays.items(): holidays_in_season.extend( iter((holiday, day_of_season(self._start_time)) for day_of_season in season_times)) return holidays_in_season def get_all_holiday_data(self): holidays_data = [] for season_length in SeasonLength: for ( holiday, season_times ) in self.season_length_content[season_length].holidays.items(): holidays_data.extend( iter((season_length, holiday, day(date_and_time.DATE_AND_TIME_ZERO).day_of_season) for day in season_times)) return holidays_data def get_holidays(self, season_length): return set(self._content_data.holidays.keys())
class CountdownState(CommonSituationState): FACTORY_TUNABLES = { 'countdown_affordance': TunableReference(manager=services.affordance_manager()), 'count_mixer': TunableReference(manager=services.affordance_manager()), 'celebrate_time': TunableTimeOfDay( description='\n Time of Day to Celebrate\n ', default_hour=0), 'time_to_start_count': TunableTimeOfDay( description= '\n Time to start performing the Count.\n ', default_hour=11, default_minute=30), 'interval_between_counts': TunableSimMinute( description= '\n The interval between each count animation.\n ', minimum=1, default=5) } def __init__(self, *args, countdown_affordance=None, count_mixer=None, celebrate_time=None, time_to_start_count=None, interval_between_counts=None, **kwargs): super().__init__(*args, **kwargs) self.countdown_affordance = countdown_affordance self.count_mixer = count_mixer self.celebrate_time = celebrate_time self.time_to_start_count = time_to_start_count self.interval_between_counts = interval_between_counts self._celebrate_timer = None self._count_timer = None def _count_callback(self, _): for sim in self.owner.all_sims_in_situation_gen(): parent_si = sim.si_state.get_si_by_affordance( self.countdown_affordance) if parent_si is not None: interaction_context = InteractionContext( sim, InteractionSource.PIE_MENU, Priority.Critical) aop = AffordanceObjectPair(self.count_mixer, None, self.countdown_affordance, parent_si) aop.test_and_execute(interaction_context) def _celebrate_callback(self, _): self._change_state(self.owner.celebrate_state()) def on_activate(self, reader=None): super().on_activate(reader) now = services.game_clock_service().now() time_till_first_count = now.time_till_next_day_time( self.time_to_start_count) time_till_celebration = now.time_till_next_day_time( self.celebrate_time) repeat_time_span = clock.interval_in_sim_minutes( self.interval_between_counts) if time_till_first_count > time_till_celebration: time_of_first_count = now + time_till_first_count + clock.interval_in_sim_days( -1) time_since_first_count = now - time_of_first_count time_of_next_count = time_of_first_count + repeat_time_span * ( int(time_since_first_count.in_ticks() / repeat_time_span.in_ticks()) + 1) time_till_first_count = time_of_next_count - now self._count_timer = alarms.add_alarm( self, time_till_first_count, self._count_callback, repeating=True, repeating_time_span=repeat_time_span) self._celebrate_timer = alarms.add_alarm(self, time_till_celebration, self._celebrate_callback) def on_deactivate(self): super().on_deactivate() if self._count_timer is not None: alarms.cancel_alarm(self._count_timer) self._count_timer = None if self._celebrate_timer is not None: alarms.cancel_alarm(self._celebrate_timer) self._celebrate_timer = None