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])
Example #2
0
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
Example #3
0
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)
Example #6
0
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)
Example #8
0
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
Example #9
0
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)
Example #11
0
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))
Example #12
0
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
Example #13
0
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)
Example #14
0
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
Example #17
0
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()
Example #18
0
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
Example #20
0
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
        })
Example #21
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))
Example #22
0
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)
Example #24
0
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